Skip to content

Commit

Permalink
Simplify API and add ability to have multiple queues
Browse files Browse the repository at this point in the history
  • Loading branch information
DavisVaughan committed Sep 5, 2024
1 parent 826cb1d commit 285bcfa
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 69 deletions.
21 changes: 2 additions & 19 deletions apps/vscode/src/host/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ExtensionHost, HostWebviewPanel, HostStatementRangeProvider } from '.';
import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocument, pythonWithReticulate } from './executors';
import { MarkdownEngine } from '../markdown/engine';
import { virtualDoc, virtualDocUri, adjustedPosition } from "../vdoc/vdoc";
import { TaskQueue } from './queue';
import { TaskQueueManager } from './manager';

declare global {
function acquirePositronApi() : hooks.PositronApi;
Expand Down Expand Up @@ -76,24 +76,7 @@ export function hooksExtensionHost() : ExtensionHost {
}
}

// Construct a new task that calls our `callback` when its our turn
const task = TaskQueue.instance.task(callback);

// Construct a promise that resolves when our task finishes
const finished = new Promise<void>((resolve, _reject) => {
const handle = TaskQueue.instance.onDidFinishTask((id) => {
if (task.id === id) {
handle.dispose();
resolve();
}
});
});

// Push the task, which may immediately run it, and then wait for it to finish.
// Using a task queue ensures that another call to `execute()` can't interleave
// its own executions while we `await` between `blocks`.
await TaskQueue.instance.push(task);
await finished;
await TaskQueueManager.instance.enqueue(language, callback);
},
executeSelection: async () : Promise<void> => {
await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', {languageId: language});
Expand Down
77 changes: 77 additions & 0 deletions apps/vscode/src/host/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* manager.ts
*
* Copyright (C) 2024 by Posit Software, PBC
*
* Unless you have received this program directly from Posit Software pursuant
* to the terms of a commercial license agreement with Posit Software, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/

import { Disposable } from 'vscode';
import { TaskCallback, TaskQueue } from './queue';

/**
* Manager of multiple task queues
*
* A singleton class that constructs and manages multiple `TaskQueue`s, identified by their `key`.
*/
export class TaskQueueManager implements Disposable {
/// Singleton instance
private static _instance: TaskQueueManager;

/// Maps a `key` to its `queue`
private _queues = new Map<string, TaskQueue>();

/**
* Constructor
*
* Private since we only want one of these per `key`. Access using `instance()` instead.
*/
private constructor() { }

/**
* Accessor for the singleton instance
*
* Creates it if it doesn't exist.
*/
static get instance(): TaskQueueManager {
if (!TaskQueueManager._instance) {
// Initialize manager if we've never accessed it
TaskQueueManager._instance = new TaskQueueManager();
}

return TaskQueueManager._instance;
}

/**
* Enqueue a `callback` for execution on `key`'s task queue
*
* Returns a promise that resolves when the task finishes
*/
async enqueue(key: string, callback: TaskCallback): Promise<void> {
let queue = this._queues.get(key);

if (queue === undefined) {
// If we've never initialized this key's queue, do so now
queue = new TaskQueue();
this._queues.set(key, queue);
}

return queue.enqueue(callback);
}

/**
* Disposal method
*
* Never called in practice, because this is a singleton and effectively a global.
*/
dispose(): void {
this._queues.forEach((queue) => queue.dispose());
}
}
95 changes: 45 additions & 50 deletions apps/vscode/src/host/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,70 +23,46 @@ export interface Task {
callback: TaskCallback
}

/**
* A task queue that maintains the ordering of tasks submitted to the queue
*/
export class TaskQueue implements Disposable {
/// Singleton instance
private static _instance: TaskQueue;

private _id: TaskId = 0;
private _tasks: Task[] = [];
private _running = false;

private readonly _onDidFinishTask = new EventEmitter<TaskId>();
onDidFinishTask = this._onDidFinishTask.event;
private readonly onDidFinishTask = this._onDidFinishTask.event;

/**
* Disposal method
*
* Not currently used since the singleton is effectively a global variable.
*/
dispose(): void {
this._onDidFinishTask.dispose();
}

/**
* Constructor
*
* Private since we only want one of these. Access using `instance()` instead.
* Enqueue a `callback` for execution
*
* Returns a promise that resolves when the task finishes
*/
private constructor() { }

/**
* Accessor for the singleton instance
*
* Creates it if it doesn't exist.
*/
static get instance(): TaskQueue {
if (!TaskQueue._instance) {
TaskQueue._instance = new TaskQueue();
}
return TaskQueue._instance;
}

/**
* Construct a new `Task` that can be pushed onto the queue
*/
task(callback: TaskCallback): Task {
const id = this.id();
return { id, callback }
}

/**
* Retrives an `id` to be used with the next task
*/
private id(): TaskId {
let id = this._id;
this._id++;
return id;
}

/**
* Pushes a `task` into the queue. Immediately runs it if nothing else is running.
*/
async push(task: Task) {
async enqueue(callback: TaskCallback): Promise<void> {
// Create an official task out of this callback
const task = this.task(callback);

// Create a promise that resolves when the task is done
const out = new Promise<void>((resolve, _reject) => {
const handle = this.onDidFinishTask((id) => {
if (task.id === id) {
handle.dispose();
resolve();
}
});
});

// Put the task on the back of the queue.
// Immediately run it if possible.
this._tasks.push(task);

// Immediately run the task if possible
this.run();

return out;
}

/**
Expand All @@ -101,7 +77,8 @@ export class TaskQueue implements Disposable {
return;
}

const task = this._tasks.pop();
// Pop off the first task in the queue
const task = this._tasks.shift();

if (task === undefined) {
// Nothing to run right now
Expand All @@ -114,10 +91,28 @@ export class TaskQueue implements Disposable {
await task.callback();
} finally {
this._running = false;
// Let the promise know this task is done
this._onDidFinishTask.fire(task.id);
}

// Run next task if one is in the queue
this.run();
}

/**
* Construct a new `Task` that can be pushed onto the queue
*/
private task(callback: TaskCallback): Task {
const id = this.id();
return { id, callback }
}

/**
* Retrives an `id` to be used with the next task
*/
private id(): TaskId {
let id = this._id;
this._id++;
return id;
}
}

0 comments on commit 285bcfa

Please sign in to comment.