Skip to content

Instantly share code, notes, and snippets.

@RomkeVdMeulen
Last active November 10, 2022 13:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RomkeVdMeulen/f774324202d3fb8b710e7e2b1dcfdeb0 to your computer and use it in GitHub Desktop.
Save RomkeVdMeulen/f774324202d3fb8b710e7e2b1dcfdeb0 to your computer and use it in GitHub Desktop.
@queuedOperation TypeScript method decorator: http://romkevandermeulen.nl/2018/01/27/queued-operation-decorator.html
interface OperationQueue {
queue: Promise<void>;
}
export function makeOperationQueue(): OperationQueue {
return {queue: Promise.resolve()};
}
export function queuedOperation(operationQueue: OperationQueue) {
return function(_target: any, _key: string, descriptor: TypedPropertyDescriptor<(...args: any[]) => any>) {
const method = descriptor.value!;
descriptor.value = function(...args: any[]) {
const operation = operationQueue.queue.then(() => {
return method.apply(this, args);
});
operationQueue.queue = operation.catch(() => {});
return operation;
};
return descriptor;
};
}
describe("makeOperationQueue() / @queuedOperation", () => {
it("puts calls to async methods in a queue", async () => {
const operationQueue = makeOperationQueue();
class QueuedOperations {
started: Array<{name: string, resolve: () => void}> = [];
get startedMethods() {
return this.started.map(o => o.name);
}
waitForNumberStarted(number: number) {
return new Promise(resolve => {
const check = () => {
if (this.started.length === number) {
resolve();
} else {
setTimeout(() => check(), 1);
}
};
check();
});
}
@queuedOperation(operationQueue)
queuedOne() {
return new Promise(res => {
this.started.push({name: "one", resolve: res});
});
}
@queuedOperation(operationQueue)
queuedTwo() {
return new Promise(res => {
this.started.push({name: "two", resolve: res});
});
}
unqueued() {
return new Promise(res => {
this.started.push({name: "unqueued", resolve: res});
});
}
}
const instance = new QueuedOperations();
expect(instance.startedMethods).to.be.empty;
instance.queuedOne();
await instance.waitForNumberStarted(1);
expect(instance.startedMethods).to.deep.equal(["one"]);
instance.queuedTwo();
instance.queuedOne();
instance.unqueued();
expect(instance.startedMethods).to.deep.equal(["one", "unqueued"]);
instance.started[0].resolve();
await instance.waitForNumberStarted(3);
expect(instance.startedMethods).to.deep.equal(["one", "unqueued", "two"]);
instance.started[2].resolve();
await instance.waitForNumberStarted(4);
expect(instance.startedMethods).to.deep.equal(["one", "unqueued", "two", "one"]);
});
it("doesn't interupt the queue if a method returns a rejected promise", async () => {
const operationQueue = makeOperationQueue();
class QueuedWithErrors {
started: string[] = [];
@queuedOperation(operationQueue)
async noError() {
this.started.push("noError");
}
@queuedOperation(operationQueue)
async withError() {
this.started.push("withError");
throw new Error("Error during operation");
}
waitForNumberStarted(number: number) {
return new Promise(resolve => {
const check = () => {
if (this.started.length === number) {
resolve();
} else {
setTimeout(() => check(), 1);
}
};
check();
});
}
}
const instance = new QueuedWithErrors();
instance.withError().catch(() => {}); // prevents "unhandled rejection Error"
instance.noError();
await instance.waitForNumberStarted(2);
expect(instance.started).to.deep.equal(["withError", "noError"]);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment