From 8b24438e3646988f1b72d171432b683a42c86048 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:25:28 -0700 Subject: [PATCH 1/6] chore: Add implementation for async task queue. --- .../__tests__/async/AsyncTaskQueue.test.ts | 151 ++++++++++++++++ .../sdk-client/src/async/AsyncTaskQueue.ts | 170 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts create mode 100644 packages/shared/sdk-client/src/async/AsyncTaskQueue.ts diff --git a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts new file mode 100644 index 0000000000..1b9b40f42c --- /dev/null +++ b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts @@ -0,0 +1,151 @@ +import { AsyncTaskQueue } from '../../src/async/AsyncTaskQueue'; + +it.each([true, false])('executes the initial task it is given: shedable: %s', async (shedable) => { + const queue = new AsyncTaskQueue(); + const task = jest.fn().mockResolvedValue('test'); + const result = await queue.execute(task, shedable); + expect(queue.pendingCount()).toBe(0); + expect(result).toEqual({ + status: 'complete', + result: 'test', + }); + expect(task).toHaveBeenCalled(); +}); + +it.each([true, false])('executes the next task in the queue when the previous task completes: shedable: %s', async (shedable) => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockResolvedValue('test1'); + const task2 = jest.fn().mockResolvedValue('test2'); + const promise1 = queue.execute(task1, shedable); + const promise2 = queue.execute(task2, shedable); + // We have not awaited, so there has not been an opportunity to execute any tasks. + expect(queue.pendingCount()).toBe(1); + + const [result1, result2] = await Promise.all([promise1, promise2]); + expect(result1).toEqual({ + status: 'complete', + result: 'test1', + }); + expect(result2).toEqual({ + status: 'complete', + result: 'test2', + }); + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); +}); + +it('can shed pending shedable tasks', async () => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockResolvedValue('test1'); + const task2 = jest.fn().mockResolvedValue('test2'); + const task3 = jest.fn().mockResolvedValue('test3'); + const promise1 = queue.execute(task1, true); + const promise2 = queue.execute(task2, true); + const promise3 = queue.execute(task3, true); + + const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]); + expect(result1).toEqual({ + status: 'complete', + result: 'test1', + }); + expect(result2).toEqual({ + status: 'shed', + }); + expect(result3).toEqual({ + status: 'complete', + result: 'test3', + }); + expect(task1).toHaveBeenCalled(); + expect(task2).not.toHaveBeenCalled(); + expect(task3).toHaveBeenCalled(); +}); + +it('does not shed pending non-shedable tasks', async () => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockResolvedValue('test1'); + const task2 = jest.fn().mockResolvedValue('test2'); + const task3 = jest.fn().mockResolvedValue('test3'); + const promise1 = queue.execute(task1, false); + const promise2 = queue.execute(task2, false); + const promise3 = queue.execute(task3, false); + + const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]); + expect(result1).toEqual({ + status: 'complete', + result: 'test1', + }); + expect(result2).toEqual({ + status: 'complete', + result: 'test2', + }); + expect(result3).toEqual({ + status: 'complete', + result: 'test3', + }); + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); + expect(task3).toHaveBeenCalled(); +}); + +it('can handle errors from tasks', async () => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockRejectedValue(new Error('test')); + const task2 = jest.fn().mockResolvedValue('test2'); + const promise1 = queue.execute(task1, true); + const promise2 = queue.execute(task2, true); + const [result1, result2] = await Promise.all([promise1, promise2]); + expect(result1).toEqual({ + status: 'error', + error: new Error('test'), + }); + expect(result2).toEqual({ + status: 'complete', + result: 'test2', + }); + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); +}); + +it('handles mix of shedable and non-shedable tasks correctly', async () => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockResolvedValue('test1'); + const task2 = jest.fn().mockResolvedValue('test2'); + const task3 = jest.fn().mockResolvedValue('test3'); + const task4 = jest.fn().mockResolvedValue('test4'); + + // Add tasks in order: shedable, non-shedable, shedable, non-shedable + const promise1 = queue.execute(task1, true); + const promise2 = queue.execute(task2, false); + const promise3 = queue.execute(task3, true); + const promise4 = queue.execute(task4, false); + + const [result1, result2, result3, result4] = await Promise.all([promise1, promise2, promise3, promise4]); + + // First task should complete + expect(result1).toEqual({ + status: 'complete', + result: 'test1', + }); + + // Second task should complete (not shedable) + expect(result2).toEqual({ + status: 'complete', + result: 'test2', + }); + + // Third task should be shed + expect(result3).toEqual({ + status: 'shed', + }); + + // Fourth task should complete + expect(result4).toEqual({ + status: 'complete', + result: 'test4', + }); + + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); + expect(task3).not.toHaveBeenCalled(); + expect(task4).toHaveBeenCalled(); +}); diff --git a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts new file mode 100644 index 0000000000..ba4be6146d --- /dev/null +++ b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts @@ -0,0 +1,170 @@ +import { LDLogger } from "@launchdarkly/js-sdk-common"; + +/** + * Represents a task that has been shed from the queue. + * This task will never be executed. + */ +export interface ShedTask { + status: 'shed'; +} + +/** + * Represents a task that has been ran to completion. + */ +export interface CompletedTask { + status: 'complete'; + result: TTaskResult +} + +/** + * Represents a task that has errored. + */ +export interface ErroredTask { + status: 'error'; + error: Error +} + +/** + * Represents the result of a task. + */ +export type TaskResult = CompletedTask | ErroredTask | ShedTask; + +/** + * Represents a pending task. This encapsulates the async function that needs to be executed as well as a promise that represents its state. + * The promise is not directly the promise associated with the async function, because we will not execute the async function until some point in the future, if at all. + **/ +interface PendingTask { + shedable: boolean; + execute: () => void; + shed: () => void; + promise: Promise>; +} + +const error = new Error('Task has already been executed or shed. This is likely an implementation error. The task will not be executed again.'); + +/** + * Creates a pending task. + * @param task The async function to execute. + * @param shedable Whether the task can be shed from the queue. + * @returns A pending task. + */ +function makePending(task: () => Promise, _logger?: LDLogger, shedable: boolean = false): PendingTask { + let res: (value: TaskResult) => void; + + const promise = new Promise>((resolve, reject) => { + res = resolve; + }); + + let executedOrShed = false; + return { + execute: () => { + if (executedOrShed) { + // This should never happen. If it does, then it represents an implementation error in the SDK. + _logger?.error(error); + } + executedOrShed = true; + task() + .then(result => res({ status: 'complete', result })) + .catch(error => res({ status: 'error', error })); + }, + shed: () => { + if (executedOrShed) { + // This should never happen. If it does, then it represents an implementation error in the SDK. + _logger?.error(error); + } + executedOrShed = true; + res({ status: 'shed' }); + }, + promise, + shedable, + } +} + +/** + * An asynchronous task queue with the ability to replace pending tasks. + * + * This is useful when you have asynchronous operations which much execute in order, and for cases where intermediate + * operations can be discarded. + * + * For instance, the SDK can only have one active context at a time, if you request identification of many contexts, + * then the ultimate state will be based on the last request. The intermediate identifies can be discarded. + * + * This class will always begin execution of the first item added to the queue, at that point the item itself is not + * queued, but active. If another request is made while that item is still active, then it is added to the queue. + * A third request would then replace the second request if the second request had not yet become active, and it was + * shedable. + * + * Once a task is active the queue will complete it. It doesn't cancel tasks that it has started, but it can shed tasks + * that have not started. + * + * TTaskResult Is the return type of the task to be executed. Tasks accept no parameters. So if you need parameters + * you should use a lambda to capture them. + * + * Exceptions from tasks are always handled and the execute method will never reject a promise. + * + * Queue management should be done synchronously. There should not be asynchronous operations between checking the queue + * and acting on the results of said check. + */ +export class AsyncTaskQueue { + private _activeTask?: Promise>; + private _queue: PendingTask[] = []; + + constructor(private readonly _logger?: LDLogger) { } + + /** + * Execute a task using the queue. + * + * @param task The async function to execute. + * @param shedable Whether the task can be shed from the queue. + * @returns A promise that resolves to the result of the task. + */ + execute(task: () => Promise, shedable: boolean = false): Promise> { + const pending = makePending(task, this._logger, shedable); + + if (!this._activeTask) { + this._activeTask = pending.promise.finally(() => { + this._activeTask = undefined; + this._checkPending(); + }); + pending.execute(); + } else { + // If the last pending task is shedable, we need to shed it before adding the new task. + if (this._queue[this._queue.length - 1]?.shedable) { + this._queue.pop()?.shed(); + } + this._queue.push(pending); + } + + return pending.promise; + } + + private _checkPending() { + // There is an existing active task, so we don't need to do anything. + if (this._activeTask) { + return; + } + + // There are pending tasks, so we need to execute the next one. + if (this._queue.length > 0) { + let nextTask = this._queue.shift()!; + + this._activeTask = nextTask.promise.finally(() => { + this._activeTask = undefined; + this._checkPending(); + }); + + nextTask.execute(); + } + } + + /** + * Returns the number of pending tasks in the queue. + * Intended for use for testing purposes only. + * + * @internal + * @returns The number of pending tasks in the queue. + */ + public pendingCount(): number { + return this._queue.length; + } +} \ No newline at end of file From 7f5fd64bdcfc089174e7020edf308298065f2955 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:31:15 -0700 Subject: [PATCH 2/6] Lint --- .../__tests__/async/AsyncTaskQueue.test.ts | 64 ++++++++++-------- .../sdk-client/src/async/AsyncTaskQueue.ts | 65 +++++++++++-------- 2 files changed, 73 insertions(+), 56 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts index 1b9b40f42c..0aa2c9c499 100644 --- a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts +++ b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts @@ -12,27 +12,30 @@ it.each([true, false])('executes the initial task it is given: shedable: %s', as expect(task).toHaveBeenCalled(); }); -it.each([true, false])('executes the next task in the queue when the previous task completes: shedable: %s', async (shedable) => { - const queue = new AsyncTaskQueue(); - const task1 = jest.fn().mockResolvedValue('test1'); - const task2 = jest.fn().mockResolvedValue('test2'); - const promise1 = queue.execute(task1, shedable); - const promise2 = queue.execute(task2, shedable); - // We have not awaited, so there has not been an opportunity to execute any tasks. - expect(queue.pendingCount()).toBe(1); +it.each([true, false])( + 'executes the next task in the queue when the previous task completes: shedable: %s', + async (shedable) => { + const queue = new AsyncTaskQueue(); + const task1 = jest.fn().mockResolvedValue('test1'); + const task2 = jest.fn().mockResolvedValue('test2'); + const promise1 = queue.execute(task1, shedable); + const promise2 = queue.execute(task2, shedable); + // We have not awaited, so there has not been an opportunity to execute any tasks. + expect(queue.pendingCount()).toBe(1); - const [result1, result2] = await Promise.all([promise1, promise2]); - expect(result1).toEqual({ - status: 'complete', - result: 'test1', - }); - expect(result2).toEqual({ - status: 'complete', - result: 'test2', - }); - expect(task1).toHaveBeenCalled(); - expect(task2).toHaveBeenCalled(); -}); + const [result1, result2] = await Promise.all([promise1, promise2]); + expect(result1).toEqual({ + status: 'complete', + result: 'test1', + }); + expect(result2).toEqual({ + status: 'complete', + result: 'test2', + }); + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); + }, +); it('can shed pending shedable tasks', async () => { const queue = new AsyncTaskQueue(); @@ -112,38 +115,43 @@ it('handles mix of shedable and non-shedable tasks correctly', async () => { const task2 = jest.fn().mockResolvedValue('test2'); const task3 = jest.fn().mockResolvedValue('test3'); const task4 = jest.fn().mockResolvedValue('test4'); - + // Add tasks in order: shedable, non-shedable, shedable, non-shedable const promise1 = queue.execute(task1, true); const promise2 = queue.execute(task2, false); const promise3 = queue.execute(task3, true); const promise4 = queue.execute(task4, false); - - const [result1, result2, result3, result4] = await Promise.all([promise1, promise2, promise3, promise4]); - + + const [result1, result2, result3, result4] = await Promise.all([ + promise1, + promise2, + promise3, + promise4, + ]); + // First task should complete expect(result1).toEqual({ status: 'complete', result: 'test1', }); - + // Second task should complete (not shedable) expect(result2).toEqual({ status: 'complete', result: 'test2', }); - + // Third task should be shed expect(result3).toEqual({ status: 'shed', }); - + // Fourth task should complete expect(result4).toEqual({ status: 'complete', result: 'test4', }); - + expect(task1).toHaveBeenCalled(); expect(task2).toHaveBeenCalled(); expect(task3).not.toHaveBeenCalled(); diff --git a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts index ba4be6146d..791e47cd88 100644 --- a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts +++ b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts @@ -1,4 +1,4 @@ -import { LDLogger } from "@launchdarkly/js-sdk-common"; +import { LDLogger } from '@launchdarkly/js-sdk-common'; /** * Represents a task that has been shed from the queue. @@ -13,7 +13,7 @@ export interface ShedTask { */ export interface CompletedTask { status: 'complete'; - result: TTaskResult + result: TTaskResult; } /** @@ -21,7 +21,7 @@ export interface CompletedTask { */ export interface ErroredTask { status: 'error'; - error: Error + error: Error; } /** @@ -30,9 +30,9 @@ export interface ErroredTask { export type TaskResult = CompletedTask | ErroredTask | ShedTask; /** - * Represents a pending task. This encapsulates the async function that needs to be executed as well as a promise that represents its state. + * Represents a pending task. This encapsulates the async function that needs to be executed as well as a promise that represents its state. * The promise is not directly the promise associated with the async function, because we will not execute the async function until some point in the future, if at all. - **/ + * */ interface PendingTask { shedable: boolean; execute: () => void; @@ -40,7 +40,9 @@ interface PendingTask { promise: Promise>; } -const error = new Error('Task has already been executed or shed. This is likely an implementation error. The task will not be executed again.'); +const duplicateExecutionError = new Error( + 'Task has already been executed or shed. This is likely an implementation error. The task will not be executed again.', +); /** * Creates a pending task. @@ -48,10 +50,14 @@ const error = new Error('Task has already been executed or shed. This is likely * @param shedable Whether the task can be shed from the queue. * @returns A pending task. */ -function makePending(task: () => Promise, _logger?: LDLogger, shedable: boolean = false): PendingTask { +function makePending( + task: () => Promise, + _logger?: LDLogger, + shedable: boolean = false, +): PendingTask { let res: (value: TaskResult) => void; - const promise = new Promise>((resolve, reject) => { + const promise = new Promise>((resolve) => { res = resolve; }); @@ -60,48 +66,48 @@ function makePending(task: () => Promise, _logger?: LD execute: () => { if (executedOrShed) { // This should never happen. If it does, then it represents an implementation error in the SDK. - _logger?.error(error); + _logger?.error(duplicateExecutionError); } executedOrShed = true; task() - .then(result => res({ status: 'complete', result })) - .catch(error => res({ status: 'error', error })); + .then((result) => res({ status: 'complete', result })) + .catch((error) => res({ status: 'error', error })); }, shed: () => { if (executedOrShed) { // This should never happen. If it does, then it represents an implementation error in the SDK. - _logger?.error(error); + _logger?.error(duplicateExecutionError); } executedOrShed = true; res({ status: 'shed' }); }, promise, shedable, - } + }; } /** * An asynchronous task queue with the ability to replace pending tasks. - * + * * This is useful when you have asynchronous operations which much execute in order, and for cases where intermediate * operations can be discarded. - * + * * For instance, the SDK can only have one active context at a time, if you request identification of many contexts, - * then the ultimate state will be based on the last request. The intermediate identifies can be discarded. - * - * This class will always begin execution of the first item added to the queue, at that point the item itself is not + * then the ultimate state will be based on the last request. The intermediate identifies can be discarded. + * + * This class will always begin execution of the first item added to the queue, at that point the item itself is not * queued, but active. If another request is made while that item is still active, then it is added to the queue. * A third request would then replace the second request if the second request had not yet become active, and it was * shedable. - * + * * Once a task is active the queue will complete it. It doesn't cancel tasks that it has started, but it can shed tasks * that have not started. - * + * * TTaskResult Is the return type of the task to be executed. Tasks accept no parameters. So if you need parameters * you should use a lambda to capture them. - * + * * Exceptions from tasks are always handled and the execute method will never reject a promise. - * + * * Queue management should be done synchronously. There should not be asynchronous operations between checking the queue * and acting on the results of said check. */ @@ -109,16 +115,19 @@ export class AsyncTaskQueue { private _activeTask?: Promise>; private _queue: PendingTask[] = []; - constructor(private readonly _logger?: LDLogger) { } + constructor(private readonly _logger?: LDLogger) {} /** * Execute a task using the queue. - * + * * @param task The async function to execute. * @param shedable Whether the task can be shed from the queue. * @returns A promise that resolves to the result of the task. */ - execute(task: () => Promise, shedable: boolean = false): Promise> { + execute( + task: () => Promise, + shedable: boolean = false, + ): Promise> { const pending = makePending(task, this._logger, shedable); if (!this._activeTask) { @@ -146,7 +155,7 @@ export class AsyncTaskQueue { // There are pending tasks, so we need to execute the next one. if (this._queue.length > 0) { - let nextTask = this._queue.shift()!; + const nextTask = this._queue.shift()!; this._activeTask = nextTask.promise.finally(() => { this._activeTask = undefined; @@ -160,11 +169,11 @@ export class AsyncTaskQueue { /** * Returns the number of pending tasks in the queue. * Intended for use for testing purposes only. - * + * * @internal * @returns The number of pending tasks in the queue. */ public pendingCount(): number { return this._queue.length; } -} \ No newline at end of file +} From 6891c52752ebe1bf46e126e9a53441615474658b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:36:57 -0700 Subject: [PATCH 3/6] More tests. --- .../__tests__/async/AsyncTaskQueue.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts index 0aa2c9c499..124e8c4879 100644 --- a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts +++ b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts @@ -157,3 +157,46 @@ it('handles mix of shedable and non-shedable tasks correctly', async () => { expect(task3).not.toHaveBeenCalled(); expect(task4).toHaveBeenCalled(); }); + +it('executes tasks in order regardless of time to complete', async () => { + const queue = new AsyncTaskQueue(); + const timedPromise = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + const callOrder: string[] = []; + const task1 = jest.fn().mockImplementation(() => { + callOrder.push('task1Start'); + return timedPromise(10).then(() => { + callOrder.push('task1End'); + return 'test1'; + }); + }); + const task2 = jest.fn().mockImplementation(() => { + callOrder.push('task2Start'); + return timedPromise(5).then(() => { + callOrder.push('task2End'); + return 'test2'; + }); + }); + const task3 = jest.fn().mockImplementation(() => { + callOrder.push('task3Start'); + return timedPromise(20).then(() => { + callOrder.push('task3End'); + return 'test3'; + }); + }); + const promise1 = queue.execute(task1, false); + const promise2 = queue.execute(task2, false); + const promise3 = queue.execute(task3, false); + + await Promise.all([promise1, promise2, promise3]); + expect(callOrder).toEqual([ + 'task1Start', + 'task1End', + 'task2Start', + 'task2End', + 'task3Start', + 'task3End', + ]); +}); From fcf099069872bf60a9feb21abdef78a0ab38496e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:38:27 -0700 Subject: [PATCH 4/6] Clarify comment. --- packages/shared/sdk-client/src/async/AsyncTaskQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts index 791e47cd88..6e05d6fba4 100644 --- a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts +++ b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts @@ -168,7 +168,7 @@ export class AsyncTaskQueue { /** * Returns the number of pending tasks in the queue. - * Intended for use for testing purposes only. + * Intended for testing purposes only. * * @internal * @returns The number of pending tasks in the queue. From a05d927437cb3bb929be852321dc548b58a6995b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 1 May 2025 09:35:21 -0700 Subject: [PATCH 5/6] Use sheddable instead of shedable. --- .../__tests__/async/AsyncTaskQueue.test.ts | 22 +++++++++---------- .../sdk-client/src/async/AsyncTaskQueue.ts | 20 ++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts index 124e8c4879..107e88cc46 100644 --- a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts +++ b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts @@ -1,9 +1,9 @@ import { AsyncTaskQueue } from '../../src/async/AsyncTaskQueue'; -it.each([true, false])('executes the initial task it is given: shedable: %s', async (shedable) => { +it.each([true, false])('executes the initial task it is given: sheddable: %s', async (sheddable) => { const queue = new AsyncTaskQueue(); const task = jest.fn().mockResolvedValue('test'); - const result = await queue.execute(task, shedable); + const result = await queue.execute(task, sheddable); expect(queue.pendingCount()).toBe(0); expect(result).toEqual({ status: 'complete', @@ -13,13 +13,13 @@ it.each([true, false])('executes the initial task it is given: shedable: %s', as }); it.each([true, false])( - 'executes the next task in the queue when the previous task completes: shedable: %s', - async (shedable) => { + 'executes the next task in the queue when the previous task completes: sheddable: %s', + async (sheddable) => { const queue = new AsyncTaskQueue(); const task1 = jest.fn().mockResolvedValue('test1'); const task2 = jest.fn().mockResolvedValue('test2'); - const promise1 = queue.execute(task1, shedable); - const promise2 = queue.execute(task2, shedable); + const promise1 = queue.execute(task1, sheddable); + const promise2 = queue.execute(task2, sheddable); // We have not awaited, so there has not been an opportunity to execute any tasks. expect(queue.pendingCount()).toBe(1); @@ -37,7 +37,7 @@ it.each([true, false])( }, ); -it('can shed pending shedable tasks', async () => { +it('can shed pending sheddable tasks', async () => { const queue = new AsyncTaskQueue(); const task1 = jest.fn().mockResolvedValue('test1'); const task2 = jest.fn().mockResolvedValue('test2'); @@ -63,7 +63,7 @@ it('can shed pending shedable tasks', async () => { expect(task3).toHaveBeenCalled(); }); -it('does not shed pending non-shedable tasks', async () => { +it('does not shed pending non-sheddable tasks', async () => { const queue = new AsyncTaskQueue(); const task1 = jest.fn().mockResolvedValue('test1'); const task2 = jest.fn().mockResolvedValue('test2'); @@ -109,14 +109,14 @@ it('can handle errors from tasks', async () => { expect(task2).toHaveBeenCalled(); }); -it('handles mix of shedable and non-shedable tasks correctly', async () => { +it('handles mix of sheddable and non-sheddable tasks correctly', async () => { const queue = new AsyncTaskQueue(); const task1 = jest.fn().mockResolvedValue('test1'); const task2 = jest.fn().mockResolvedValue('test2'); const task3 = jest.fn().mockResolvedValue('test3'); const task4 = jest.fn().mockResolvedValue('test4'); - // Add tasks in order: shedable, non-shedable, shedable, non-shedable + // Add tasks in order: sheddable, non-sheddable, sheddable, non-sheddable const promise1 = queue.execute(task1, true); const promise2 = queue.execute(task2, false); const promise3 = queue.execute(task3, true); @@ -135,7 +135,7 @@ it('handles mix of shedable and non-shedable tasks correctly', async () => { result: 'test1', }); - // Second task should complete (not shedable) + // Second task should complete (not sheddable) expect(result2).toEqual({ status: 'complete', result: 'test2', diff --git a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts index 6e05d6fba4..d6fcd9777f 100644 --- a/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts +++ b/packages/shared/sdk-client/src/async/AsyncTaskQueue.ts @@ -34,7 +34,7 @@ export type TaskResult = CompletedTask | ErroredTask | * The promise is not directly the promise associated with the async function, because we will not execute the async function until some point in the future, if at all. * */ interface PendingTask { - shedable: boolean; + sheddable: boolean; execute: () => void; shed: () => void; promise: Promise>; @@ -47,13 +47,13 @@ const duplicateExecutionError = new Error( /** * Creates a pending task. * @param task The async function to execute. - * @param shedable Whether the task can be shed from the queue. + * @param sheddable Whether the task can be shed from the queue. * @returns A pending task. */ function makePending( task: () => Promise, _logger?: LDLogger, - shedable: boolean = false, + sheddable: boolean = false, ): PendingTask { let res: (value: TaskResult) => void; @@ -82,7 +82,7 @@ function makePending( res({ status: 'shed' }); }, promise, - shedable, + sheddable, }; } @@ -98,7 +98,7 @@ function makePending( * This class will always begin execution of the first item added to the queue, at that point the item itself is not * queued, but active. If another request is made while that item is still active, then it is added to the queue. * A third request would then replace the second request if the second request had not yet become active, and it was - * shedable. + * sheddable. * * Once a task is active the queue will complete it. It doesn't cancel tasks that it has started, but it can shed tasks * that have not started. @@ -121,14 +121,14 @@ export class AsyncTaskQueue { * Execute a task using the queue. * * @param task The async function to execute. - * @param shedable Whether the task can be shed from the queue. + * @param sheddable Whether the task can be shed from the queue. * @returns A promise that resolves to the result of the task. */ execute( task: () => Promise, - shedable: boolean = false, + sheddable: boolean = false, ): Promise> { - const pending = makePending(task, this._logger, shedable); + const pending = makePending(task, this._logger, sheddable); if (!this._activeTask) { this._activeTask = pending.promise.finally(() => { @@ -137,8 +137,8 @@ export class AsyncTaskQueue { }); pending.execute(); } else { - // If the last pending task is shedable, we need to shed it before adding the new task. - if (this._queue[this._queue.length - 1]?.shedable) { + // If the last pending task is sheddable, we need to shed it before adding the new task. + if (this._queue[this._queue.length - 1]?.sheddable) { this._queue.pop()?.shed(); } this._queue.push(pending); From 092530c5621c6b4bf3c01d3ca5b8fefdc9b843de Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 1 May 2025 09:40:01 -0700 Subject: [PATCH 6/6] Lint --- .../__tests__/async/AsyncTaskQueue.test.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts index 107e88cc46..029c44cd21 100644 --- a/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts +++ b/packages/shared/sdk-client/__tests__/async/AsyncTaskQueue.test.ts @@ -1,16 +1,19 @@ import { AsyncTaskQueue } from '../../src/async/AsyncTaskQueue'; -it.each([true, false])('executes the initial task it is given: sheddable: %s', async (sheddable) => { - const queue = new AsyncTaskQueue(); - const task = jest.fn().mockResolvedValue('test'); - const result = await queue.execute(task, sheddable); - expect(queue.pendingCount()).toBe(0); - expect(result).toEqual({ - status: 'complete', - result: 'test', - }); - expect(task).toHaveBeenCalled(); -}); +it.each([true, false])( + 'executes the initial task it is given: sheddable: %s', + async (sheddable) => { + const queue = new AsyncTaskQueue(); + const task = jest.fn().mockResolvedValue('test'); + const result = await queue.execute(task, sheddable); + expect(queue.pendingCount()).toBe(0); + expect(result).toEqual({ + status: 'complete', + result: 'test', + }); + expect(task).toHaveBeenCalled(); + }, +); it.each([true, false])( 'executes the next task in the queue when the previous task completes: sheddable: %s',