@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.
Files changed (59) hide show
  1. package/lib/context/AgenticaOperation.d.ts +3 -4
  2. package/lib/context/internal/AgenticaOperationComposer.js +8 -1
  3. package/lib/context/internal/AgenticaOperationComposer.js.map +1 -1
  4. package/lib/context/internal/AgenticaOperationComposer.spec.js +39 -10
  5. package/lib/context/internal/AgenticaOperationComposer.spec.js.map +1 -1
  6. package/lib/functional/assertHttpLlmApplication.js +168 -168
  7. package/lib/functional/assertMcpController.d.ts +24 -0
  8. package/lib/functional/assertMcpController.js +1701 -0
  9. package/lib/functional/assertMcpController.js.map +1 -0
  10. package/lib/functional/validateHttpLlmApplication.js +148 -148
  11. package/lib/index.d.ts +1 -1
  12. package/lib/index.js +1 -1
  13. package/lib/index.js.map +1 -1
  14. package/lib/index.mjs +2013 -404
  15. package/lib/index.mjs.map +1 -1
  16. package/lib/orchestrate/call.js +11 -1
  17. package/lib/orchestrate/call.js.map +1 -1
  18. package/lib/orchestrate/initialize.js +60 -60
  19. package/lib/structures/IAgenticaController.d.ts +8 -4
  20. package/lib/structures/mcp/index.d.ts +0 -2
  21. package/lib/structures/mcp/index.js +0 -2
  22. package/lib/structures/mcp/index.js.map +1 -1
  23. package/lib/utils/AsyncQueue.d.ts +10 -0
  24. package/lib/utils/AsyncQueue.js +33 -9
  25. package/lib/utils/AsyncQueue.js.map +1 -1
  26. package/lib/utils/AsyncQueue.spec.d.ts +1 -0
  27. package/lib/utils/AsyncQueue.spec.js +280 -0
  28. package/lib/utils/AsyncQueue.spec.js.map +1 -0
  29. package/lib/utils/MPSC.spec.d.ts +1 -0
  30. package/lib/utils/MPSC.spec.js +222 -0
  31. package/lib/utils/MPSC.spec.js.map +1 -0
  32. package/lib/utils/StreamUtil.spec.d.ts +1 -0
  33. package/lib/utils/StreamUtil.spec.js +471 -0
  34. package/lib/utils/StreamUtil.spec.js.map +1 -0
  35. package/package.json +3 -3
  36. package/src/context/AgenticaOperation.ts +5 -6
  37. package/src/context/internal/AgenticaOperationComposer.spec.ts +45 -14
  38. package/src/context/internal/AgenticaOperationComposer.ts +10 -2
  39. package/src/functional/assertMcpController.ts +49 -0
  40. package/src/index.ts +1 -1
  41. package/src/orchestrate/call.ts +14 -4
  42. package/src/structures/IAgenticaController.ts +9 -4
  43. package/src/structures/mcp/index.ts +0 -2
  44. package/src/utils/AsyncQueue.spec.ts +355 -0
  45. package/src/utils/AsyncQueue.ts +36 -8
  46. package/src/utils/MPSC.spec.ts +276 -0
  47. package/src/utils/StreamUtil.spec.ts +520 -0
  48. package/lib/functional/assertMcpLlmApplication.d.ts +0 -18
  49. package/lib/functional/assertMcpLlmApplication.js +0 -74
  50. package/lib/functional/assertMcpLlmApplication.js.map +0 -1
  51. package/lib/structures/mcp/IMcpLlmApplication.d.ts +0 -9
  52. package/lib/structures/mcp/IMcpLlmApplication.js +0 -3
  53. package/lib/structures/mcp/IMcpLlmApplication.js.map +0 -1
  54. package/lib/structures/mcp/IMcpLlmFunction.d.ts +0 -17
  55. package/lib/structures/mcp/IMcpLlmFunction.js +0 -3
  56. package/lib/structures/mcp/IMcpLlmFunction.js.map +0 -1
  57. package/src/functional/assertMcpLlmApplication.ts +0 -32
  58. package/src/structures/mcp/IMcpLlmApplication.ts +0 -10
  59. 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(controllers => controllers.application.functions.map(func => func.name)).flat();
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/assertMcpLlmApplication";
26
+ export * from "./functional/assertMcpController";
27
27
  export * from "./functional/validateHttpLlmApplication";
28
28
  // @TODO: implement validateMcpLlmApplication
29
29
 
@@ -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.application.client.callTool({
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
  /**
@@ -1,3 +1 @@
1
- export * from "./IMcpLlmApplication";
2
- export * from "./IMcpLlmFunction";
3
1
  export * from "./IMcpLlmTransportProps";
@@ -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
+ });
@@ -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
- if (this.queue.length > 0) {
17
- return { value: this.queue.shift()!, done: false };
18
- }
19
- if (this.closed) {
20
- if (this.emptyResolvers.length > 0) {
21
- this.emptyResolvers.forEach(resolve => resolve());
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 { value: undefined, done: true };
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();