@unrdf/kgc-runtime 26.4.2

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 (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. package/vitest.config.mjs +18 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tests for Bulkhead Isolation Pattern
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { BulkheadManager, BulkheadCoordinator } from '../src/bulkhead-manager.mjs';
7
+
8
+ describe('Bulkhead Isolation Pattern', () => {
9
+ describe('BulkheadManager', () => {
10
+ let bulkhead;
11
+
12
+ beforeEach(() => {
13
+ bulkhead = new BulkheadManager({
14
+ name: 'test-bulkhead',
15
+ maxConcurrent: 2,
16
+ maxQueueSize: 3,
17
+ timeout: 1000,
18
+ });
19
+ });
20
+
21
+ it('should execute function immediately when under capacity', async () => {
22
+ const result = await bulkhead.execute(async () => {
23
+ return 'success';
24
+ });
25
+
26
+ expect(result).toBe('success');
27
+ expect(bulkhead.getStats().completed).toBe(1);
28
+ });
29
+
30
+ it('should queue tasks when at capacity', async () => {
31
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
32
+
33
+ // Start 2 concurrent tasks (at capacity)
34
+ const task1 = bulkhead.execute(async () => {
35
+ await delay(50);
36
+ return 'task1';
37
+ });
38
+
39
+ const task2 = bulkhead.execute(async () => {
40
+ await delay(50);
41
+ return 'task2';
42
+ });
43
+
44
+ // Third task should be queued
45
+ const task3 = bulkhead.execute(async () => 'task3');
46
+
47
+ expect(bulkhead.getStats().active).toBe(2);
48
+
49
+ const results = await Promise.all([task1, task2, task3]);
50
+
51
+ expect(results).toEqual(['task1', 'task2', 'task3']);
52
+ expect(bulkhead.getStats().completed).toBe(3);
53
+ });
54
+
55
+ it('should reject when queue is full', async () => {
56
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
57
+
58
+ // Fill capacity (2)
59
+ bulkhead.execute(async () => await delay(100));
60
+ bulkhead.execute(async () => await delay(100));
61
+
62
+ // Fill queue (3)
63
+ bulkhead.execute(async () => 'q1');
64
+ bulkhead.execute(async () => 'q2');
65
+ bulkhead.execute(async () => 'q3');
66
+
67
+ // This should be rejected
68
+ await expect(
69
+ bulkhead.execute(async () => 'overflow')
70
+ ).rejects.toThrow('queue full');
71
+
72
+ expect(bulkhead.getStats().rejected).toBe(1);
73
+ });
74
+
75
+ it('should respect priority in queue', async () => {
76
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
77
+ const results = [];
78
+
79
+ // Fill capacity
80
+ const blocker1 = bulkhead.execute(async () => await delay(50));
81
+ const blocker2 = bulkhead.execute(async () => await delay(50));
82
+
83
+ // Queue with different priorities
84
+ const low = bulkhead.execute(async () => {
85
+ results.push('low');
86
+ return 'low';
87
+ }, { priority: 1 });
88
+
89
+ const high = bulkhead.execute(async () => {
90
+ results.push('high');
91
+ return 'high';
92
+ }, { priority: 10 });
93
+
94
+ await Promise.all([blocker1, blocker2, low, high]);
95
+
96
+ // High priority should execute first
97
+ expect(results[0]).toBe('high');
98
+ expect(results[1]).toBe('low');
99
+ });
100
+
101
+ it('should timeout long-running tasks', async () => {
102
+ const bulkheadWithTimeout = new BulkheadManager({
103
+ name: 'timeout-test',
104
+ maxConcurrent: 1,
105
+ maxQueueSize: 0,
106
+ timeout: 50,
107
+ });
108
+
109
+ await expect(
110
+ bulkheadWithTimeout.execute(async () => {
111
+ await new Promise(resolve => setTimeout(resolve, 200));
112
+ return 'never';
113
+ })
114
+ ).rejects.toThrow('timed out');
115
+
116
+ expect(bulkheadWithTimeout.getStats().timedOut).toBe(1);
117
+ });
118
+
119
+ it('should drain all active and queued tasks', async () => {
120
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
121
+
122
+ bulkhead.execute(async () => await delay(50));
123
+ bulkhead.execute(async () => await delay(50));
124
+
125
+ const drainPromise = bulkhead.drain();
126
+
127
+ expect(bulkhead.getStats().active).toBeGreaterThan(0);
128
+
129
+ await drainPromise;
130
+
131
+ expect(bulkhead.getStats().active).toBe(0);
132
+ expect(bulkhead.getStats().queued).toBe(0);
133
+ });
134
+
135
+ it('should clear queue and reject pending tasks', async () => {
136
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
137
+
138
+ // Fill capacity
139
+ bulkhead.execute(async () => await delay(100));
140
+ bulkhead.execute(async () => await delay(100));
141
+
142
+ // Add to queue
143
+ const queued = bulkhead.execute(async () => 'queued');
144
+
145
+ const cleared = bulkhead.clearQueue();
146
+
147
+ expect(cleared).toBe(1);
148
+ await expect(queued).rejects.toThrow('queue cleared');
149
+ });
150
+
151
+ it('should generate receipt for operations', async () => {
152
+ await bulkhead.execute(async () => 'test');
153
+
154
+ const receipt = await bulkhead.generateReceipt(
155
+ 'test-operation',
156
+ { input: 'data' },
157
+ { output: 'result' }
158
+ );
159
+
160
+ expect(receipt.operation).toContain('bulkhead:test-bulkhead');
161
+ expect(receipt.outputs.stats).toBeDefined();
162
+ });
163
+ });
164
+
165
+ describe('BulkheadCoordinator', () => {
166
+ let coordinator;
167
+
168
+ beforeEach(() => {
169
+ coordinator = new BulkheadCoordinator();
170
+ });
171
+
172
+ it('should register multiple bulkheads', () => {
173
+ coordinator.register({
174
+ name: 'db',
175
+ maxConcurrent: 10,
176
+ maxQueueSize: 100,
177
+ });
178
+
179
+ coordinator.register({
180
+ name: 'api',
181
+ maxConcurrent: 20,
182
+ maxQueueSize: 50,
183
+ });
184
+
185
+ const stats = coordinator.getAllStats();
186
+
187
+ expect(stats.db).toBeDefined();
188
+ expect(stats.api).toBeDefined();
189
+ expect(stats.db.maxConcurrent).toBe(10);
190
+ expect(stats.api.maxConcurrent).toBe(20);
191
+ });
192
+
193
+ it('should execute on named bulkhead', async () => {
194
+ coordinator.register({
195
+ name: 'test',
196
+ maxConcurrent: 1,
197
+ maxQueueSize: 0,
198
+ });
199
+
200
+ const result = await coordinator.execute('test', async () => 'success');
201
+
202
+ expect(result).toBe('success');
203
+ });
204
+
205
+ it('should throw when bulkhead not found', async () => {
206
+ await expect(
207
+ coordinator.execute('nonexistent', async () => 'never')
208
+ ).rejects.toThrow('not found');
209
+ });
210
+
211
+ it('should drain all bulkheads', async () => {
212
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
213
+
214
+ coordinator.register({
215
+ name: 'b1',
216
+ maxConcurrent: 1,
217
+ maxQueueSize: 0,
218
+ });
219
+
220
+ coordinator.register({
221
+ name: 'b2',
222
+ maxConcurrent: 1,
223
+ maxQueueSize: 0,
224
+ });
225
+
226
+ coordinator.execute('b1', async () => await delay(50));
227
+ coordinator.execute('b2', async () => await delay(50));
228
+
229
+ await coordinator.drainAll();
230
+
231
+ const stats = coordinator.getAllStats();
232
+ expect(stats.b1.active).toBe(0);
233
+ expect(stats.b2.active).toBe(0);
234
+ });
235
+ });
236
+ });