jinaga-server 3.5.2 → 3.5.3

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 (57) hide show
  1. package/dist/authorization/authorization-keystore.js +2 -2
  2. package/dist/authorization/authorization-keystore.js.map +1 -1
  3. package/dist/http/csv-metadata.d.ts +24 -0
  4. package/dist/http/csv-metadata.js +3 -0
  5. package/dist/http/csv-metadata.js.map +1 -0
  6. package/dist/http/csv-validator.d.ts +23 -0
  7. package/dist/http/csv-validator.js +83 -0
  8. package/dist/http/csv-validator.js.map +1 -0
  9. package/dist/http/output-formatters.d.ts +28 -0
  10. package/dist/http/output-formatters.js +197 -0
  11. package/dist/http/output-formatters.js.map +1 -0
  12. package/dist/http/result-stream.d.ts +35 -0
  13. package/dist/http/result-stream.js +78 -0
  14. package/dist/http/result-stream.js.map +1 -0
  15. package/dist/http/router.d.ts +7 -1
  16. package/dist/http/router.js +93 -46
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/postgres/postgres-store.js +7 -5
  19. package/dist/postgres/postgres-store.js.map +1 -1
  20. package/dist/postgres/purge-sql.js +15 -2
  21. package/dist/postgres/purge-sql.js.map +1 -1
  22. package/dist/postgres/specification-result-sql.d.ts +2 -0
  23. package/dist/postgres/specification-result-sql.js +49 -5
  24. package/dist/postgres/specification-result-sql.js.map +1 -1
  25. package/dist/postgres/specification-sql.js +28 -6
  26. package/dist/postgres/specification-sql.js.map +1 -1
  27. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  28. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  29. package/integration-test/jinaga-test/Dockerfile +1 -6
  30. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  31. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  32. package/integration-test/jinaga-test/package-lock.json +329 -32
  33. package/integration-test/jinaga-test/package.json +5 -1
  34. package/integration-test/jinaga-test/read-csv.test.js +330 -0
  35. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  36. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  37. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  38. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  39. package/integration-test/jinaga-test/test-helpers.js +12 -0
  40. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  41. package/package.json +3 -2
  42. package/src/authorization/authorization-keystore.ts +4 -4
  43. package/src/http/csv-metadata.ts +34 -0
  44. package/src/http/csv-validator.ts +90 -0
  45. package/src/http/output-formatters.ts +196 -0
  46. package/src/http/result-stream.ts +69 -0
  47. package/src/http/router.ts +112 -33
  48. package/src/postgres/postgres-store.ts +13 -6
  49. package/src/postgres/purge-sql.ts +18 -2
  50. package/src/postgres/specification-result-sql.ts +70 -8
  51. package/src/postgres/specification-sql.ts +36 -8
  52. package/test/http/csv-validatorSpec.ts +205 -0
  53. package/test/http/result-streamSpec.ts +151 -0
  54. package/test/postgres/postgresReadSpec.ts +192 -14
  55. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  56. package/test/postgres/specificationConditionsSpec.ts +268 -0
  57. package/documentation/streaming-feeds-full-results-solution.md +0 -230
@@ -0,0 +1,442 @@
1
+ const request = require('supertest');
2
+ const { JinagaServer } = require('./jinaga-server');
3
+ const express = require('express');
4
+
5
+ const host = "db";
6
+ const connectionString = `postgresql://dev:devpw@${host}:5432/integrationtest`;
7
+
8
+ // Test model classes
9
+ class Root {
10
+ static Type = "ReadTest.Root";
11
+ type = Root.Type;
12
+
13
+ constructor(identifier) {
14
+ this.identifier = identifier;
15
+ }
16
+ }
17
+
18
+ class Successor {
19
+ static Type = "ReadTest.Successor";
20
+ type = Successor.Type;
21
+
22
+ constructor(identifier, predecessor) {
23
+ this.identifier = identifier;
24
+ this.pred = predecessor;
25
+ }
26
+ }
27
+
28
+ // Helper to create test app
29
+ async function createTestApp() {
30
+ const { handler, j, close } = JinagaServer.create({
31
+ pgKeystore: connectionString,
32
+ pgStore: connectionString
33
+ });
34
+
35
+ const app = express();
36
+ app.use(express.json());
37
+ app.use(express.text());
38
+ app.use(handler);
39
+
40
+ return { app, j, close };
41
+ }
42
+
43
+ // Helper to create test data
44
+ async function createTestData(j, count) {
45
+ const root = await j.fact(new Root(Math.random().toString(36).substring(2, 10)));
46
+ const successors = [];
47
+
48
+ for (let i = 0; i < count; i++) {
49
+ const successor = await j.fact(new Successor(`successor-${i}`, root));
50
+ successors.push(successor);
51
+ }
52
+
53
+ return { root, successors };
54
+ }
55
+
56
+ // Helper to parse NDJSON
57
+ function parseNDJSON(text) {
58
+ return text
59
+ .trim()
60
+ .split('\n')
61
+ .filter(line => line.trim() !== '')
62
+ .map(line => JSON.parse(line));
63
+ }
64
+
65
+ describe('Read Endpoint - Phase 1: Content Negotiation', () => {
66
+ let app;
67
+ let j;
68
+ let close;
69
+ let root;
70
+ let rootHash;
71
+ let successors;
72
+
73
+ beforeEach(async () => {
74
+ ({ app, j, close } = await createTestApp());
75
+ await j.local();
76
+
77
+ // Create test data: 1 Root and 2 Successors
78
+ ({ root, successors } = await createTestData(j, 2));
79
+ rootHash = j.hash(root);
80
+ });
81
+
82
+ afterEach(async () => {
83
+ if (close) {
84
+ await close();
85
+ }
86
+ });
87
+
88
+ describe('1.1: Default JSON Response (Backward Compatibility)', () => {
89
+ it('should return pretty-printed JSON array with default Accept header', async () => {
90
+ const specification = `let root: ReadTest.Root = #${rootHash}
91
+
92
+ (root: ReadTest.Root) {
93
+ s: ReadTest.Successor [
94
+ s->pred: ReadTest.Root = root
95
+ ]
96
+ } => {
97
+ successorId = s.identifier
98
+ successorHash = #s
99
+ }`;
100
+
101
+ const response = await request(app)
102
+ .post('/read')
103
+ .set('Content-Type', 'text/plain')
104
+ .send(specification);
105
+
106
+ if (response.status !== 200) {
107
+ expect(response.text).toBe('');
108
+ expect(response.status).toBe(200);
109
+ }
110
+ expect(response.status).toBe(200);
111
+ expect(response.type).toBe('text/plain');
112
+
113
+ // Parse the response
114
+ const results = JSON.parse(response.text);
115
+ expect(Array.isArray(results)).toBe(true);
116
+ expect(results.length).toBe(2);
117
+
118
+ // Verify projection fields
119
+ results.forEach(item => {
120
+ expect(item).toHaveProperty('successorId');
121
+ expect(item).toHaveProperty('successorHash');
122
+ });
123
+
124
+ // Check that response is pretty-printed (has whitespace)
125
+ expect(response.text).toContain('\n');
126
+ expect(response.text).toContain(' '); // 2-space indentation
127
+ });
128
+ });
129
+
130
+ describe('1.2: Compact JSON Response', () => {
131
+ it('should return compact JSON when Accept: application/json', async () => {
132
+ const specification = `let root: ReadTest.Root = #${rootHash}
133
+
134
+ (root: ReadTest.Root) {
135
+ s: ReadTest.Successor [
136
+ s->pred: ReadTest.Root = root
137
+ ]
138
+ } => {
139
+ successor = s
140
+ }`;
141
+
142
+ const response = await request(app)
143
+ .post('/read')
144
+ .set('Content-Type', 'text/plain')
145
+ .set('Accept', 'application/json')
146
+ .send(specification);
147
+
148
+ if (response.status !== 200) {
149
+ expect(response.text).toBe('');
150
+ expect(response.status).toBe(200);
151
+ }
152
+ expect(response.status).toBe(200);
153
+ expect(response.type).toBe('application/json');
154
+
155
+ // Parse the response
156
+ const results = JSON.parse(response.text);
157
+ expect(Array.isArray(results)).toBe(true);
158
+ expect(results.length).toBe(2);
159
+
160
+ // Check that response is compact (minimal whitespace)
161
+ // Compact JSON should not have formatting newlines or multiple spaces
162
+ const withoutData = response.text.replace(/"[^"]*"/g, '""'); // Remove string contents
163
+ expect(withoutData).not.toContain('\n ');
164
+ });
165
+ });
166
+
167
+ describe('1.3: Pretty JSON for Debugging', () => {
168
+ it('should return pretty JSON when Accept: text/plain', async () => {
169
+ const specification = `let root: ReadTest.Root = #${rootHash}
170
+
171
+ (root: ReadTest.Root) {
172
+ s: ReadTest.Successor [
173
+ s->pred: ReadTest.Root = root
174
+ ]
175
+ } => {
176
+ successor = s
177
+ }`;
178
+
179
+ const response = await request(app)
180
+ .post('/read')
181
+ .set('Content-Type', 'text/plain')
182
+ .set('Accept', 'text/plain')
183
+ .send(specification);
184
+
185
+ if (response.status !== 200) {
186
+ expect(response.text).toBe('');
187
+ expect(response.status).toBe(200);
188
+ }
189
+ expect(response.status).toBe(200);
190
+ expect(response.type).toBe('text/plain');
191
+
192
+ // Parse the response
193
+ const results = JSON.parse(response.text);
194
+ expect(Array.isArray(results)).toBe(true);
195
+ expect(results.length).toBe(2);
196
+
197
+ // Check 2-space indentation
198
+ const lines = response.text.split('\n');
199
+ const indentedLines = lines.filter(line => line.startsWith(' '));
200
+ expect(indentedLines.length).toBeGreaterThan(0);
201
+ });
202
+ });
203
+
204
+ describe('1.4: NDJSON Format', () => {
205
+ it('should return newline-delimited JSON when Accept: application/x-ndjson', async () => {
206
+ // Create 5 successors for this test
207
+ await j.fact(new Successor(`successor-2`, root));
208
+ await j.fact(new Successor(`successor-3`, root));
209
+ await j.fact(new Successor(`successor-4`, root));
210
+
211
+ const specification = `let root: ReadTest.Root = #${rootHash}
212
+
213
+ (root: ReadTest.Root) {
214
+ s: ReadTest.Successor [
215
+ s->pred: ReadTest.Root = root
216
+ ]
217
+ } => {
218
+ successor = s
219
+ }`;
220
+
221
+ const response = await request(app)
222
+ .post('/read')
223
+ .set('Content-Type', 'text/plain')
224
+ .set('Accept', 'application/x-ndjson')
225
+ .send(specification);
226
+
227
+ if (response.status !== 200) {
228
+ expect(response.text).toBe('');
229
+ expect(response.status).toBe(200);
230
+ }
231
+ expect(response.status).toBe(200);
232
+ expect(response.type).toBe('application/x-ndjson');
233
+
234
+ // Parse NDJSON response
235
+ const results = parseNDJSON(response.text);
236
+ expect(results.length).toBe(5);
237
+
238
+ // Each line should be valid JSON
239
+ results.forEach(item => {
240
+ expect(item).toHaveProperty('successor');
241
+ });
242
+ });
243
+ });
244
+
245
+ describe('1.5: OPTIONS Endpoint Update', () => {
246
+ it('should return supported content types in OPTIONS response', async () => {
247
+ const response = await request(app)
248
+ .options('/read');
249
+
250
+ expect(response.status).toBe(204);
251
+
252
+ const acceptPost = response.headers['accept-post'];
253
+ expect(acceptPost).toBeDefined();
254
+ expect(acceptPost).toContain('text/plain');
255
+
256
+ // After implementation, should also include:
257
+ // expect(acceptPost).toContain('application/json');
258
+ // expect(acceptPost).toContain('application/x-ndjson');
259
+ });
260
+ });
261
+
262
+ describe('1.6: Large Result Set', () => {
263
+ it('should handle 1000 results with all formats', async () => {
264
+ // Create 1000 successors
265
+ const largeRoot = await j.fact(new Root('large-test-' + Date.now()));
266
+ const largeRootHash = j.hash(largeRoot);
267
+
268
+ // Create in batches to avoid memory issues
269
+ for (let i = 0; i < 1000; i++) {
270
+ await j.fact(new Successor(`large-successor-${i}`, largeRoot));
271
+ }
272
+
273
+ const specification = `let root: ReadTest.Root = #${largeRootHash}
274
+
275
+ (root: ReadTest.Root) {
276
+ s: ReadTest.Successor [
277
+ s->pred: ReadTest.Root = root
278
+ ]
279
+ } => {
280
+ successor = s
281
+ }`;
282
+
283
+ // Test with application/json
284
+ const jsonResponse = await request(app)
285
+ .post('/read')
286
+ .set('Content-Type', 'text/plain')
287
+ .set('Accept', 'application/json')
288
+ .send(specification);
289
+
290
+ if (jsonResponse.status !== 200) {
291
+ expect(jsonResponse.text).toBe('');
292
+ expect(jsonResponse.status).toBe(200);
293
+ }
294
+ expect(jsonResponse.status).toBe(200);
295
+ const jsonResults = JSON.parse(jsonResponse.text);
296
+ expect(jsonResults.length).toBe(1000);
297
+
298
+ // Test with text/plain
299
+ const plainResponse = await request(app)
300
+ .post('/read')
301
+ .set('Content-Type', 'text/plain')
302
+ .set('Accept', 'text/plain')
303
+ .send(specification);
304
+
305
+ if (plainResponse.status !== 200) {
306
+ expect(plainResponse.text).toBe('');
307
+ expect(plainResponse.status).toBe(200);
308
+ }
309
+ expect(plainResponse.status).toBe(200);
310
+ const plainResults = JSON.parse(plainResponse.text);
311
+ expect(plainResults.length).toBe(1000);
312
+
313
+ // Test with application/x-ndjson
314
+ const ndjsonResponse = await request(app)
315
+ .post('/read')
316
+ .set('Content-Type', 'text/plain')
317
+ .set('Accept', 'application/x-ndjson')
318
+ .send(specification);
319
+
320
+ if (ndjsonResponse.status !== 200) {
321
+ expect(ndjsonResponse.text).toBe('');
322
+ expect(ndjsonResponse.status).toBe(200);
323
+ }
324
+ expect(ndjsonResponse.status).toBe(200);
325
+ const ndjsonResults = parseNDJSON(ndjsonResponse.text);
326
+ expect(ndjsonResults.length).toBe(1000);
327
+ }, 60000); // 60 second timeout for large dataset
328
+ });
329
+ });
330
+
331
+ describe('Read Endpoint - Regression Tests', () => {
332
+ let app;
333
+ let j;
334
+ let close;
335
+
336
+ // Test model for nested projections
337
+ class TestSite {
338
+ static Type = "ReadTest.Site";
339
+ type = TestSite.Type;
340
+
341
+ constructor(domain) {
342
+ this.domain = domain;
343
+ }
344
+ }
345
+
346
+ class TestPost {
347
+ static Type = "ReadTest.Post";
348
+ type = TestPost.Type;
349
+
350
+ constructor(site, createdAt) {
351
+ this.site = site;
352
+ this.createdAt = createdAt;
353
+ }
354
+ }
355
+
356
+ class TestTitle {
357
+ static Type = "ReadTest.Title";
358
+ type = TestTitle.Type;
359
+
360
+ constructor(post, value, prior) {
361
+ this.post = post;
362
+ this.value = value;
363
+ this.prior = prior;
364
+ }
365
+ }
366
+
367
+ beforeEach(async () => {
368
+ ({ app, j, close } = await createTestApp());
369
+ await j.local();
370
+ });
371
+
372
+ afterEach(async () => {
373
+ if (close) {
374
+ await close();
375
+ }
376
+ });
377
+
378
+ it('should handle nested projections without CSV validation when no Accept header is set', async () => {
379
+ // Create test data with nested structure
380
+ const site = await j.fact(new TestSite('test.com'));
381
+ const siteHash = j.hash(site);
382
+
383
+ // Create posts
384
+ const post1 = await j.fact(new TestPost(site, new Date().toISOString()));
385
+ const post2 = await j.fact(new TestPost(site, new Date().toISOString()));
386
+
387
+ // Create titles for posts (nested projections)
388
+ await j.fact(new TestTitle(post1, 'First Post Title', null));
389
+ await j.fact(new TestTitle(post2, 'Second Post Title', null));
390
+ await j.fact(new TestTitle(post2, 'Updated Second Post Title', null));
391
+
392
+ // Specification with nested projection (similar to the bug report)
393
+ const specification = `let site: ReadTest.Site = #${siteHash}
394
+
395
+ (site: ReadTest.Site) {
396
+ post: ReadTest.Post [
397
+ post->site: ReadTest.Site = site
398
+ ]
399
+ } => {
400
+ id = #post
401
+ createdAt = post.createdAt
402
+ titles = {
403
+ title: ReadTest.Title [
404
+ title->post: ReadTest.Post = post
405
+ ]
406
+ } => title.value
407
+ }`;
408
+
409
+ // Make request WITHOUT setting Accept header (this was triggering CSV validation bug)
410
+ const response = await request(app)
411
+ .post('/read')
412
+ .set('Content-Type', 'text/plain')
413
+ .send(specification);
414
+
415
+ // Verify request succeeds
416
+ expect(response.status).toBe(200);
417
+
418
+ // Verify response is JSON (text/plain is the default format, returns JSON)
419
+ expect(response.type).toBe('text/plain');
420
+
421
+ // Parse and verify the response is valid JSON
422
+ const results = JSON.parse(response.text);
423
+ expect(Array.isArray(results)).toBe(true);
424
+ expect(results.length).toBe(2);
425
+
426
+ // Verify nested projection data is included
427
+ results.forEach(post => {
428
+ expect(post).toHaveProperty('id');
429
+ expect(post).toHaveProperty('createdAt');
430
+ expect(post).toHaveProperty('titles');
431
+ expect(Array.isArray(post.titles)).toBe(true);
432
+ expect(post.titles.length).toBeGreaterThan(0);
433
+ post.titles.forEach(title => {
434
+ expect(typeof title).toBe('string');
435
+ });
436
+ });
437
+
438
+ // Verify no CSV validation error was thrown (we successfully got JSON back)
439
+ expect(response.text).not.toContain('CSV');
440
+ expect(response.text).not.toContain('error');
441
+ });
442
+ });
@@ -0,0 +1,221 @@
1
+ const request = require('supertest');
2
+ const { JinagaServer } = require('./jinaga-server');
3
+ const express = require('express');
4
+
5
+ const host = "db";
6
+ const connectionString = `postgresql://dev:devpw@${host}:5432/integrationtest`;
7
+
8
+ // Test model classes
9
+ class Root {
10
+ static Type = "PerfTest.Root";
11
+ type = Root.Type;
12
+
13
+ constructor(identifier) {
14
+ this.identifier = identifier;
15
+ }
16
+ }
17
+
18
+ class Successor {
19
+ static Type = "PerfTest.Successor";
20
+ type = Successor.Type;
21
+
22
+ constructor(identifier, predecessor) {
23
+ this.identifier = identifier;
24
+ this.pred = predecessor;
25
+ }
26
+ }
27
+
28
+ // Helper to create test app
29
+ async function createTestApp() {
30
+ const { handler, j, close } = JinagaServer.create({
31
+ pgKeystore: connectionString,
32
+ pgStore: connectionString
33
+ });
34
+
35
+ const app = express();
36
+ app.use(express.json());
37
+ app.use(express.text());
38
+ app.use(handler);
39
+
40
+ return { app, j, close };
41
+ }
42
+
43
+ // Helper to parse NDJSON
44
+ function parseNDJSON(text) {
45
+ return text
46
+ .trim()
47
+ .split('\n')
48
+ .filter(line => line.trim() !== '')
49
+ .map(line => JSON.parse(line));
50
+ }
51
+
52
+ // Helper to measure memory
53
+ function getMemoryUsage() {
54
+ return process.memoryUsage().heapUsed / 1024 / 1024; // MB
55
+ }
56
+
57
+ describe('Read Endpoint - Performance Benchmarks', () => {
58
+ let app;
59
+ let j;
60
+ let close;
61
+
62
+ beforeEach(async () => {
63
+ ({ app, j, close } = await createTestApp());
64
+ await j.local();
65
+ });
66
+
67
+ afterEach(async () => {
68
+ if (close) {
69
+ await close();
70
+ }
71
+ });
72
+
73
+ describe('Performance: 1000 results', () => {
74
+ it('should complete within reasonable time', async () => {
75
+ const root = await j.fact(new Root('perf-1k-' + Date.now()));
76
+ const rootHash = j.hash(root);
77
+
78
+ for (let i = 0; i < 1000; i++) {
79
+ await j.fact(new Successor(`perf-successor-${i}`, root));
80
+ }
81
+
82
+ const specification = `let root: PerfTest.Root = #${rootHash}
83
+
84
+ (root: PerfTest.Root) {
85
+ s: PerfTest.Successor [
86
+ s->pred: PerfTest.Root = root
87
+ ]
88
+ } => {
89
+ successor = s
90
+ }`;
91
+
92
+ const startTime = Date.now();
93
+ const response = await request(app)
94
+ .post('/read')
95
+ .set('Content-Type', 'text/plain')
96
+ .set('Accept', 'application/json')
97
+ .send(specification);
98
+
99
+ const duration = Date.now() - startTime;
100
+
101
+ if (response.status !== 200) {
102
+ expect(response.text).toBe('');
103
+ expect(response.status).toBe(200);
104
+ }
105
+ expect(response.status).toBe(200);
106
+ const results = JSON.parse(response.text);
107
+ expect(results.length).toBe(1000);
108
+
109
+ console.log(`1000 results completed in ${duration}ms`);
110
+ expect(duration).toBeLessThan(30000); // 30 seconds
111
+ }, 60000);
112
+ });
113
+
114
+ describe('Performance: NDJSON vs JSON', () => {
115
+ it('should measure time to first byte for NDJSON', async () => {
116
+ const root = await j.fact(new Root('perf-ttfb-' + Date.now()));
117
+ const rootHash = j.hash(root);
118
+
119
+ for (let i = 0; i < 100; i++) {
120
+ await j.fact(new Successor(`perf-successor-${i}`, root));
121
+ }
122
+
123
+ const specification = `let root: PerfTest.Root = #${rootHash}
124
+
125
+ (root: PerfTest.Root) {
126
+ s: PerfTest.Successor [
127
+ s->pred: PerfTest.Root = root
128
+ ]
129
+ } => {
130
+ successor = s
131
+ }`;
132
+
133
+ // Test NDJSON
134
+ const ndjsonStart = Date.now();
135
+ const ndjsonResponse = await request(app)
136
+ .post('/read')
137
+ .set('Content-Type', 'text/plain')
138
+ .set('Accept', 'application/x-ndjson')
139
+ .send(specification);
140
+ const ndjsonDuration = Date.now() - ndjsonStart;
141
+
142
+ if (ndjsonResponse.status !== 200) {
143
+ expect(ndjsonResponse.text).toBe('');
144
+ expect(ndjsonResponse.status).toBe(200);
145
+ }
146
+ expect(ndjsonResponse.status).toBe(200);
147
+ const ndjsonResults = parseNDJSON(ndjsonResponse.text);
148
+ expect(ndjsonResults.length).toBe(100);
149
+
150
+ // Test JSON
151
+ const jsonStart = Date.now();
152
+ const jsonResponse = await request(app)
153
+ .post('/read')
154
+ .set('Content-Type', 'text/plain')
155
+ .set('Accept', 'application/json')
156
+ .send(specification);
157
+ const jsonDuration = Date.now() - jsonStart;
158
+
159
+ if (jsonResponse.status !== 200) {
160
+ expect(jsonResponse.text).toBe('');
161
+ expect(jsonResponse.status).toBe(200);
162
+ }
163
+ expect(jsonResponse.status).toBe(200);
164
+ const jsonResults = JSON.parse(jsonResponse.text);
165
+ expect(jsonResults.length).toBe(100);
166
+
167
+ console.log(`NDJSON: ${ndjsonDuration}ms, JSON: ${jsonDuration}ms`);
168
+ }, 60000);
169
+ });
170
+
171
+ describe('Concurrent streams', () => {
172
+ it('should handle multiple concurrent requests', async () => {
173
+ const root = await j.fact(new Root('perf-concurrent-' + Date.now()));
174
+ const rootHash = j.hash(root);
175
+
176
+ for (let i = 0; i < 100; i++) {
177
+ await j.fact(new Successor(`perf-successor-${i}`, root));
178
+ }
179
+
180
+ const specification = `let root: PerfTest.Root = #${rootHash}
181
+
182
+ (root: PerfTest.Root) {
183
+ s: PerfTest.Successor [
184
+ s->pred: PerfTest.Root = root
185
+ ]
186
+ } => {
187
+ successor = s
188
+ }`;
189
+
190
+ // Launch 5 concurrent requests
191
+ const promises = [];
192
+ for (let i = 0; i < 5; i++) {
193
+ promises.push(
194
+ request(app)
195
+ .post('/read')
196
+ .set('Content-Type', 'text/plain')
197
+ .set('Accept', 'application/x-ndjson')
198
+ .send(specification)
199
+ );
200
+ }
201
+
202
+ const startTime = Date.now();
203
+ const responses = await Promise.all(promises);
204
+ const duration = Date.now() - startTime;
205
+
206
+ // All should succeed
207
+ responses.forEach(response => {
208
+ if (response.status !== 200) {
209
+ expect(response.text).toBe('');
210
+ expect(response.status).toBe(200);
211
+ }
212
+ expect(response.status).toBe(200);
213
+ const results = parseNDJSON(response.text);
214
+ expect(results.length).toBe(100);
215
+ });
216
+
217
+ console.log(`5 concurrent requests completed in ${duration}ms`);
218
+ expect(duration).toBeLessThan(30000); // 30 seconds
219
+ }, 60000);
220
+ });
221
+ });