@powerhousedao/reactor 4.1.0-dev.56 → 4.1.0-dev.57

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 (101) hide show
  1. package/dist/bench/event-bus.bench.d.ts +2 -0
  2. package/dist/bench/event-bus.bench.d.ts.map +1 -0
  3. package/dist/bench/event-bus.bench.js +228 -0
  4. package/dist/bench/event-bus.bench.js.map +1 -0
  5. package/dist/bench/queue-only.bench.d.ts +2 -0
  6. package/dist/bench/queue-only.bench.d.ts.map +1 -0
  7. package/dist/bench/queue-only.bench.js +47 -0
  8. package/dist/bench/queue-only.bench.js.map +1 -0
  9. package/dist/bench/reactor-throughput.bench.d.ts +2 -0
  10. package/dist/bench/reactor-throughput.bench.d.ts.map +1 -0
  11. package/dist/bench/reactor-throughput.bench.js +145 -0
  12. package/dist/bench/reactor-throughput.bench.js.map +1 -0
  13. package/dist/src/client/reactor-client.d.ts +46 -7
  14. package/dist/src/client/reactor-client.d.ts.map +1 -1
  15. package/dist/src/client/reactor-client.js +13 -0
  16. package/dist/src/client/reactor-client.js.map +1 -1
  17. package/dist/src/client/types.d.ts +2 -2
  18. package/dist/src/client/types.d.ts.map +1 -1
  19. package/dist/src/client/types.js.map +1 -1
  20. package/dist/src/core/reactor.d.ts +3 -4
  21. package/dist/src/core/reactor.d.ts.map +1 -1
  22. package/dist/src/core/reactor.js +3 -8
  23. package/dist/src/core/reactor.js.map +1 -1
  24. package/dist/src/core/types.d.ts +2 -2
  25. package/dist/src/core/types.d.ts.map +1 -1
  26. package/dist/src/executor/simple-job-executor.d.ts +1 -1
  27. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  28. package/dist/src/index.js +8 -0
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/src/registry/implementation.d.ts.map +1 -1
  31. package/dist/src/registry/implementation.js +1 -0
  32. package/dist/src/registry/implementation.js.map +1 -1
  33. package/dist/src/shared/awaiter.d.ts +1 -1
  34. package/dist/src/shared/awaiter.d.ts.map +1 -1
  35. package/dist/src/shared/awaiter.js.map +1 -1
  36. package/dist/test/client/client-passthrough.test.d.ts +2 -0
  37. package/dist/test/client/client-passthrough.test.d.ts.map +1 -0
  38. package/dist/test/client/client-passthrough.test.js +199 -0
  39. package/dist/test/client/client-passthrough.test.js.map +1 -0
  40. package/dist/test/event-bus.test.d.ts +2 -0
  41. package/dist/test/event-bus.test.d.ts.map +1 -0
  42. package/dist/test/event-bus.test.js +541 -0
  43. package/dist/test/event-bus.test.js.map +1 -0
  44. package/dist/test/executor/executor-integration.test.d.ts +2 -0
  45. package/dist/test/executor/executor-integration.test.d.ts.map +1 -0
  46. package/dist/test/executor/executor-integration.test.js +287 -0
  47. package/dist/test/executor/executor-integration.test.js.map +1 -0
  48. package/dist/test/executor/job-execution-handle.test.d.ts +2 -0
  49. package/dist/test/executor/job-execution-handle.test.d.ts.map +1 -0
  50. package/dist/test/executor/job-execution-handle.test.js +272 -0
  51. package/dist/test/executor/job-execution-handle.test.js.map +1 -0
  52. package/dist/test/executor/simple-job-executor-manager.test.d.ts +2 -0
  53. package/dist/test/executor/simple-job-executor-manager.test.d.ts.map +1 -0
  54. package/dist/test/executor/simple-job-executor-manager.test.js +132 -0
  55. package/dist/test/executor/simple-job-executor-manager.test.js.map +1 -0
  56. package/dist/test/executor/simple-job-executor.test.d.ts +2 -0
  57. package/dist/test/executor/simple-job-executor.test.d.ts.map +1 -0
  58. package/dist/test/executor/simple-job-executor.test.js +139 -0
  59. package/dist/test/executor/simple-job-executor.test.js.map +1 -0
  60. package/dist/test/factories.d.ts +122 -0
  61. package/dist/test/factories.d.ts.map +1 -0
  62. package/dist/test/factories.js +319 -0
  63. package/dist/test/factories.js.map +1 -0
  64. package/dist/test/integration/document-drive-integration.test.d.ts +2 -0
  65. package/dist/test/integration/document-drive-integration.test.d.ts.map +1 -0
  66. package/dist/test/integration/document-drive-integration.test.js +1102 -0
  67. package/dist/test/integration/document-drive-integration.test.js.map +1 -0
  68. package/dist/test/integration/reactor-read.test.d.ts +2 -0
  69. package/dist/test/integration/reactor-read.test.d.ts.map +1 -0
  70. package/dist/test/integration/reactor-read.test.js +291 -0
  71. package/dist/test/integration/reactor-read.test.js.map +1 -0
  72. package/dist/test/queue/queue-integration.test.d.ts +2 -0
  73. package/dist/test/queue/queue-integration.test.d.ts.map +1 -0
  74. package/dist/test/queue/queue-integration.test.js +322 -0
  75. package/dist/test/queue/queue-integration.test.js.map +1 -0
  76. package/dist/test/queue/queue.test.d.ts +2 -0
  77. package/dist/test/queue/queue.test.d.ts.map +1 -0
  78. package/dist/test/queue/queue.test.js +770 -0
  79. package/dist/test/queue/queue.test.js.map +1 -0
  80. package/dist/test/registry/registry.test.d.ts +2 -0
  81. package/dist/test/registry/registry.test.d.ts.map +1 -0
  82. package/dist/test/registry/registry.test.js +182 -0
  83. package/dist/test/registry/registry.test.js.map +1 -0
  84. package/dist/test/shared/awaiter.test.d.ts +2 -0
  85. package/dist/test/shared/awaiter.test.d.ts.map +1 -0
  86. package/dist/test/shared/awaiter.test.js +330 -0
  87. package/dist/test/shared/awaiter.test.js.map +1 -0
  88. package/dist/test/subs/react-subscription-manager.test.d.ts +2 -0
  89. package/dist/test/subs/react-subscription-manager.test.d.ts.map +1 -0
  90. package/dist/test/subs/react-subscription-manager.test.js +693 -0
  91. package/dist/test/subs/react-subscription-manager.test.js.map +1 -0
  92. package/dist/test/utils.test.d.ts +2 -0
  93. package/dist/test/utils.test.d.ts.map +1 -0
  94. package/dist/test/utils.test.js +66 -0
  95. package/dist/test/utils.test.js.map +1 -0
  96. package/dist/tsconfig.tsbuildinfo +1 -0
  97. package/dist/vitest.config.d.ts +3 -0
  98. package/dist/vitest.config.d.ts.map +1 -0
  99. package/dist/vitest.config.js +11 -0
  100. package/dist/vitest.config.js.map +1 -0
  101. package/package.json +9 -19
@@ -0,0 +1,693 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ReactorSubscriptionManager } from "../../src/subs/react-subscription-manager.js";
3
+ import { RelationshipChangeType, } from "../../src/shared/types.js";
4
+ describe("ReactorSubscriptionManager", () => {
5
+ let manager;
6
+ let mockErrorHandler;
7
+ beforeEach(() => {
8
+ // Use a mock error handler that doesn't throw for most tests
9
+ mockErrorHandler = {
10
+ handleError: vi.fn(),
11
+ };
12
+ manager = new ReactorSubscriptionManager(mockErrorHandler);
13
+ });
14
+ describe("Subscription Methods", () => {
15
+ describe("onDocumentCreated", () => {
16
+ it("should register a subscription and return unsubscribe function", () => {
17
+ const callback = vi.fn();
18
+ const unsubscribe = manager.onDocumentCreated(callback);
19
+ expect(unsubscribe).toBeInstanceOf(Function);
20
+ // Verify subscription works
21
+ manager.notifyDocumentsCreated(["doc1"]);
22
+ expect(callback).toHaveBeenCalledTimes(1);
23
+ // Unsubscribe and verify it no longer receives notifications
24
+ unsubscribe();
25
+ manager.notifyDocumentsCreated(["doc2"]);
26
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1, not 2
27
+ });
28
+ it("should register multiple subscriptions", () => {
29
+ const callback1 = vi.fn();
30
+ const callback2 = vi.fn();
31
+ const unsub1 = manager.onDocumentCreated(callback1);
32
+ const unsub2 = manager.onDocumentCreated(callback2);
33
+ // Both should receive notifications
34
+ manager.notifyDocumentsCreated(["doc1"]);
35
+ expect(callback1).toHaveBeenCalledTimes(1);
36
+ expect(callback2).toHaveBeenCalledTimes(1);
37
+ // Unsubscribe first callback
38
+ unsub1();
39
+ manager.notifyDocumentsCreated(["doc2"]);
40
+ expect(callback1).toHaveBeenCalledTimes(1); // Still 1
41
+ expect(callback2).toHaveBeenCalledTimes(2); // Now 2
42
+ // Unsubscribe second callback
43
+ unsub2();
44
+ manager.notifyDocumentsCreated(["doc3"]);
45
+ expect(callback1).toHaveBeenCalledTimes(1); // Still 1
46
+ expect(callback2).toHaveBeenCalledTimes(2); // Still 2
47
+ });
48
+ it("should store search filters with subscription", () => {
49
+ const callback = vi.fn();
50
+ const search = { type: "Document" };
51
+ manager.onDocumentCreated(callback, search);
52
+ manager.notifyDocumentsCreated(["doc1"], new Map([["doc1", "Document"]]));
53
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
54
+ results: ["doc1"],
55
+ }));
56
+ });
57
+ });
58
+ describe("onDocumentDeleted", () => {
59
+ it("should register a subscription and return unsubscribe function", () => {
60
+ const callback = vi.fn();
61
+ const unsubscribe = manager.onDocumentDeleted(callback);
62
+ expect(unsubscribe).toBeInstanceOf(Function);
63
+ // Verify subscription works
64
+ manager.notifyDocumentsDeleted(["doc1"]);
65
+ expect(callback).toHaveBeenCalledTimes(1);
66
+ // Unsubscribe and verify it no longer receives notifications
67
+ unsubscribe();
68
+ manager.notifyDocumentsDeleted(["doc2"]);
69
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1, not 2
70
+ });
71
+ });
72
+ describe("onDocumentStateUpdated", () => {
73
+ it("should register a subscription with view filter", () => {
74
+ const callback = vi.fn();
75
+ const search = { type: "Document" };
76
+ const view = { branch: "main" };
77
+ const unsubscribe = manager.onDocumentStateUpdated(callback, search, view);
78
+ expect(unsubscribe).toBeInstanceOf(Function);
79
+ // Verify subscription works with matching filter
80
+ const doc = {
81
+ header: {
82
+ id: "doc1",
83
+ documentType: "Document",
84
+ slug: "doc-1",
85
+ },
86
+ };
87
+ manager.notifyDocumentsUpdated([doc]);
88
+ expect(callback).toHaveBeenCalledTimes(1);
89
+ // Unsubscribe and verify it no longer receives notifications
90
+ unsubscribe();
91
+ manager.notifyDocumentsUpdated([doc]);
92
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
93
+ });
94
+ });
95
+ describe("onRelationshipChanged", () => {
96
+ it("should register a subscription", () => {
97
+ const callback = vi.fn();
98
+ const unsubscribe = manager.onRelationshipChanged(callback);
99
+ expect(unsubscribe).toBeInstanceOf(Function);
100
+ // Verify subscription works
101
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
102
+ expect(callback).toHaveBeenCalledTimes(1);
103
+ // Unsubscribe and verify it no longer receives notifications
104
+ unsubscribe();
105
+ manager.notifyRelationshipChanged("parent2", "child2", RelationshipChangeType.Added);
106
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
107
+ });
108
+ });
109
+ });
110
+ describe("Notification Methods", () => {
111
+ describe("notifyDocumentsCreated", () => {
112
+ it("should notify all subscribers with created documents", () => {
113
+ const callback1 = vi.fn();
114
+ const callback2 = vi.fn();
115
+ manager.onDocumentCreated(callback1);
116
+ manager.onDocumentCreated(callback2);
117
+ const documentIds = ["doc1", "doc2"];
118
+ manager.notifyDocumentsCreated(documentIds);
119
+ expect(callback1).toHaveBeenCalledWith(expect.objectContaining({
120
+ results: documentIds,
121
+ options: { cursor: "", limit: 2 },
122
+ }));
123
+ expect(callback2).toHaveBeenCalledWith(expect.objectContaining({
124
+ results: documentIds,
125
+ options: { cursor: "", limit: 2 },
126
+ }));
127
+ });
128
+ it("should filter documents by type", () => {
129
+ const callback = vi.fn();
130
+ const search = { type: "Task" };
131
+ manager.onDocumentCreated(callback, search);
132
+ const documentIds = ["doc1", "doc2", "doc3"];
133
+ const types = new Map([
134
+ ["doc1", "Task"],
135
+ ["doc2", "Document"],
136
+ ["doc3", "Task"],
137
+ ]);
138
+ manager.notifyDocumentsCreated(documentIds, types);
139
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
140
+ results: ["doc1", "doc3"],
141
+ }));
142
+ });
143
+ it("should filter documents by parentId", () => {
144
+ const callback = vi.fn();
145
+ const search = { parentId: "parent1" };
146
+ manager.onDocumentCreated(callback, search);
147
+ const documentIds = ["doc1", "doc2", "doc3"];
148
+ const parentIds = new Map([
149
+ ["doc1", "parent1"],
150
+ ["doc2", "parent2"],
151
+ ["doc3", "parent1"],
152
+ ]);
153
+ manager.notifyDocumentsCreated(documentIds, undefined, parentIds);
154
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
155
+ results: ["doc1", "doc3"],
156
+ }));
157
+ });
158
+ it("should not notify if no documents match filter", () => {
159
+ const callback = vi.fn();
160
+ const search = { type: "NonExistent" };
161
+ manager.onDocumentCreated(callback, search);
162
+ const documentIds = ["doc1", "doc2"];
163
+ const types = new Map([
164
+ ["doc1", "Task"],
165
+ ["doc2", "Document"],
166
+ ]);
167
+ manager.notifyDocumentsCreated(documentIds, types);
168
+ expect(callback).not.toHaveBeenCalled();
169
+ });
170
+ });
171
+ describe("notifyDocumentsDeleted", () => {
172
+ it("should notify subscribers with deleted document IDs", () => {
173
+ const callback = vi.fn();
174
+ manager.onDocumentDeleted(callback);
175
+ const documentIds = ["doc1", "doc2"];
176
+ manager.notifyDocumentsDeleted(documentIds);
177
+ expect(callback).toHaveBeenCalledWith(documentIds);
178
+ });
179
+ it("should filter deleted documents by search criteria", () => {
180
+ const callback = vi.fn();
181
+ const search = { ids: ["doc1", "doc3"] };
182
+ manager.onDocumentDeleted(callback, search);
183
+ const documentIds = ["doc1", "doc2", "doc3", "doc4"];
184
+ manager.notifyDocumentsDeleted(documentIds);
185
+ expect(callback).toHaveBeenCalledWith(["doc1", "doc3"]);
186
+ });
187
+ });
188
+ describe("notifyDocumentsUpdated", () => {
189
+ it("should notify subscribers with updated documents", () => {
190
+ const callback = vi.fn();
191
+ manager.onDocumentStateUpdated(callback);
192
+ const documents = [
193
+ {
194
+ header: {
195
+ id: "doc1",
196
+ documentType: "Task",
197
+ slug: "task-1",
198
+ },
199
+ },
200
+ {
201
+ header: {
202
+ id: "doc2",
203
+ documentType: "Document",
204
+ slug: "doc-2",
205
+ },
206
+ },
207
+ ];
208
+ manager.notifyDocumentsUpdated(documents);
209
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
210
+ results: documents,
211
+ options: { cursor: "", limit: 2 },
212
+ }));
213
+ });
214
+ it("should filter updated documents by type", () => {
215
+ const callback = vi.fn();
216
+ const search = { type: "Task" };
217
+ manager.onDocumentStateUpdated(callback, search);
218
+ const documents = [
219
+ {
220
+ header: {
221
+ id: "doc1",
222
+ documentType: "Task",
223
+ slug: "task-1",
224
+ },
225
+ },
226
+ {
227
+ header: {
228
+ id: "doc2",
229
+ documentType: "Document",
230
+ slug: "doc-2",
231
+ },
232
+ },
233
+ ];
234
+ manager.notifyDocumentsUpdated(documents);
235
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
236
+ results: [documents[0]],
237
+ }));
238
+ });
239
+ it("should filter updated documents by slug", () => {
240
+ const callback = vi.fn();
241
+ const search = { slugs: ["task-1", "task-3"] };
242
+ manager.onDocumentStateUpdated(callback, search);
243
+ const documents = [
244
+ {
245
+ header: {
246
+ id: "doc1",
247
+ documentType: "Task",
248
+ slug: "task-1",
249
+ },
250
+ },
251
+ {
252
+ header: {
253
+ id: "doc2",
254
+ documentType: "Task",
255
+ slug: "task-2",
256
+ },
257
+ },
258
+ {
259
+ header: {
260
+ id: "doc3",
261
+ documentType: "Task",
262
+ slug: "task-3",
263
+ },
264
+ },
265
+ ];
266
+ manager.notifyDocumentsUpdated(documents);
267
+ const result = callback.mock.calls[0][0];
268
+ expect(result.results).toHaveLength(2);
269
+ expect(result.results[0].header.slug).toBe("task-1");
270
+ expect(result.results[1].header.slug).toBe("task-3");
271
+ });
272
+ });
273
+ describe("notifyRelationshipChanged", () => {
274
+ it("should notify subscribers about relationship changes", () => {
275
+ const callback = vi.fn();
276
+ manager.onRelationshipChanged(callback);
277
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
278
+ expect(callback).toHaveBeenCalledWith("parent1", "child1", RelationshipChangeType.Added);
279
+ });
280
+ it("should filter by parentId", () => {
281
+ const callback = vi.fn();
282
+ const search = { parentId: "parent1" };
283
+ manager.onRelationshipChanged(callback, search);
284
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
285
+ manager.notifyRelationshipChanged("parent2", "child2", RelationshipChangeType.Added);
286
+ expect(callback).toHaveBeenCalledTimes(1);
287
+ expect(callback).toHaveBeenCalledWith("parent1", "child1", RelationshipChangeType.Added);
288
+ });
289
+ it("should filter by child document type", () => {
290
+ const callback = vi.fn();
291
+ const search = { type: "Task" };
292
+ manager.onRelationshipChanged(callback, search);
293
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added, "Task");
294
+ manager.notifyRelationshipChanged("parent1", "child2", RelationshipChangeType.Added, "Document");
295
+ expect(callback).toHaveBeenCalledTimes(1);
296
+ expect(callback).toHaveBeenCalledWith("parent1", "child1", RelationshipChangeType.Added);
297
+ });
298
+ it("should filter by child IDs", () => {
299
+ const callback = vi.fn();
300
+ const search = { ids: ["child1", "child3"] };
301
+ manager.onRelationshipChanged(callback, search);
302
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
303
+ manager.notifyRelationshipChanged("parent1", "child2", RelationshipChangeType.Added);
304
+ manager.notifyRelationshipChanged("parent1", "child3", RelationshipChangeType.Removed);
305
+ expect(callback).toHaveBeenCalledTimes(2);
306
+ expect(callback).toHaveBeenNthCalledWith(1, "parent1", "child1", RelationshipChangeType.Added);
307
+ expect(callback).toHaveBeenNthCalledWith(2, "parent1", "child3", RelationshipChangeType.Removed);
308
+ });
309
+ });
310
+ });
311
+ describe("Utility Methods", () => {
312
+ describe("clearAll", () => {
313
+ it("should remove all subscriptions", () => {
314
+ const createdCallback = vi.fn();
315
+ const deletedCallback = vi.fn();
316
+ const updatedCallback = vi.fn();
317
+ const relationshipCallback = vi.fn();
318
+ manager.onDocumentCreated(createdCallback);
319
+ manager.onDocumentDeleted(deletedCallback);
320
+ manager.onDocumentStateUpdated(updatedCallback);
321
+ manager.onRelationshipChanged(relationshipCallback);
322
+ // Verify all subscriptions work
323
+ manager.notifyDocumentsCreated(["doc1"]);
324
+ manager.notifyDocumentsDeleted(["doc2"]);
325
+ const doc = {
326
+ header: { id: "doc3", documentType: "Task", slug: "task-3" },
327
+ };
328
+ manager.notifyDocumentsUpdated([doc]);
329
+ manager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
330
+ expect(createdCallback).toHaveBeenCalledTimes(1);
331
+ expect(deletedCallback).toHaveBeenCalledTimes(1);
332
+ expect(updatedCallback).toHaveBeenCalledTimes(1);
333
+ expect(relationshipCallback).toHaveBeenCalledTimes(1);
334
+ // Clear all subscriptions
335
+ manager.clearAll();
336
+ // Verify no callbacks are called after clearAll
337
+ manager.notifyDocumentsCreated(["doc4"]);
338
+ manager.notifyDocumentsDeleted(["doc5"]);
339
+ manager.notifyDocumentsUpdated([doc]);
340
+ manager.notifyRelationshipChanged("parent2", "child2", RelationshipChangeType.Removed);
341
+ // All callbacks should still have been called only once
342
+ expect(createdCallback).toHaveBeenCalledTimes(1);
343
+ expect(deletedCallback).toHaveBeenCalledTimes(1);
344
+ expect(updatedCallback).toHaveBeenCalledTimes(1);
345
+ expect(relationshipCallback).toHaveBeenCalledTimes(1);
346
+ });
347
+ it("should not notify cleared subscriptions", () => {
348
+ const callback = vi.fn();
349
+ manager.onDocumentCreated(callback);
350
+ // Verify subscription works before clear
351
+ manager.notifyDocumentsCreated(["doc1"]);
352
+ expect(callback).toHaveBeenCalledTimes(1);
353
+ // Clear and verify no more notifications
354
+ manager.clearAll();
355
+ manager.notifyDocumentsCreated(["doc2"]);
356
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
357
+ });
358
+ });
359
+ });
360
+ describe("Complex Filtering Scenarios", () => {
361
+ it("should handle multiple filters in combination", () => {
362
+ const callback = vi.fn();
363
+ const search = {
364
+ type: "Task",
365
+ parentId: "parent1",
366
+ ids: ["doc1", "doc2", "doc3"],
367
+ };
368
+ manager.onDocumentCreated(callback, search);
369
+ const documentIds = ["doc1", "doc2", "doc3", "doc4"];
370
+ const types = new Map([
371
+ ["doc1", "Task"],
372
+ ["doc2", "Document"],
373
+ ["doc3", "Task"],
374
+ ["doc4", "Task"],
375
+ ]);
376
+ const parentIds = new Map([
377
+ ["doc1", "parent1"],
378
+ ["doc2", "parent1"],
379
+ ["doc3", "parent2"],
380
+ ["doc4", "parent1"],
381
+ ]);
382
+ manager.notifyDocumentsCreated(documentIds, types, parentIds);
383
+ // Only doc1 matches all criteria: id in list, type=Task, parentId=parent1
384
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
385
+ results: ["doc1"],
386
+ }));
387
+ });
388
+ it("should handle subscriptions with no filters", () => {
389
+ const callback = vi.fn();
390
+ manager.onDocumentCreated(callback);
391
+ const documentIds = ["doc1", "doc2", "doc3"];
392
+ manager.notifyDocumentsCreated(documentIds);
393
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
394
+ results: documentIds,
395
+ }));
396
+ });
397
+ it("should handle empty document lists", () => {
398
+ const callback = vi.fn();
399
+ manager.onDocumentCreated(callback);
400
+ manager.notifyDocumentsCreated([]);
401
+ expect(callback).not.toHaveBeenCalled();
402
+ });
403
+ it("should handle null parent IDs", () => {
404
+ const callback = vi.fn();
405
+ const search = { parentId: "parent1" };
406
+ manager.onDocumentCreated(callback, search);
407
+ const documentIds = ["doc1", "doc2", "doc3"];
408
+ const parentIds = new Map([
409
+ ["doc1", "parent1"],
410
+ ["doc2", null],
411
+ ["doc3", "parent1"],
412
+ ]);
413
+ manager.notifyDocumentsCreated(documentIds, undefined, parentIds);
414
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
415
+ results: ["doc1", "doc3"],
416
+ }));
417
+ });
418
+ });
419
+ describe("Independent Subscription Management", () => {
420
+ it("should manage subscriptions independently", () => {
421
+ const createdCallback = vi.fn();
422
+ const deletedCallback = vi.fn();
423
+ const updatedCallback = vi.fn();
424
+ manager.onDocumentCreated(createdCallback);
425
+ manager.onDocumentDeleted(deletedCallback);
426
+ manager.onDocumentStateUpdated(updatedCallback);
427
+ // Notify created - should only call created callback
428
+ manager.notifyDocumentsCreated(["doc1"]);
429
+ expect(createdCallback).toHaveBeenCalledTimes(1);
430
+ expect(deletedCallback).not.toHaveBeenCalled();
431
+ expect(updatedCallback).not.toHaveBeenCalled();
432
+ // Notify deleted - should only call deleted callback
433
+ manager.notifyDocumentsDeleted(["doc2"]);
434
+ expect(createdCallback).toHaveBeenCalledTimes(1);
435
+ expect(deletedCallback).toHaveBeenCalledTimes(1);
436
+ expect(updatedCallback).not.toHaveBeenCalled();
437
+ // Notify updated - should only call updated callback
438
+ const doc = {
439
+ header: { id: "doc3", documentType: "Task", slug: "task-3" },
440
+ };
441
+ manager.notifyDocumentsUpdated([doc]);
442
+ expect(createdCallback).toHaveBeenCalledTimes(1);
443
+ expect(deletedCallback).toHaveBeenCalledTimes(1);
444
+ expect(updatedCallback).toHaveBeenCalledTimes(1);
445
+ });
446
+ it("should handle multiple unsubscribes correctly", () => {
447
+ const callback1 = vi.fn();
448
+ const callback2 = vi.fn();
449
+ const callback3 = vi.fn();
450
+ const unsub1 = manager.onDocumentCreated(callback1);
451
+ const unsub2 = manager.onDocumentCreated(callback2);
452
+ const unsub3 = manager.onDocumentCreated(callback3);
453
+ // Unsubscribe middle one
454
+ unsub2();
455
+ manager.notifyDocumentsCreated(["doc1"]);
456
+ expect(callback1).toHaveBeenCalled();
457
+ expect(callback2).not.toHaveBeenCalled();
458
+ expect(callback3).toHaveBeenCalled();
459
+ // Unsubscribe remaining
460
+ unsub1();
461
+ unsub3();
462
+ manager.notifyDocumentsCreated(["doc2"]);
463
+ expect(callback1).toHaveBeenCalledTimes(1);
464
+ expect(callback2).toHaveBeenCalledTimes(0);
465
+ expect(callback3).toHaveBeenCalledTimes(1);
466
+ });
467
+ it("should handle double unsubscribe gracefully", () => {
468
+ const callback = vi.fn();
469
+ const unsubscribe = manager.onDocumentCreated(callback);
470
+ // Verify subscription works
471
+ manager.notifyDocumentsCreated(["doc1"]);
472
+ expect(callback).toHaveBeenCalledTimes(1);
473
+ // First unsubscribe
474
+ unsubscribe();
475
+ manager.notifyDocumentsCreated(["doc2"]);
476
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
477
+ // Second unsubscribe should not cause issues
478
+ unsubscribe();
479
+ manager.notifyDocumentsCreated(["doc3"]);
480
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
481
+ });
482
+ });
483
+ describe("Error Handling and Guaranteed Delivery", () => {
484
+ let errorHandler;
485
+ let errorManager;
486
+ beforeEach(() => {
487
+ errorHandler = {
488
+ handleError: vi.fn(),
489
+ };
490
+ errorManager = new ReactorSubscriptionManager(errorHandler);
491
+ });
492
+ describe("Document Created Events", () => {
493
+ it("should catch errors and continue delivering to other subscribers", () => {
494
+ const callback1 = vi.fn();
495
+ const callback2 = vi.fn().mockImplementation(() => {
496
+ throw new Error("Callback 2 error");
497
+ });
498
+ const callback3 = vi.fn();
499
+ errorManager.onDocumentCreated(callback1);
500
+ errorManager.onDocumentCreated(callback2);
501
+ errorManager.onDocumentCreated(callback3);
502
+ errorManager.notifyDocumentsCreated(["doc1"]);
503
+ // All callbacks should be called despite callback2 throwing
504
+ expect(callback1).toHaveBeenCalledTimes(1);
505
+ expect(callback2).toHaveBeenCalledTimes(1);
506
+ expect(callback3).toHaveBeenCalledTimes(1);
507
+ // Error handler should be called for callback2
508
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(1);
509
+ expect(errorHandler.handleError).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({
510
+ eventType: "created",
511
+ subscriptionId: expect.stringContaining("created-"),
512
+ eventData: ["doc1"],
513
+ }));
514
+ });
515
+ it("should provide error context with filtered data", () => {
516
+ const callback = vi.fn().mockImplementation(() => {
517
+ throw new Error("Test error");
518
+ });
519
+ errorManager.onDocumentCreated(callback, { type: "Task" });
520
+ const documentIds = ["doc1", "doc2"];
521
+ const types = new Map([
522
+ ["doc1", "Task"],
523
+ ["doc2", "Document"],
524
+ ]);
525
+ errorManager.notifyDocumentsCreated(documentIds, types);
526
+ expect(errorHandler.handleError).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({
527
+ eventType: "created",
528
+ eventData: ["doc1"], // Only filtered doc
529
+ }));
530
+ });
531
+ });
532
+ describe("Document Deleted Events", () => {
533
+ it("should catch errors and continue delivering to other subscribers", () => {
534
+ const callback1 = vi.fn();
535
+ const callback2 = vi.fn().mockImplementation(() => {
536
+ throw new Error("Delete callback error");
537
+ });
538
+ const callback3 = vi.fn();
539
+ errorManager.onDocumentDeleted(callback1);
540
+ errorManager.onDocumentDeleted(callback2);
541
+ errorManager.onDocumentDeleted(callback3);
542
+ errorManager.notifyDocumentsDeleted(["doc1", "doc2"]);
543
+ expect(callback1).toHaveBeenCalledTimes(1);
544
+ expect(callback2).toHaveBeenCalledTimes(1);
545
+ expect(callback3).toHaveBeenCalledTimes(1);
546
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(1);
547
+ expect(errorHandler.handleError).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({
548
+ eventType: "deleted",
549
+ eventData: ["doc1", "doc2"],
550
+ }));
551
+ });
552
+ });
553
+ describe("Document Updated Events", () => {
554
+ it("should catch errors and continue delivering to other subscribers", () => {
555
+ const callback1 = vi.fn();
556
+ const callback2 = vi.fn().mockImplementation(() => {
557
+ throw new Error("Update callback error");
558
+ });
559
+ const callback3 = vi.fn();
560
+ errorManager.onDocumentStateUpdated(callback1);
561
+ errorManager.onDocumentStateUpdated(callback2);
562
+ errorManager.onDocumentStateUpdated(callback3);
563
+ const doc = {
564
+ header: {
565
+ id: "doc1",
566
+ documentType: "Task",
567
+ slug: "task-1",
568
+ },
569
+ };
570
+ errorManager.notifyDocumentsUpdated([doc]);
571
+ expect(callback1).toHaveBeenCalledTimes(1);
572
+ expect(callback2).toHaveBeenCalledTimes(1);
573
+ expect(callback3).toHaveBeenCalledTimes(1);
574
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(1);
575
+ expect(errorHandler.handleError).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({
576
+ eventType: "updated",
577
+ eventData: [doc],
578
+ }));
579
+ });
580
+ });
581
+ describe("Relationship Changed Events", () => {
582
+ it("should catch errors and continue delivering to other subscribers", () => {
583
+ const callback1 = vi.fn();
584
+ const callback2 = vi.fn().mockImplementation(() => {
585
+ throw new Error("Relationship callback error");
586
+ });
587
+ const callback3 = vi.fn();
588
+ errorManager.onRelationshipChanged(callback1);
589
+ errorManager.onRelationshipChanged(callback2);
590
+ errorManager.onRelationshipChanged(callback3);
591
+ errorManager.notifyRelationshipChanged("parent1", "child1", RelationshipChangeType.Added);
592
+ expect(callback1).toHaveBeenCalledTimes(1);
593
+ expect(callback2).toHaveBeenCalledTimes(1);
594
+ expect(callback3).toHaveBeenCalledTimes(1);
595
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(1);
596
+ expect(errorHandler.handleError).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({
597
+ eventType: "relationshipChanged",
598
+ eventData: {
599
+ parentId: "parent1",
600
+ childId: "child1",
601
+ changeType: RelationshipChangeType.Added,
602
+ },
603
+ }));
604
+ });
605
+ });
606
+ describe("Multiple Errors", () => {
607
+ it("should handle multiple errors in the same notification", () => {
608
+ const callback1 = vi.fn().mockImplementation(() => {
609
+ throw new Error("Error 1");
610
+ });
611
+ const callback2 = vi.fn();
612
+ const callback3 = vi.fn().mockImplementation(() => {
613
+ throw new Error("Error 3");
614
+ });
615
+ errorManager.onDocumentCreated(callback1);
616
+ errorManager.onDocumentCreated(callback2);
617
+ errorManager.onDocumentCreated(callback3);
618
+ errorManager.notifyDocumentsCreated(["doc1"]);
619
+ // All callbacks should be called
620
+ expect(callback1).toHaveBeenCalledTimes(1);
621
+ expect(callback2).toHaveBeenCalledTimes(1);
622
+ expect(callback3).toHaveBeenCalledTimes(1);
623
+ // Error handler should be called twice
624
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(2);
625
+ });
626
+ it("should handle errors across different event types", () => {
627
+ const createdCallback = vi.fn().mockImplementation(() => {
628
+ throw new Error("Created error");
629
+ });
630
+ const deletedCallback = vi.fn().mockImplementation(() => {
631
+ throw new Error("Deleted error");
632
+ });
633
+ errorManager.onDocumentCreated(createdCallback);
634
+ errorManager.onDocumentDeleted(deletedCallback);
635
+ errorManager.notifyDocumentsCreated(["doc1"]);
636
+ errorManager.notifyDocumentsDeleted(["doc2"]);
637
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(2);
638
+ expect(errorHandler.handleError).toHaveBeenNthCalledWith(1, expect.any(Error), expect.objectContaining({ eventType: "created" }));
639
+ expect(errorHandler.handleError).toHaveBeenNthCalledWith(2, expect.any(Error), expect.objectContaining({ eventType: "deleted" }));
640
+ });
641
+ });
642
+ describe("Default Error Handler", () => {
643
+ it("should throw enhanced errors with context", async () => {
644
+ const { DefaultSubscriptionErrorHandler } = await import("../../src/subs/default-error-handler.js");
645
+ const throwingHandler = new DefaultSubscriptionErrorHandler();
646
+ const throwingManager = new ReactorSubscriptionManager(throwingHandler);
647
+ const callback = vi.fn().mockImplementation(() => {
648
+ throw new Error("Test error");
649
+ });
650
+ throwingManager.onDocumentCreated(callback);
651
+ // The notification should throw because the default handler re-throws
652
+ expect(() => {
653
+ throwingManager.notifyDocumentsCreated(["doc1"]);
654
+ }).toThrow("Subscription error in created");
655
+ });
656
+ it("should handle non-Error objects in default handler", async () => {
657
+ const { DefaultSubscriptionErrorHandler } = await import("../../src/subs/default-error-handler.js");
658
+ const throwingHandler = new DefaultSubscriptionErrorHandler();
659
+ const throwingManager = new ReactorSubscriptionManager(throwingHandler);
660
+ const callback = vi.fn().mockImplementation(() => {
661
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
662
+ throw "string error"; // Throwing a non-Error object to test handling
663
+ });
664
+ throwingManager.onDocumentCreated(callback);
665
+ expect(() => {
666
+ throwingManager.notifyDocumentsCreated(["doc1"]);
667
+ }).toThrow("Subscription error in created");
668
+ });
669
+ });
670
+ describe("Error Handler receives correct subscription IDs", () => {
671
+ it("should pass unique subscription IDs to error handler", () => {
672
+ const callback1 = vi.fn().mockImplementation(() => {
673
+ throw new Error("Error 1");
674
+ });
675
+ const callback2 = vi.fn().mockImplementation(() => {
676
+ throw new Error("Error 2");
677
+ });
678
+ errorManager.onDocumentCreated(callback1);
679
+ errorManager.onDocumentCreated(callback2);
680
+ errorManager.notifyDocumentsCreated(["doc1"]);
681
+ expect(errorHandler.handleError).toHaveBeenCalledTimes(2);
682
+ const mockHandleError = vi.mocked(errorHandler.handleError);
683
+ const calls = mockHandleError.mock.calls;
684
+ const context1 = calls[0][1];
685
+ const context2 = calls[1][1];
686
+ expect(context1.subscriptionId).not.toBe(context2.subscriptionId);
687
+ expect(context1.subscriptionId).toContain("created-");
688
+ expect(context2.subscriptionId).toContain("created-");
689
+ });
690
+ });
691
+ });
692
+ });
693
+ //# sourceMappingURL=react-subscription-manager.test.js.map