jinaga-server 3.5.1 → 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 (64) 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 +276 -70
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/http/stream.d.ts +3 -0
  19. package/dist/http/stream.js +79 -5
  20. package/dist/http/stream.js.map +1 -1
  21. package/dist/postgres/postgres-store.js +7 -5
  22. package/dist/postgres/postgres-store.js.map +1 -1
  23. package/dist/postgres/purge-sql.js +15 -2
  24. package/dist/postgres/purge-sql.js.map +1 -1
  25. package/dist/postgres/specification-result-sql.d.ts +2 -0
  26. package/dist/postgres/specification-result-sql.js +49 -5
  27. package/dist/postgres/specification-result-sql.js.map +1 -1
  28. package/dist/postgres/specification-sql.js +28 -6
  29. package/dist/postgres/specification-sql.js.map +1 -1
  30. package/docs/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
  31. package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
  32. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  33. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  34. package/integration-test/jinaga-test/Dockerfile +1 -6
  35. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  36. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  37. package/integration-test/jinaga-test/package-lock.json +329 -32
  38. package/integration-test/jinaga-test/package.json +5 -1
  39. package/integration-test/jinaga-test/read-csv.test.js +330 -0
  40. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  41. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  42. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  43. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  44. package/integration-test/jinaga-test/test-helpers.js +12 -0
  45. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  46. package/package.json +3 -2
  47. package/src/authorization/authorization-keystore.ts +4 -4
  48. package/src/http/csv-metadata.ts +34 -0
  49. package/src/http/csv-validator.ts +90 -0
  50. package/src/http/output-formatters.ts +196 -0
  51. package/src/http/result-stream.ts +69 -0
  52. package/src/http/router.ts +344 -62
  53. package/src/http/stream.ts +97 -5
  54. package/src/postgres/postgres-store.ts +13 -6
  55. package/src/postgres/purge-sql.ts +18 -2
  56. package/src/postgres/specification-result-sql.ts +70 -8
  57. package/src/postgres/specification-sql.ts +36 -8
  58. package/test/http/csv-validatorSpec.ts +205 -0
  59. package/test/http/result-streamSpec.ts +151 -0
  60. package/test/postgres/postgresReadSpec.ts +192 -14
  61. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  62. package/test/postgres/specificationConditionsSpec.ts +268 -0
  63. package/test-concurrent-streams.js +219 -0
  64. package/documentation/streaming-feeds-full-results-solution.md +0 -230
@@ -12,58 +12,125 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.HttpRouter = void 0;
13
13
  const express_1 = require("express");
14
14
  const jinaga_1 = require("jinaga");
15
+ const csv_validator_1 = require("./csv-validator");
15
16
  const line_reader_1 = require("./line-reader");
17
+ const output_formatters_1 = require("./output-formatters");
18
+ const result_stream_1 = require("./result-stream");
16
19
  const stream_1 = require("./stream");
17
20
  function getOrStream(getMethod, streamMethod) {
18
21
  return (req, res, next) => {
22
+ const connectionId = Math.random().toString(36).substring(2, 10);
19
23
  const user = req.user;
20
24
  const accept = req.headers["accept"];
25
+ console.log(`[HttpConnection:${connectionId}] New request - User: ${(user === null || user === void 0 ? void 0 : user.id) || 'anonymous'}, Accept: ${accept}, URL: ${req.url}`);
21
26
  if (accept && accept.indexOf("application/x-jinaga-feed-stream") >= 0) {
27
+ console.log(`[HttpConnection:${connectionId}] STREAMING request detected`);
22
28
  streamMethod(user, req.params, req.query)
23
29
  .then(response => {
24
30
  if (!response) {
31
+ console.log(`[HttpConnection:${connectionId}] Stream method returned null - sending 404`);
25
32
  res.sendStatus(404);
26
33
  next();
27
34
  }
28
35
  else {
36
+ console.log(`[HttpConnection:${connectionId}] Setting up streaming response`);
29
37
  res.type("application/x-jinaga-feed-stream");
30
38
  res.set("Connection", "keep-alive");
31
39
  res.set("Cache-Control", "no-cache");
32
40
  res.set("Access-Control-Allow-Origin", "*");
33
41
  res.flushHeaders();
42
+ console.log(`[HttpConnection:${connectionId}] Headers flushed - setting up event handlers`);
43
+ let clientDisconnected = false;
44
+ let timeoutTriggered = false;
45
+ let streamClosed = false;
46
+ // Client disconnect handler
34
47
  req.on("close", () => {
35
- response.close();
48
+ console.log(`[HttpConnection:${connectionId}] CLIENT DISCONNECTED`);
49
+ clientDisconnected = true;
50
+ if (!streamClosed) {
51
+ console.log(`[HttpConnection:${connectionId}] Closing stream due to client disconnect`);
52
+ response.close();
53
+ streamClosed = true;
54
+ }
36
55
  });
56
+ // Timeout handler
37
57
  const timeout = setTimeout(() => {
38
- response.close();
58
+ console.log(`[HttpConnection:${connectionId}] TIMEOUT TRIGGERED (5 minutes)`);
59
+ timeoutTriggered = true;
60
+ if (!streamClosed) {
61
+ console.log(`[HttpConnection:${connectionId}] Closing stream due to timeout`);
62
+ response.close();
63
+ streamClosed = true;
64
+ }
39
65
  }, 5 * 60 * 1000);
66
+ let messageCount = 0;
40
67
  response
41
68
  .next(data => {
42
- res.write(JSON.stringify(data) + "\n\n");
69
+ messageCount++;
70
+ const writeStart = Date.now();
71
+ if (clientDisconnected) {
72
+ console.warn(`[HttpConnection:${connectionId}] Attempted to write to disconnected client - Message #${messageCount}`);
73
+ return;
74
+ }
75
+ try {
76
+ const jsonData = JSON.stringify(data) + "\n\n";
77
+ res.write(jsonData);
78
+ const writeDuration = Date.now() - writeStart;
79
+ console.log(`[HttpConnection:${connectionId}] Message #${messageCount} sent - Size: ${jsonData.length} bytes, Duration: ${writeDuration}ms`);
80
+ if (writeDuration > 50) {
81
+ console.warn(`[HttpConnection:${connectionId}] SLOW write operation - Duration: ${writeDuration}ms`);
82
+ }
83
+ }
84
+ catch (error) {
85
+ console.error(`[HttpConnection:${connectionId}] ERROR writing message #${messageCount}: ${error}`);
86
+ }
43
87
  })
44
88
  .done(() => {
45
89
  var _a;
90
+ console.log(`[HttpConnection:${connectionId}] STREAM DONE - Messages sent: ${messageCount}, Client disconnected: ${clientDisconnected}, Timeout triggered: ${timeoutTriggered}`);
46
91
  clearTimeout(timeout);
47
- (_a = res.socket) === null || _a === void 0 ? void 0 : _a.end();
92
+ streamClosed = true;
93
+ try {
94
+ if (!clientDisconnected) {
95
+ console.log(`[HttpConnection:${connectionId}] Ending socket connection`);
96
+ (_a = res.socket) === null || _a === void 0 ? void 0 : _a.end();
97
+ }
98
+ else {
99
+ console.log(`[HttpConnection:${connectionId}] Client already disconnected - skipping socket end`);
100
+ }
101
+ }
102
+ catch (error) {
103
+ console.error(`[HttpConnection:${connectionId}] ERROR ending socket: ${error}`);
104
+ }
48
105
  });
106
+ console.log(`[HttpConnection:${connectionId}] Stream handlers configured`);
49
107
  }
50
108
  })
51
- .catch(error => handleError(error, req, res, next));
109
+ .catch(error => {
110
+ console.error(`[HttpConnection:${connectionId}] ERROR in stream method: ${error}`);
111
+ handleError(error, req, res, next);
112
+ });
52
113
  }
53
114
  else {
115
+ console.log(`[HttpConnection:${connectionId}] Regular GET request`);
54
116
  getMethod(user, req.params, req.query)
55
117
  .then(response => {
56
118
  if (!response) {
119
+ console.log(`[HttpConnection:${connectionId}] Get method returned null - sending 404`);
57
120
  res.sendStatus(404);
58
121
  next();
59
122
  }
60
123
  else {
124
+ console.log(`[HttpConnection:${connectionId}] Sending JSON response`);
61
125
  res.type("json");
62
126
  res.send(JSON.stringify(response));
63
127
  next();
64
128
  }
65
129
  })
66
- .catch(error => handleError(error, req, res, next));
130
+ .catch(error => {
131
+ console.error(`[HttpConnection:${connectionId}] ERROR in get method: ${error}`);
132
+ handleError(error, req, res, next);
133
+ });
67
134
  }
68
135
  };
69
136
  }
@@ -105,25 +172,6 @@ function post(parse, method, output) {
105
172
  .catch(error => handleError(error, req, res, next));
106
173
  };
107
174
  }
108
- function postString(method) {
109
- return (req, res, next) => {
110
- const user = req.user;
111
- const input = parseString(req.body);
112
- if (!input || typeof (input) !== 'string') {
113
- res.type("text");
114
- res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
115
- }
116
- else {
117
- method(user, input)
118
- .then(response => {
119
- res.type("text");
120
- res.send(JSON.stringify(response, null, 2));
121
- next();
122
- })
123
- .catch(error => handleError(error, req, res, next));
124
- }
125
- };
126
- }
127
175
  function postCreate(parse, method) {
128
176
  return (req, res, next) => {
129
177
  const user = req.user;
@@ -154,6 +202,32 @@ function postStringCreate(method) {
154
202
  }
155
203
  };
156
204
  }
205
+ function postReadWithStreaming(method) {
206
+ return (req, res, next) => {
207
+ const user = req.user;
208
+ const input = parseString(req.body);
209
+ if (!input || typeof (input) !== 'string') {
210
+ res.type("text");
211
+ res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
212
+ }
213
+ else {
214
+ // Check if Accept header explicitly prefers a specific format
215
+ const acceptHeader = req.get('Accept');
216
+ let acceptType = 'text/plain'; // Default for backward compatibility
217
+ if (acceptHeader && acceptHeader !== '*/*') {
218
+ // Only use req.accepts() when there's a specific preference
219
+ const preferredType = req.accepts(['text/csv', 'application/x-ndjson', 'application/json', 'text/plain']);
220
+ acceptType = preferredType ? String(preferredType) : 'text/plain';
221
+ }
222
+ method(user, input, acceptType)
223
+ .then(({ resultStream, csvMetadata }) => {
224
+ outputReadResults(resultStream, res, acceptType, csvMetadata);
225
+ next();
226
+ })
227
+ .catch(error => handleError(error, req, res, next));
228
+ }
229
+ };
230
+ }
157
231
  function serializeUserIdentity(user) {
158
232
  if (!user) {
159
233
  return null;
@@ -203,6 +277,16 @@ function outputFeeds(result, res, accepts) {
203
277
  res.type("json");
204
278
  res.send(JSON.stringify(result));
205
279
  }
280
+ function outputReadResults(result, res, acceptType, csvMetadata) {
281
+ // Use streaming output formatter
282
+ (0, output_formatters_1.outputReadResultsStreaming)(result, res, acceptType, csvMetadata)
283
+ .catch(error => {
284
+ console.error('Error in outputReadResults:', error);
285
+ if (!res.headersSent) {
286
+ res.status(500).send('Internal server error');
287
+ }
288
+ });
289
+ }
206
290
  class HttpRouter {
207
291
  constructor(factManager, authorization, feedCache, allowedOrigin) {
208
292
  this.factManager = factManager;
@@ -214,7 +298,7 @@ class HttpRouter {
214
298
  router.get('/login', applyAllowOrigin, getAuthenticate(user => this.login(user)));
215
299
  router.post('/load', applyAllowOrigin, post(jinaga_1.parseLoadMessage, (user, loadMessage) => this.load(user, loadMessage), outputGraph));
216
300
  router.post('/save', applyAllowOrigin, postCreate(inputSaveMessage, (user, graphSource) => this.save(user, graphSource)));
217
- router.post('/read', applyAllowOrigin, postString((user, input) => this.read(user, input)));
301
+ router.post('/read', applyAllowOrigin, postReadWithStreaming((user, input, acceptType) => this.readWithStreaming(user, input, acceptType === 'text/csv' ? preProcessForCsv : preProcessForOther)));
218
302
  router.post('/write', applyAllowOrigin, postStringCreate((user, input) => this.write(user, input)));
219
303
  router.post('/feeds', applyAllowOrigin, post(parseString, (user, input, accepts) => this.feeds(user, input), outputFeeds));
220
304
  router.get('/feeds/:hash', applyAllowOrigin, getOrStream((user, params, query) => this.feed(user, params, query), (user, params, query) => this.streamFeed(user, params, query)));
@@ -273,25 +357,38 @@ class HttpRouter {
273
357
  }));
274
358
  }));
275
359
  }
276
- read(user, input) {
277
- return jinaga_1.Trace.dependency("read", "", () => __awaiter(this, void 0, void 0, function* () {
278
- const knownFacts = yield this.getKnownFacts(user);
279
- const parser = new jinaga_1.SpecificationParser(input);
280
- parser.skipWhitespace();
281
- const declaration = parser.parseDeclaration(knownFacts);
282
- const specification = parser.parseSpecification();
283
- parser.expectEnd();
284
- const start = this.selectStart(specification, declaration);
285
- var failures = this.factManager.testSpecificationForCompliance(specification);
286
- if (failures.length > 0) {
287
- throw new jinaga_1.Invalid(failures.join("\n"));
288
- }
289
- const userIdentity = serializeUserIdentity(user);
290
- const results = yield this.authorization.read(userIdentity, start, specification);
291
- const extracted = extractResults(results);
292
- jinaga_1.Trace.counter("facts_read", extracted.count);
293
- return extracted.result;
294
- }));
360
+ /**
361
+ * Read with streaming support.
362
+ * Uses readStream if available on the authorization object, otherwise falls back to read().
363
+ *
364
+ * When CSV format is requested, validates that the specification contains only flat projections.
365
+ */
366
+ readWithStreaming(user, input, preProcess) {
367
+ return __awaiter(this, void 0, void 0, function* () {
368
+ return jinaga_1.Trace.dependency("readWithStreaming", "", () => __awaiter(this, void 0, void 0, function* () {
369
+ const knownFacts = yield this.getKnownFacts(user);
370
+ const parser = new jinaga_1.SpecificationParser(input);
371
+ parser.skipWhitespace();
372
+ const declaration = parser.parseDeclaration(knownFacts);
373
+ const specification = parser.parseSpecification();
374
+ parser.expectEnd();
375
+ var failures = this.factManager.testSpecificationForCompliance(specification);
376
+ if (failures.length > 0) {
377
+ throw new jinaga_1.Invalid(failures.join("\n"));
378
+ }
379
+ const csvMetadata = preProcess(specification);
380
+ const userIdentity = serializeUserIdentity(user);
381
+ const start = this.selectStart(specification, declaration);
382
+ const results = yield this.authorization.read(userIdentity, start, specification);
383
+ const extracted = extractResults(results);
384
+ jinaga_1.Trace.counter("facts_read", extracted.count);
385
+ const resultStream = (0, result_stream_1.arrayToResultStream)(extracted.result);
386
+ return {
387
+ resultStream,
388
+ csvMetadata
389
+ };
390
+ }));
391
+ });
295
392
  }
296
393
  write(user, input) {
297
394
  return jinaga_1.Trace.dependency("write", "", () => __awaiter(this, void 0, void 0, function* () {
@@ -329,8 +426,8 @@ class HttpRouter {
329
426
  }
330
427
  // Verify that the input type matches the start fact type
331
428
  for (let i = 0; i < start.length; i++) {
332
- if (start[i].type !== specification.given[i].type) {
333
- throw new jinaga_1.Invalid(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
429
+ if (start[i].type !== specification.given[i].label.type) {
430
+ throw new jinaga_1.Invalid(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].label.type})`);
334
431
  }
335
432
  }
336
433
  // Verify that the specification is compliant with purge conditions
@@ -338,7 +435,7 @@ class HttpRouter {
338
435
  if (failures.length > 0) {
339
436
  throw new jinaga_1.Invalid(failures.join("\n"));
340
437
  }
341
- const namedStart = specification.given.reduce((map, label, index) => (Object.assign(Object.assign({}, map), { [label.name]: start[index] })), {});
438
+ const namedStart = specification.given.reduce((map, g, index) => (Object.assign(Object.assign({}, map), { [g.label.name]: start[index] })), {});
342
439
  // Verify that I can distribute all feeds to the user.
343
440
  const feeds = (0, jinaga_1.buildFeeds)(specification);
344
441
  const userIdentity = serializeUserIdentity(user);
@@ -362,7 +459,7 @@ class HttpRouter {
362
459
  }
363
460
  const bookmark = (_a = query["b"]) !== null && _a !== void 0 ? _a : "";
364
461
  const userIdentity = serializeUserIdentity(user);
365
- const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
462
+ const start = feedDefinition.feed.given.map(g => feedDefinition.namedStart[g.label.name]);
366
463
  const results = yield this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
367
464
  // Return distinct fact references from all the tuples.
368
465
  const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index);
@@ -376,64 +473,135 @@ class HttpRouter {
376
473
  streamFeed(user, params, query) {
377
474
  var _a;
378
475
  return __awaiter(this, void 0, void 0, function* () {
476
+ const connectionId = Math.random().toString(36).substring(2, 10);
477
+ const startTime = Date.now();
478
+ console.log(`[StreamFeed:${connectionId}] NEW CONNECTION - User: ${(user === null || user === void 0 ? void 0 : user.id) || 'anonymous'}, Hash: ${params["hash"]}`);
379
479
  const feedHash = params["hash"];
380
480
  if (!feedHash) {
481
+ console.log(`[StreamFeed:${connectionId}] No feed hash provided`);
381
482
  return null;
382
483
  }
383
484
  const feedDefinition = yield this.feedCache.getFeed(feedHash);
384
485
  if (!feedDefinition) {
486
+ console.log(`[StreamFeed:${connectionId}] Feed definition not found for hash: ${feedHash}`);
385
487
  return null;
386
488
  }
387
489
  let bookmark = (_a = query["b"]) !== null && _a !== void 0 ? _a : "";
490
+ console.log(`[StreamFeed:${connectionId}] Initial bookmark: ${bookmark || 'empty'}`);
388
491
  const userIdentity = serializeUserIdentity(user);
389
- const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
492
+ const start = feedDefinition.feed.given.map(g => feedDefinition.namedStart[g.label.name]);
390
493
  const givenHash = (0, jinaga_1.computeObjectHash)(feedDefinition.namedStart);
494
+ console.log(`[StreamFeed:${connectionId}] Feed setup - Given hash: ${givenHash.substring(0, 8)}..., Start facts: ${start.length}`);
391
495
  const stream = new stream_1.Stream();
392
- // Continuous initial query until exhausted
393
- bookmark = yield this.streamAllInitialResults(userIdentity, feedDefinition, start, bookmark, stream);
394
- // Set up real-time listeners after initial data is complete
395
- const inverses = (0, jinaga_1.invertSpecification)(feedDefinition.feed);
396
- const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(inverse.inverseSpecification, (results) => __awaiter(this, void 0, void 0, function* () {
397
- // Filter out results that do not match the given.
398
- const matchingResults = results.filter(pr => givenHash === (0, jinaga_1.computeTupleSubsetHash)(pr.tuple, inverse.givenSubset));
399
- if (matchingResults.length != 0) {
400
- bookmark = yield this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
401
- }
402
- })));
403
- stream.done(() => {
404
- for (const listener of listeners) {
405
- this.factManager.removeSpecificationListener(listener);
406
- }
407
- });
408
- return stream;
496
+ try {
497
+ // Continuous initial query until exhausted
498
+ console.log(`[StreamFeed:${connectionId}] Starting initial data streaming...`);
499
+ const initialStart = Date.now();
500
+ bookmark = yield this.streamAllInitialResults(userIdentity, feedDefinition, start, bookmark, stream);
501
+ const initialDuration = Date.now() - initialStart;
502
+ console.log(`[StreamFeed:${connectionId}] Initial data streaming complete - Duration: ${initialDuration}ms, Final bookmark: ${bookmark || 'empty'}`);
503
+ // Set up real-time listeners after initial data is complete
504
+ console.log(`[StreamFeed:${connectionId}] Setting up real-time listeners...`);
505
+ const inverses = (0, jinaga_1.invertSpecification)(feedDefinition.feed);
506
+ console.log(`[StreamFeed:${connectionId}] Created ${inverses.length} inverse specifications`);
507
+ const listeners = inverses.map((inverse, index) => {
508
+ console.log(`[StreamFeed:${connectionId}] Adding listener ${index + 1}/${inverses.length}`);
509
+ return this.factManager.addSpecificationListener(inverse.inverseSpecification, (results) => __awaiter(this, void 0, void 0, function* () {
510
+ const eventStart = Date.now();
511
+ console.log(`[StreamFeed:${connectionId}] EVENT RECEIVED - Listener ${index + 1}, Results: ${results.length}`);
512
+ try {
513
+ // Filter out results that do not match the given.
514
+ const matchingResults = results.filter(pr => givenHash === (0, jinaga_1.computeTupleSubsetHash)(pr.tuple, inverse.givenSubset));
515
+ console.log(`[StreamFeed:${connectionId}] Filtered results - Matching: ${matchingResults.length}/${results.length}`);
516
+ if (matchingResults.length != 0) {
517
+ console.log(`[StreamFeed:${connectionId}] Processing matching results...`);
518
+ const responseStart = Date.now();
519
+ bookmark = yield this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
520
+ const responseDuration = Date.now() - responseStart;
521
+ console.log(`[StreamFeed:${connectionId}] Feed response sent - Duration: ${responseDuration}ms, New bookmark: ${bookmark || 'empty'}`);
522
+ }
523
+ else {
524
+ console.log(`[StreamFeed:${connectionId}] No matching results - skipping response`);
525
+ }
526
+ }
527
+ catch (error) {
528
+ console.error(`[StreamFeed:${connectionId}] ERROR processing event: ${error}`);
529
+ }
530
+ const eventDuration = Date.now() - eventStart;
531
+ if (eventDuration > 100) {
532
+ console.warn(`[StreamFeed:${connectionId}] SLOW event processing - Duration: ${eventDuration}ms`);
533
+ }
534
+ }));
535
+ });
536
+ console.log(`[StreamFeed:${connectionId}] All listeners registered - Count: ${listeners.length}`);
537
+ stream.done(() => {
538
+ const cleanupStart = Date.now();
539
+ console.log(`[StreamFeed:${connectionId}] CLEANUP STARTED - Removing ${listeners.length} listeners`);
540
+ for (let i = 0; i < listeners.length; i++) {
541
+ try {
542
+ this.factManager.removeSpecificationListener(listeners[i]);
543
+ console.log(`[StreamFeed:${connectionId}] Removed listener ${i + 1}/${listeners.length}`);
544
+ }
545
+ catch (error) {
546
+ console.error(`[StreamFeed:${connectionId}] ERROR removing listener ${i + 1}: ${error}`);
547
+ }
548
+ }
549
+ const cleanupDuration = Date.now() - cleanupStart;
550
+ const totalDuration = Date.now() - startTime;
551
+ console.log(`[StreamFeed:${connectionId}] CLEANUP COMPLETE - Cleanup duration: ${cleanupDuration}ms, Total connection duration: ${totalDuration}ms`);
552
+ });
553
+ const setupDuration = Date.now() - startTime;
554
+ console.log(`[StreamFeed:${connectionId}] Stream setup complete - Duration: ${setupDuration}ms`);
555
+ return stream;
556
+ }
557
+ catch (error) {
558
+ console.error(`[StreamFeed:${connectionId}] ERROR during setup: ${error}`);
559
+ throw error;
560
+ }
409
561
  });
410
562
  }
411
563
  streamAllInitialResults(userIdentity, feedDefinition, start, initialBookmark, stream) {
412
564
  return __awaiter(this, void 0, void 0, function* () {
565
+ const streamId = Math.random().toString(36).substring(2, 8);
566
+ console.log(`[InitialResults:${streamId}] Starting initial data fetch - Bookmark: ${initialBookmark || 'empty'}`);
413
567
  let bookmark = initialBookmark;
414
568
  let hasMoreResults = true;
415
569
  let pageCount = 0;
570
+ let totalReferences = 0;
416
571
  const maxPages = 1000; // Safety limit to prevent infinite loops
572
+ const startTime = Date.now();
417
573
  while (hasMoreResults && pageCount < maxPages) {
574
+ const pageStart = Date.now();
575
+ console.log(`[InitialResults:${streamId}] Fetching page ${pageCount + 1} - Bookmark: ${bookmark || 'empty'}`);
418
576
  const results = yield this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
577
+ const fetchDuration = Date.now() - pageStart;
578
+ console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} fetched - Tuples: ${results.tuples.length}, Duration: ${fetchDuration}ms`);
419
579
  // Check if we got results
420
580
  if (results.tuples.length === 0) {
581
+ console.log(`[InitialResults:${streamId}] No more results - ending pagination`);
421
582
  hasMoreResults = false;
422
583
  break;
423
584
  }
424
585
  // Process and send results
586
+ const processStart = Date.now();
425
587
  const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index);
588
+ const processDuration = Date.now() - processStart;
589
+ console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} processed - References: ${references.length}, Process duration: ${processDuration}ms`);
426
590
  if (references.length > 0) {
427
591
  const response = {
428
592
  references,
429
593
  bookmark: results.bookmark
430
594
  };
595
+ const feedStart = Date.now();
431
596
  stream.feed(response);
597
+ const feedDuration = Date.now() - feedStart;
598
+ totalReferences += references.length;
599
+ console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} sent to stream - Feed duration: ${feedDuration}ms`);
432
600
  }
433
601
  // Update bookmark for next iteration
434
602
  const newBookmark = results.bookmark;
435
603
  if (newBookmark === bookmark) {
436
- // No progress made, avoid infinite loop
604
+ console.log(`[InitialResults:${streamId}] Bookmark unchanged - ending pagination`);
437
605
  hasMoreResults = false;
438
606
  }
439
607
  else {
@@ -441,29 +609,53 @@ class HttpRouter {
441
609
  }
442
610
  // Check if we got fewer results than the page size (indicates end)
443
611
  if (results.tuples.length < 100) {
612
+ console.log(`[InitialResults:${streamId}] Partial page received (${results.tuples.length} < 100) - ending pagination`);
444
613
  hasMoreResults = false;
445
614
  }
446
615
  pageCount++;
447
616
  // Add small delay to prevent overwhelming the database
448
617
  if (hasMoreResults && pageCount % 10 === 0) {
618
+ console.log(`[InitialResults:${streamId}] Adding delay after ${pageCount} pages`);
449
619
  yield new Promise(resolve => setTimeout(resolve, 10));
450
620
  }
451
621
  }
622
+ const totalDuration = Date.now() - startTime;
623
+ console.log(`[InitialResults:${streamId}] Initial data streaming complete - Pages: ${pageCount}, Total references: ${totalReferences}, Duration: ${totalDuration}ms, Final bookmark: ${bookmark || 'empty'}`);
624
+ if (pageCount >= maxPages) {
625
+ console.warn(`[InitialResults:${streamId}] Hit maximum page limit (${maxPages}) - possible infinite loop`);
626
+ }
452
627
  return bookmark;
453
628
  });
454
629
  }
455
630
  streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, skipIfEmpty = false) {
456
631
  return __awaiter(this, void 0, void 0, function* () {
632
+ const responseId = Math.random().toString(36).substring(2, 6);
633
+ const startTime = Date.now();
634
+ console.log(`[FeedResponse:${responseId}] Starting feed response - Bookmark: ${bookmark || 'empty'}, Skip if empty: ${skipIfEmpty}`);
635
+ const feedStart = Date.now();
457
636
  const results = yield this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
637
+ const feedDuration = Date.now() - feedStart;
638
+ console.log(`[FeedResponse:${responseId}] Authorization feed complete - Tuples: ${results.tuples.length}, Duration: ${feedDuration}ms`);
458
639
  // Return distinct fact references from all the tuples.
640
+ const processStart = Date.now();
459
641
  const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index);
642
+ const processDuration = Date.now() - processStart;
643
+ console.log(`[FeedResponse:${responseId}] References processed - Count: ${references.length}, Duration: ${processDuration}ms`);
460
644
  if (!skipIfEmpty || references.length > 0) {
461
645
  const response = {
462
646
  references,
463
647
  bookmark: results.bookmark
464
648
  };
649
+ const streamStart = Date.now();
465
650
  stream.feed(response);
651
+ const streamDuration = Date.now() - streamStart;
652
+ console.log(`[FeedResponse:${responseId}] Response sent to stream - Duration: ${streamDuration}ms`);
466
653
  }
654
+ else {
655
+ console.log(`[FeedResponse:${responseId}] Skipping empty response`);
656
+ }
657
+ const totalDuration = Date.now() - startTime;
658
+ console.log(`[FeedResponse:${responseId}] Feed response complete - New bookmark: ${results.bookmark || 'empty'}, Total duration: ${totalDuration}ms`);
467
659
  return results.bookmark;
468
660
  });
469
661
  }
@@ -495,9 +687,9 @@ class HttpRouter {
495
687
  selectStart(specification, declaration) {
496
688
  // Select starting facts that match the inputs
497
689
  return specification.given.map(input => {
498
- const declaredFact = declaration.find(d => d.name === input.name);
690
+ const declaredFact = declaration.find(d => d.name === input.label.name);
499
691
  if (!declaredFact) {
500
- throw new jinaga_1.Invalid(`No fact named ${input.name} was declared`);
692
+ throw new jinaga_1.Invalid(`No fact named ${input.label.name} was declared`);
501
693
  }
502
694
  return declaredFact.declared.reference;
503
695
  });
@@ -568,6 +760,20 @@ class HttpRouter {
568
760
  }
569
761
  }
570
762
  exports.HttpRouter = HttpRouter;
763
+ function preProcessForCsv(specification) {
764
+ const csvMetadata = (0, csv_validator_1.validateSpecificationForCsv)(specification);
765
+ if (!csvMetadata.isValid) {
766
+ throw new jinaga_1.Invalid(`Specification is not compatible with CSV format:\n\n` +
767
+ csvMetadata.errors.join('\n\n') +
768
+ `\n\nHint: CSV requires flat projections with single-valued fields. ` +
769
+ `Avoid nested specifications.`);
770
+ }
771
+ return csvMetadata;
772
+ }
773
+ function preProcessForOther(specification) {
774
+ // For non-CSV formats, no special preprocessing is needed.
775
+ return undefined;
776
+ }
571
777
  function parseString(input) {
572
778
  if (typeof input !== 'string') {
573
779
  throw new jinaga_1.Invalid("Expected a string. Check the content type of the request.");