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.
- package/dist/authorization/authorization-keystore.js +2 -2
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/http/csv-metadata.d.ts +24 -0
- package/dist/http/csv-metadata.js +3 -0
- package/dist/http/csv-metadata.js.map +1 -0
- package/dist/http/csv-validator.d.ts +23 -0
- package/dist/http/csv-validator.js +83 -0
- package/dist/http/csv-validator.js.map +1 -0
- package/dist/http/output-formatters.d.ts +28 -0
- package/dist/http/output-formatters.js +197 -0
- package/dist/http/output-formatters.js.map +1 -0
- package/dist/http/result-stream.d.ts +35 -0
- package/dist/http/result-stream.js +78 -0
- package/dist/http/result-stream.js.map +1 -0
- package/dist/http/router.d.ts +7 -1
- package/dist/http/router.js +276 -70
- package/dist/http/router.js.map +1 -1
- package/dist/http/stream.d.ts +3 -0
- package/dist/http/stream.js +79 -5
- package/dist/http/stream.js.map +1 -1
- package/dist/postgres/postgres-store.js +7 -5
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/purge-sql.js +15 -2
- package/dist/postgres/purge-sql.js.map +1 -1
- package/dist/postgres/specification-result-sql.d.ts +2 -0
- package/dist/postgres/specification-result-sql.js +49 -5
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.js +28 -6
- package/dist/postgres/specification-sql.js.map +1 -1
- package/docs/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
- package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
- package/docs/design/feeds-persistent-proxy-plan.md +294 -0
- package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
- package/integration-test/jinaga-test/Dockerfile +1 -6
- package/integration-test/jinaga-test/http-test-helpers.js +77 -0
- package/integration-test/jinaga-test/jinaga.test.js +9 -9
- package/integration-test/jinaga-test/package-lock.json +329 -32
- package/integration-test/jinaga-test/package.json +5 -1
- package/integration-test/jinaga-test/read-csv.test.js +330 -0
- package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
- package/integration-test/jinaga-test/read-performance.test.js +221 -0
- package/integration-test/jinaga-test/read-streaming.test.js +199 -0
- package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
- package/integration-test/jinaga-test/test-helpers.js +12 -0
- package/integration-test/jinaga-test/time-projection.test.js +235 -0
- package/package.json +3 -2
- package/src/authorization/authorization-keystore.ts +4 -4
- package/src/http/csv-metadata.ts +34 -0
- package/src/http/csv-validator.ts +90 -0
- package/src/http/output-formatters.ts +196 -0
- package/src/http/result-stream.ts +69 -0
- package/src/http/router.ts +344 -62
- package/src/http/stream.ts +97 -5
- package/src/postgres/postgres-store.ts +13 -6
- package/src/postgres/purge-sql.ts +18 -2
- package/src/postgres/specification-result-sql.ts +70 -8
- package/src/postgres/specification-sql.ts +36 -8
- package/test/http/csv-validatorSpec.ts +205 -0
- package/test/http/result-streamSpec.ts +151 -0
- package/test/postgres/postgresReadSpec.ts +192 -14
- package/test/postgres/postgresSpecificationSpec.ts +3 -3
- package/test/postgres/specificationConditionsSpec.ts +268 -0
- package/test-concurrent-streams.js +219 -0
- package/documentation/streaming-feeds-full-results-solution.md +0 -230
package/dist/http/router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 =>
|
|
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,
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
this.factManager.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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.");
|