@agentica/core 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/AgenticaOperation.d.ts +3 -4
- package/lib/context/internal/AgenticaOperationComposer.js +8 -1
- package/lib/context/internal/AgenticaOperationComposer.js.map +1 -1
- package/lib/context/internal/AgenticaOperationComposer.spec.js +39 -10
- package/lib/context/internal/AgenticaOperationComposer.spec.js.map +1 -1
- package/lib/functional/assertHttpLlmApplication.js +168 -168
- package/lib/functional/assertMcpController.d.ts +24 -0
- package/lib/functional/assertMcpController.js +1701 -0
- package/lib/functional/assertMcpController.js.map +1 -0
- package/lib/functional/validateHttpLlmApplication.js +148 -148
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +2013 -404
- package/lib/index.mjs.map +1 -1
- package/lib/orchestrate/call.js +11 -1
- package/lib/orchestrate/call.js.map +1 -1
- package/lib/orchestrate/initialize.js +60 -60
- package/lib/structures/IAgenticaController.d.ts +8 -4
- package/lib/structures/mcp/index.d.ts +0 -2
- package/lib/structures/mcp/index.js +0 -2
- package/lib/structures/mcp/index.js.map +1 -1
- package/lib/utils/AsyncQueue.d.ts +10 -0
- package/lib/utils/AsyncQueue.js +33 -9
- package/lib/utils/AsyncQueue.js.map +1 -1
- package/lib/utils/AsyncQueue.spec.d.ts +1 -0
- package/lib/utils/AsyncQueue.spec.js +280 -0
- package/lib/utils/AsyncQueue.spec.js.map +1 -0
- package/lib/utils/MPSC.spec.d.ts +1 -0
- package/lib/utils/MPSC.spec.js +222 -0
- package/lib/utils/MPSC.spec.js.map +1 -0
- package/lib/utils/StreamUtil.spec.d.ts +1 -0
- package/lib/utils/StreamUtil.spec.js +471 -0
- package/lib/utils/StreamUtil.spec.js.map +1 -0
- package/package.json +3 -3
- package/src/context/AgenticaOperation.ts +5 -6
- package/src/context/internal/AgenticaOperationComposer.spec.ts +45 -14
- package/src/context/internal/AgenticaOperationComposer.ts +10 -2
- package/src/functional/assertMcpController.ts +49 -0
- package/src/index.ts +1 -1
- package/src/orchestrate/call.ts +14 -4
- package/src/structures/IAgenticaController.ts +9 -4
- package/src/structures/mcp/index.ts +0 -2
- package/src/utils/AsyncQueue.spec.ts +355 -0
- package/src/utils/AsyncQueue.ts +36 -8
- package/src/utils/MPSC.spec.ts +276 -0
- package/src/utils/StreamUtil.spec.ts +520 -0
- package/lib/functional/assertMcpLlmApplication.d.ts +0 -18
- package/lib/functional/assertMcpLlmApplication.js +0 -74
- package/lib/functional/assertMcpLlmApplication.js.map +0 -1
- package/lib/structures/mcp/IMcpLlmApplication.d.ts +0 -9
- package/lib/structures/mcp/IMcpLlmApplication.js +0 -3
- package/lib/structures/mcp/IMcpLlmApplication.js.map +0 -1
- package/lib/structures/mcp/IMcpLlmFunction.d.ts +0 -17
- package/lib/structures/mcp/IMcpLlmFunction.js +0 -3
- package/lib/structures/mcp/IMcpLlmFunction.js.map +0 -1
- package/src/functional/assertMcpLlmApplication.ts +0 -32
- package/src/structures/mcp/IMcpLlmApplication.ts +0 -10
- package/src/structures/mcp/IMcpLlmFunction.ts +0 -19
|
@@ -21,7 +21,10 @@ export function compose<Model extends ILlmSchema.Model>(props: {
|
|
|
21
21
|
config?: IAgenticaConfig<Model> | IMicroAgenticaConfig<Model> | undefined;
|
|
22
22
|
}): AgenticaOperationCollection<Model> {
|
|
23
23
|
const unique: boolean = (props.controllers.length === 1 || (() => {
|
|
24
|
-
const names = props.controllers.map(
|
|
24
|
+
const names = props.controllers.map(
|
|
25
|
+
// eslint-disable-next-line ts/no-unsafe-return, ts/no-unsafe-call, ts/no-unsafe-member-access
|
|
26
|
+
controllers => controllers.application.functions.map((func: { name: string }) => func.name),
|
|
27
|
+
).flat();
|
|
25
28
|
return new Set(names).size === names.length;
|
|
26
29
|
})());
|
|
27
30
|
|
|
@@ -126,19 +129,24 @@ export function toClassOperations<Model extends ILlmSchema.Model>(props: {
|
|
|
126
129
|
* @internal
|
|
127
130
|
*/
|
|
128
131
|
export function toMcpOperations<Model extends ILlmSchema.Model>(props: {
|
|
129
|
-
controller: IAgenticaController.IMcp
|
|
132
|
+
controller: IAgenticaController.IMcp<Model>;
|
|
130
133
|
index: number;
|
|
131
134
|
naming: (func: string, controllerIndex: number) => string;
|
|
132
135
|
}): AgenticaOperation<Model>[] {
|
|
136
|
+
// eslint-disable-next-line ts/no-unsafe-call, ts/no-unsafe-member-access, ts/no-unsafe-return
|
|
133
137
|
return props.controller.application.functions.map(func => ({
|
|
134
138
|
protocol: "mcp",
|
|
135
139
|
controller: props.controller,
|
|
140
|
+
// eslint-disable-next-line ts/no-unsafe-assignment
|
|
136
141
|
function: func,
|
|
142
|
+
// eslint-disable-next-line ts/no-unsafe-argument, ts/no-unsafe-member-access
|
|
137
143
|
name: props.naming(func.name, props.index),
|
|
138
144
|
toJSON: () => ({
|
|
139
145
|
protocol: "mcp",
|
|
140
146
|
controller: props.controller.name,
|
|
147
|
+
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
|
|
141
148
|
function: func.name,
|
|
149
|
+
// eslint-disable-next-line ts/no-unsafe-argument, ts/no-unsafe-member-access
|
|
142
150
|
name: props.naming(func.name, props.index),
|
|
143
151
|
}),
|
|
144
152
|
}));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.d.ts";
|
|
2
|
+
import type { ILlmSchema, IMcpLlmApplication, IMcpTool } from "@samchon/openapi";
|
|
3
|
+
|
|
4
|
+
import { McpLlm } from "@samchon/openapi";
|
|
5
|
+
import typia from "typia";
|
|
6
|
+
|
|
7
|
+
import type { IAgenticaController } from "../structures/IAgenticaController";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an MCP controller with type assertion.
|
|
11
|
+
*
|
|
12
|
+
* Create an {@link IAgenticaController.IMcp} instance which represents
|
|
13
|
+
* an MCP (Model Context Protocol) controller with LLM function calling
|
|
14
|
+
* schemas and client connection.
|
|
15
|
+
*
|
|
16
|
+
* @param props Properties to create the MCP controller
|
|
17
|
+
* @param props.name Name of the MCP implementation.
|
|
18
|
+
* @param props.client Client connection to the MCP implementation.
|
|
19
|
+
* @param props.model Model schema of the LLM function calling.
|
|
20
|
+
* @param props.options Options to create the MCP controller.
|
|
21
|
+
* @returns MCP LLM application instance
|
|
22
|
+
* @author Samchon
|
|
23
|
+
*/
|
|
24
|
+
export async function assertMcpController<Model extends ILlmSchema.Model>(props: {
|
|
25
|
+
name: string;
|
|
26
|
+
client: Client;
|
|
27
|
+
model: Model;
|
|
28
|
+
options?: Partial<IMcpLlmApplication.IOptions<Model>>;
|
|
29
|
+
}): Promise<IAgenticaController.IMcp<Model>> {
|
|
30
|
+
// for peerDependencies
|
|
31
|
+
const { ListToolsResultSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
32
|
+
|
|
33
|
+
// get list of tools
|
|
34
|
+
const { tools } = await props.client.request({ method: "tools/list" }, ListToolsResultSchema);
|
|
35
|
+
|
|
36
|
+
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-call, ts/no-unsafe-member-access
|
|
37
|
+
const application: IMcpLlmApplication<Model> = McpLlm.application<Model>({
|
|
38
|
+
model: props.model,
|
|
39
|
+
tools: typia.assert<Array<IMcpTool>>(tools),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
protocol: "mcp",
|
|
44
|
+
name: props.name,
|
|
45
|
+
client: props.client,
|
|
46
|
+
// eslint-disable-next-line ts/no-unsafe-assignment
|
|
47
|
+
application,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export * from "./events/MicroAgenticaEvent";
|
|
|
23
23
|
export * as factory from "./factory";
|
|
24
24
|
|
|
25
25
|
export * from "./functional/assertHttpLlmApplication";
|
|
26
|
-
export * from "./functional/
|
|
26
|
+
export * from "./functional/assertMcpController";
|
|
27
27
|
export * from "./functional/validateHttpLlmApplication";
|
|
28
28
|
// @TODO: implement validateMcpLlmApplication
|
|
29
29
|
|
package/src/orchestrate/call.ts
CHANGED
|
@@ -72,10 +72,13 @@ export async function call<Model extends ILlmSchema.Model>(
|
|
|
72
72
|
type: "function",
|
|
73
73
|
function: {
|
|
74
74
|
name: s.name,
|
|
75
|
+
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
|
|
75
76
|
description: s.function.description,
|
|
76
77
|
parameters: (
|
|
77
78
|
"separated" in s.function
|
|
79
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
78
80
|
&& s.function.separated !== undefined
|
|
81
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
79
82
|
? (s.function.separated.llm
|
|
80
83
|
?? ({
|
|
81
84
|
type: "object",
|
|
@@ -84,6 +87,7 @@ export async function call<Model extends ILlmSchema.Model>(
|
|
|
84
87
|
additionalProperties: false,
|
|
85
88
|
$defs: {},
|
|
86
89
|
} satisfies IChatGptSchema.IParameters))
|
|
90
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
87
91
|
: s.function.parameters) as Record<string, any>,
|
|
88
92
|
},
|
|
89
93
|
}) as OpenAI.ChatCompletionTool,
|
|
@@ -368,7 +372,7 @@ async function propagateClass<Model extends ILlmSchema.Model>(props: {
|
|
|
368
372
|
|
|
369
373
|
async function propagateMcp<Model extends ILlmSchema.Model>(props: {
|
|
370
374
|
ctx: AgenticaContext<Model> | MicroAgenticaContext<Model>;
|
|
371
|
-
operation: AgenticaOperation.Mcp
|
|
375
|
+
operation: AgenticaOperation.Mcp<Model>;
|
|
372
376
|
call: AgenticaCallEvent<Model>;
|
|
373
377
|
retry: number;
|
|
374
378
|
}): Promise<AgenticaExecuteHistory<Model>> {
|
|
@@ -435,12 +439,14 @@ async function executeClassOperation<Model extends ILlmSchema.Model>(operation:
|
|
|
435
439
|
return ((execute as Record<string, unknown>)[operation.function.name] as (...args: unknown[]) => Promise<unknown>)(operationArguments);
|
|
436
440
|
}
|
|
437
441
|
|
|
438
|
-
async function executeMcpOperation(
|
|
439
|
-
operation: AgenticaOperation.Mcp
|
|
442
|
+
async function executeMcpOperation<Model extends ILlmSchema.Model>(
|
|
443
|
+
operation: AgenticaOperation.Mcp<Model>,
|
|
440
444
|
operationArguments: Record<string, unknown>,
|
|
441
445
|
): Promise<unknown> {
|
|
442
|
-
return operation.controller.
|
|
446
|
+
return operation.controller.client.callTool({
|
|
447
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
443
448
|
method: operation.function.name,
|
|
449
|
+
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
|
|
444
450
|
name: operation.function.name,
|
|
445
451
|
arguments: operationArguments,
|
|
446
452
|
}).then(v => v.content);
|
|
@@ -512,6 +518,7 @@ async function correct<Model extends ILlmSchema.Model>(
|
|
|
512
518
|
type: "function",
|
|
513
519
|
function: {
|
|
514
520
|
name: call.operation.name,
|
|
521
|
+
// eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
|
|
515
522
|
description: call.operation.function.description,
|
|
516
523
|
/**
|
|
517
524
|
* @TODO fix it
|
|
@@ -519,7 +526,9 @@ async function correct<Model extends ILlmSchema.Model>(
|
|
|
519
526
|
*/
|
|
520
527
|
parameters: (
|
|
521
528
|
"separated" in call.operation.function
|
|
529
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
522
530
|
&& call.operation.function.separated !== undefined
|
|
531
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
523
532
|
? (call.operation.function.separated?.llm
|
|
524
533
|
?? ({
|
|
525
534
|
$defs: {},
|
|
@@ -528,6 +537,7 @@ async function correct<Model extends ILlmSchema.Model>(
|
|
|
528
537
|
additionalProperties: false,
|
|
529
538
|
required: [],
|
|
530
539
|
} satisfies IChatGptSchema.IParameters))
|
|
540
|
+
// eslint-disable-next-line ts/no-unsafe-member-access
|
|
531
541
|
: call.operation.function.parameters) as unknown as Record<string, unknown>,
|
|
532
542
|
},
|
|
533
543
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.d.ts";
|
|
1
2
|
import type {
|
|
2
3
|
IHttpConnection,
|
|
3
4
|
IHttpLlmApplication,
|
|
@@ -6,10 +7,9 @@ import type {
|
|
|
6
7
|
ILlmApplication,
|
|
7
8
|
ILlmFunction,
|
|
8
9
|
ILlmSchema,
|
|
10
|
+
IMcpLlmApplication,
|
|
9
11
|
} from "@samchon/openapi";
|
|
10
12
|
|
|
11
|
-
import type { IMcpLlmApplication } from "./mcp/IMcpLlmApplication";
|
|
12
|
-
|
|
13
13
|
/**
|
|
14
14
|
* Controller of the Agentica Agent.
|
|
15
15
|
*
|
|
@@ -29,7 +29,7 @@ import type { IMcpLlmApplication } from "./mcp/IMcpLlmApplication";
|
|
|
29
29
|
export type IAgenticaController<Model extends ILlmSchema.Model> =
|
|
30
30
|
| IAgenticaController.IHttp<Model>
|
|
31
31
|
| IAgenticaController.IClass<Model>
|
|
32
|
-
| IAgenticaController.IMcp
|
|
32
|
+
| IAgenticaController.IMcp<Model>;
|
|
33
33
|
|
|
34
34
|
export namespace IAgenticaController {
|
|
35
35
|
/**
|
|
@@ -122,7 +122,12 @@ export namespace IAgenticaController {
|
|
|
122
122
|
/**
|
|
123
123
|
* MCP Server controller.
|
|
124
124
|
*/
|
|
125
|
-
export interface IMcp extends IBase<"mcp", IMcpLlmApplication
|
|
125
|
+
export interface IMcp<Model extends ILlmSchema.Model> extends IBase<"mcp", IMcpLlmApplication<Model>> {
|
|
126
|
+
/**
|
|
127
|
+
* MCP client for connection.
|
|
128
|
+
*/
|
|
129
|
+
client: Client;
|
|
130
|
+
}
|
|
126
131
|
|
|
127
132
|
interface IBase<Protocol, Application> {
|
|
128
133
|
/**
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { AsyncQueue } from "./AsyncQueue";
|
|
2
|
+
|
|
3
|
+
describe("the AsyncQueue", () => {
|
|
4
|
+
describe("basic functionality", () => {
|
|
5
|
+
it("enqueue and dequeue test", async () => {
|
|
6
|
+
const queue = new AsyncQueue<number>();
|
|
7
|
+
|
|
8
|
+
// Enqueue items
|
|
9
|
+
queue.enqueue(1);
|
|
10
|
+
queue.enqueue(2);
|
|
11
|
+
queue.enqueue(3);
|
|
12
|
+
|
|
13
|
+
// Dequeue items
|
|
14
|
+
const result1 = await queue.dequeue();
|
|
15
|
+
const result2 = await queue.dequeue();
|
|
16
|
+
const result3 = await queue.dequeue();
|
|
17
|
+
|
|
18
|
+
expect(result1.value).toBe(1);
|
|
19
|
+
expect(result1.done).toBe(false);
|
|
20
|
+
expect(result2.value).toBe(2);
|
|
21
|
+
expect(result2.done).toBe(false);
|
|
22
|
+
expect(result3.value).toBe(3);
|
|
23
|
+
expect(result3.done).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("isEmpty test", async () => {
|
|
27
|
+
const queue = new AsyncQueue<number>();
|
|
28
|
+
|
|
29
|
+
expect(queue.isEmpty()).toBe(true);
|
|
30
|
+
|
|
31
|
+
queue.enqueue(1);
|
|
32
|
+
expect(queue.isEmpty()).toBe(false);
|
|
33
|
+
|
|
34
|
+
await queue.dequeue();
|
|
35
|
+
expect(queue.isEmpty()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("isClosed test", () => {
|
|
39
|
+
const queue = new AsyncQueue<number>();
|
|
40
|
+
|
|
41
|
+
expect(queue.isClosed()).toBe(false);
|
|
42
|
+
|
|
43
|
+
queue.close();
|
|
44
|
+
expect(queue.isClosed()).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("done test", async () => {
|
|
48
|
+
const queue = new AsyncQueue<number>();
|
|
49
|
+
|
|
50
|
+
expect(queue.done()).toBe(false);
|
|
51
|
+
|
|
52
|
+
queue.enqueue(1);
|
|
53
|
+
expect(queue.done()).toBe(false);
|
|
54
|
+
|
|
55
|
+
await queue.dequeue();
|
|
56
|
+
expect(queue.done()).toBe(false);
|
|
57
|
+
|
|
58
|
+
queue.close();
|
|
59
|
+
expect(queue.done()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("close functionality", () => {
|
|
64
|
+
it("close test with empty queue", async () => {
|
|
65
|
+
const queue = new AsyncQueue<number>();
|
|
66
|
+
|
|
67
|
+
queue.close();
|
|
68
|
+
|
|
69
|
+
const result = await queue.dequeue();
|
|
70
|
+
expect(result.done).toBe(true);
|
|
71
|
+
expect(result.value).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("close test with non-empty queue", async () => {
|
|
75
|
+
const queue = new AsyncQueue<number>();
|
|
76
|
+
|
|
77
|
+
queue.enqueue(1);
|
|
78
|
+
queue.enqueue(2);
|
|
79
|
+
queue.close();
|
|
80
|
+
|
|
81
|
+
const result1 = await queue.dequeue();
|
|
82
|
+
const result2 = await queue.dequeue();
|
|
83
|
+
const result3 = await queue.dequeue();
|
|
84
|
+
|
|
85
|
+
expect(result1.value).toBe(1);
|
|
86
|
+
expect(result1.done).toBe(false);
|
|
87
|
+
expect(result2.value).toBe(2);
|
|
88
|
+
expect(result2.done).toBe(false);
|
|
89
|
+
expect(result3.done).toBe(true);
|
|
90
|
+
expect(result3.value).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("close test with waiting dequeue", async () => {
|
|
94
|
+
const queue = new AsyncQueue<number>();
|
|
95
|
+
|
|
96
|
+
// Start dequeue before enqueue
|
|
97
|
+
const dequeuePromise = queue.dequeue();
|
|
98
|
+
|
|
99
|
+
// Close the queue
|
|
100
|
+
queue.close();
|
|
101
|
+
|
|
102
|
+
const result = await dequeuePromise;
|
|
103
|
+
expect(result.done).toBe(true);
|
|
104
|
+
expect(result.value).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("waitUntilEmpty functionality", () => {
|
|
109
|
+
it("waitUntilEmpty test with empty queue", async () => {
|
|
110
|
+
const queue = new AsyncQueue<number>();
|
|
111
|
+
|
|
112
|
+
// Should resolve immediately since queue is empty
|
|
113
|
+
await queue.waitUntilEmpty();
|
|
114
|
+
|
|
115
|
+
queue.enqueue(1);
|
|
116
|
+
const result = await queue.dequeue();
|
|
117
|
+
expect(result.value).toBe(1);
|
|
118
|
+
expect(result.done).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("waitUntilEmpty test with non-empty queue", async () => {
|
|
122
|
+
const queue = new AsyncQueue<number>();
|
|
123
|
+
|
|
124
|
+
queue.enqueue(1);
|
|
125
|
+
queue.enqueue(2);
|
|
126
|
+
|
|
127
|
+
// waitUntilEmpty should not resolve since queue is not empty
|
|
128
|
+
const waitPromise = queue.waitUntilEmpty();
|
|
129
|
+
|
|
130
|
+
// Dequeue first value
|
|
131
|
+
const result1 = await queue.dequeue();
|
|
132
|
+
expect(result1.value).toBe(1);
|
|
133
|
+
|
|
134
|
+
// Dequeue second value
|
|
135
|
+
const result2 = await queue.dequeue();
|
|
136
|
+
expect(result2.value).toBe(2);
|
|
137
|
+
|
|
138
|
+
// Now queue is empty, waitUntilEmpty should resolve\
|
|
139
|
+
await waitPromise;
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("waitClosed functionality", () => {
|
|
144
|
+
it("waitClosed test with unclosed queue", async () => {
|
|
145
|
+
const queue = new AsyncQueue<number>();
|
|
146
|
+
|
|
147
|
+
// waitClosed should not resolve since queue is not closed
|
|
148
|
+
const waitPromise = queue.waitClosed();
|
|
149
|
+
|
|
150
|
+
queue.enqueue(1);
|
|
151
|
+
const result = await queue.dequeue();
|
|
152
|
+
expect(result.value).toBe(1);
|
|
153
|
+
|
|
154
|
+
// Close the queue
|
|
155
|
+
queue.close();
|
|
156
|
+
|
|
157
|
+
// Now queue is closed, waitClosed should resolve
|
|
158
|
+
await waitPromise;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("waitClosed test with already closed queue", async () => {
|
|
162
|
+
const queue = new AsyncQueue<number>();
|
|
163
|
+
|
|
164
|
+
queue.close();
|
|
165
|
+
|
|
166
|
+
// waitClosed should resolve immediately since queue is already closed
|
|
167
|
+
await queue.waitClosed();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("multiple waitClosed calls test", async () => {
|
|
171
|
+
const queue = new AsyncQueue<number>();
|
|
172
|
+
|
|
173
|
+
// Create multiple waitClosed promises
|
|
174
|
+
const waitPromises = [queue.waitClosed(), queue.waitClosed(), queue.waitClosed()];
|
|
175
|
+
|
|
176
|
+
// Close the queue
|
|
177
|
+
queue.close();
|
|
178
|
+
|
|
179
|
+
// All promises should resolve
|
|
180
|
+
await Promise.all(waitPromises);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("waitClosed test with delayed close", async () => {
|
|
184
|
+
const queue = new AsyncQueue<string>();
|
|
185
|
+
|
|
186
|
+
// Start waiting for close
|
|
187
|
+
const closePromise = queue.waitClosed();
|
|
188
|
+
|
|
189
|
+
// Close after delay
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
queue.close();
|
|
192
|
+
}, 10);
|
|
193
|
+
|
|
194
|
+
await closePromise; // Should resolve when queue is closed
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("dequeue behavior", () => {
|
|
199
|
+
it("dequeue before enqueue test", async () => {
|
|
200
|
+
const queue = new AsyncQueue<number>();
|
|
201
|
+
|
|
202
|
+
// Start dequeue before enqueue
|
|
203
|
+
const dequeuePromise = queue.dequeue();
|
|
204
|
+
|
|
205
|
+
// Enqueue after a small delay
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
queue.enqueue(42);
|
|
208
|
+
}, 10);
|
|
209
|
+
|
|
210
|
+
const result = await dequeuePromise;
|
|
211
|
+
expect(result.value).toBe(42);
|
|
212
|
+
expect(result.done).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("multiple dequeue calls test", async () => {
|
|
216
|
+
const queue = new AsyncQueue<number>();
|
|
217
|
+
|
|
218
|
+
// Start multiple dequeue calls
|
|
219
|
+
const dequeuePromises = [
|
|
220
|
+
queue.dequeue(),
|
|
221
|
+
queue.dequeue(),
|
|
222
|
+
queue.dequeue(),
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// Enqueue values
|
|
226
|
+
queue.enqueue(1);
|
|
227
|
+
queue.enqueue(2);
|
|
228
|
+
queue.enqueue(3);
|
|
229
|
+
|
|
230
|
+
const results = await Promise.all(dequeuePromises);
|
|
231
|
+
|
|
232
|
+
expect(results[0]?.value).toBe(1);
|
|
233
|
+
expect(results[0]?.done).toBe(false);
|
|
234
|
+
expect(results[1]?.value).toBe(2);
|
|
235
|
+
expect(results[1]?.done).toBe(false);
|
|
236
|
+
expect(results[2]?.value).toBe(3);
|
|
237
|
+
expect(results[2]?.done).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("dequeue after close test", async () => {
|
|
241
|
+
const queue = new AsyncQueue<number>();
|
|
242
|
+
|
|
243
|
+
queue.enqueue(1);
|
|
244
|
+
queue.close();
|
|
245
|
+
|
|
246
|
+
const result1 = await queue.dequeue();
|
|
247
|
+
expect(result1.value).toBe(1);
|
|
248
|
+
expect(result1.done).toBe(false);
|
|
249
|
+
|
|
250
|
+
const result2 = await queue.dequeue();
|
|
251
|
+
expect(result2.done).toBe(true);
|
|
252
|
+
expect(result2.value).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("duplicate dequeue test", async () => {
|
|
256
|
+
const queue = new AsyncQueue<string>();
|
|
257
|
+
|
|
258
|
+
// Start dequeue operation that will wait for an item
|
|
259
|
+
const pendingDequeue = queue.dequeue();
|
|
260
|
+
|
|
261
|
+
// Add item after a small delay
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
queue.enqueue("delayed item");
|
|
264
|
+
}, 10);
|
|
265
|
+
|
|
266
|
+
const delayedResult = await pendingDequeue;
|
|
267
|
+
expect(delayedResult.value).toBe("delayed item");
|
|
268
|
+
expect(delayedResult.done).toBe(false);
|
|
269
|
+
|
|
270
|
+
// Check for duplicate dequeue
|
|
271
|
+
const duplicatedResult = await Promise.race([
|
|
272
|
+
queue.dequeue(),
|
|
273
|
+
new Promise(resolve => setTimeout(resolve, 0, false)),
|
|
274
|
+
]) as false | IteratorResult<string, undefined>;
|
|
275
|
+
|
|
276
|
+
// If duplicatedResult is false, it means the race timed out (expected)
|
|
277
|
+
// If it's an IteratorResult, it should not have the same value
|
|
278
|
+
if (duplicatedResult !== false) {
|
|
279
|
+
expect(duplicatedResult.value).not.toBe("delayed item");
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("edge cases and error handling", () => {
|
|
285
|
+
it("enqueue after close test", async () => {
|
|
286
|
+
const queue = new AsyncQueue<number>();
|
|
287
|
+
|
|
288
|
+
queue.close();
|
|
289
|
+
queue.enqueue(1); // Should still work, but dequeue will return done: true
|
|
290
|
+
|
|
291
|
+
const result = await queue.dequeue();
|
|
292
|
+
expect(result.done).toBe(true);
|
|
293
|
+
expect(result.value).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("multiple close calls test", async () => {
|
|
297
|
+
const queue = new AsyncQueue<number>();
|
|
298
|
+
|
|
299
|
+
queue.close();
|
|
300
|
+
queue.close(); // Second close should not cause issues
|
|
301
|
+
|
|
302
|
+
const result = await queue.dequeue();
|
|
303
|
+
expect(result.done).toBe(true);
|
|
304
|
+
expect(result.value).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("waitUntilEmpty with multiple calls test", async () => {
|
|
308
|
+
const queue = new AsyncQueue<number>();
|
|
309
|
+
|
|
310
|
+
queue.enqueue(1);
|
|
311
|
+
|
|
312
|
+
// Create multiple waitUntilEmpty promises
|
|
313
|
+
const waitPromises = [queue.waitUntilEmpty(), queue.waitUntilEmpty()];
|
|
314
|
+
|
|
315
|
+
// Dequeue the value
|
|
316
|
+
await queue.dequeue();
|
|
317
|
+
|
|
318
|
+
// All promises should resolve
|
|
319
|
+
await Promise.all(waitPromises);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("concurrent enqueue and dequeue test", async () => {
|
|
323
|
+
const queue = new AsyncQueue<number>();
|
|
324
|
+
const results: number[] = [];
|
|
325
|
+
|
|
326
|
+
// Start multiple dequeue operations
|
|
327
|
+
const dequeuePromises = Array.from({ length: 5 }).fill(0).map(async () => queue.dequeue());
|
|
328
|
+
|
|
329
|
+
// Enqueue values with small delays
|
|
330
|
+
for (let i = 0; i < 5; i++) {
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
queue.enqueue(i);
|
|
333
|
+
}, i * 10);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Wait for all dequeue operations to complete
|
|
337
|
+
const dequeuedResults = await Promise.all(dequeuePromises);
|
|
338
|
+
|
|
339
|
+
// Collect values
|
|
340
|
+
dequeuedResults.forEach((result) => {
|
|
341
|
+
if (result.value !== undefined) {
|
|
342
|
+
results.push(result.value);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Check that all values were dequeued
|
|
347
|
+
expect(results.length).toBe(5);
|
|
348
|
+
expect(results).toContain(0);
|
|
349
|
+
expect(results).toContain(1);
|
|
350
|
+
expect(results).toContain(2);
|
|
351
|
+
expect(results).toContain(3);
|
|
352
|
+
expect(results).toContain(4);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
package/src/utils/AsyncQueue.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export class AsyncQueueClosedError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "AsyncQueueClosedError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export class AsyncQueue<T> {
|
|
2
9
|
private queue: T[] = [];
|
|
3
10
|
private resolvers: ((value: IteratorResult<T, undefined>) => void)[] = [];
|
|
@@ -6,6 +13,11 @@ export class AsyncQueue<T> {
|
|
|
6
13
|
private closed = false;
|
|
7
14
|
|
|
8
15
|
enqueue(item: T) {
|
|
16
|
+
if (this.closed) {
|
|
17
|
+
console.error(new AsyncQueueClosedError("Cannot enqueue item: queue is closed."));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
this.queue.push(item);
|
|
10
22
|
if (this.resolvers.length > 0) {
|
|
11
23
|
this.resolvers.shift()?.({ value: this.queue.shift()!, done: false });
|
|
@@ -13,16 +25,25 @@ export class AsyncQueue<T> {
|
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
async dequeue(): Promise<IteratorResult<T, undefined>> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (this.
|
|
21
|
-
|
|
22
|
-
this.emptyResolvers = [];
|
|
28
|
+
const item = (() => {
|
|
29
|
+
if (!this.isEmpty()) {
|
|
30
|
+
return { value: this.queue.shift()!, done: false } as const;
|
|
31
|
+
}
|
|
32
|
+
if (this.isClosed()) {
|
|
33
|
+
return { value: undefined, done: true } as const;
|
|
23
34
|
}
|
|
24
|
-
return
|
|
35
|
+
return null;
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
if (this.isEmpty() && this.emptyResolvers.length !== 0) {
|
|
39
|
+
this.emptyResolvers.forEach(resolve => resolve());
|
|
40
|
+
this.emptyResolvers = [];
|
|
25
41
|
}
|
|
42
|
+
|
|
43
|
+
if (item !== null) {
|
|
44
|
+
return item;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
return new Promise(resolve => this.resolvers.push(resolve));
|
|
27
48
|
}
|
|
28
49
|
|
|
@@ -46,6 +67,13 @@ export class AsyncQueue<T> {
|
|
|
46
67
|
this.closeResolvers.forEach(resolve => resolve());
|
|
47
68
|
}
|
|
48
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Wait until the queue is empty
|
|
72
|
+
*
|
|
73
|
+
* if the queue is closed, it will not resolve promise
|
|
74
|
+
* this function only check the queue is empty
|
|
75
|
+
* @returns A promise that resolves when the queue is empty
|
|
76
|
+
*/
|
|
49
77
|
async waitUntilEmpty() {
|
|
50
78
|
if (this.isEmpty()) {
|
|
51
79
|
return Promise.resolve();
|