jinaga-server 3.5.1 → 3.5.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.
- package/dist/http/router.js +183 -24
- 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/docs/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
- package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
- package/package.json +2 -2
- package/src/http/router.ts +232 -29
- package/src/http/stream.ts +97 -5
- package/test-concurrent-streams.js +219 -0
package/src/http/router.ts
CHANGED
|
@@ -40,53 +40,131 @@ function getOrStream<U>(
|
|
|
40
40
|
streamMethod: ((req: RequestUser, params: { [key: string]: string }, query: qs.ParsedQs) => Promise<Stream<U> | null>)
|
|
41
41
|
): Handler {
|
|
42
42
|
return (req, res, next) => {
|
|
43
|
+
const connectionId = Math.random().toString(36).substring(2, 10);
|
|
43
44
|
const user = <RequestUser>(req as any).user;
|
|
44
45
|
const accept = req.headers["accept"];
|
|
46
|
+
|
|
47
|
+
console.log(`[HttpConnection:${connectionId}] New request - User: ${user?.id || 'anonymous'}, Accept: ${accept}, URL: ${req.url}`);
|
|
48
|
+
|
|
45
49
|
if (accept && accept.indexOf("application/x-jinaga-feed-stream") >= 0) {
|
|
50
|
+
console.log(`[HttpConnection:${connectionId}] STREAMING request detected`);
|
|
51
|
+
|
|
46
52
|
streamMethod(user, req.params, req.query)
|
|
47
53
|
.then(response => {
|
|
48
54
|
if (!response) {
|
|
55
|
+
console.log(`[HttpConnection:${connectionId}] Stream method returned null - sending 404`);
|
|
49
56
|
res.sendStatus(404);
|
|
50
57
|
next();
|
|
51
58
|
}
|
|
52
59
|
else {
|
|
60
|
+
console.log(`[HttpConnection:${connectionId}] Setting up streaming response`);
|
|
61
|
+
|
|
53
62
|
res.type("application/x-jinaga-feed-stream");
|
|
54
63
|
res.set("Connection", "keep-alive");
|
|
55
64
|
res.set("Cache-Control", "no-cache");
|
|
56
65
|
res.set("Access-Control-Allow-Origin", "*");
|
|
57
66
|
res.flushHeaders();
|
|
67
|
+
|
|
68
|
+
console.log(`[HttpConnection:${connectionId}] Headers flushed - setting up event handlers`);
|
|
69
|
+
|
|
70
|
+
let clientDisconnected = false;
|
|
71
|
+
let timeoutTriggered = false;
|
|
72
|
+
let streamClosed = false;
|
|
73
|
+
|
|
74
|
+
// Client disconnect handler
|
|
58
75
|
req.on("close", () => {
|
|
59
|
-
|
|
76
|
+
console.log(`[HttpConnection:${connectionId}] CLIENT DISCONNECTED`);
|
|
77
|
+
clientDisconnected = true;
|
|
78
|
+
if (!streamClosed) {
|
|
79
|
+
console.log(`[HttpConnection:${connectionId}] Closing stream due to client disconnect`);
|
|
80
|
+
response.close();
|
|
81
|
+
streamClosed = true;
|
|
82
|
+
}
|
|
60
83
|
});
|
|
84
|
+
|
|
85
|
+
// Timeout handler
|
|
61
86
|
const timeout = setTimeout(() => {
|
|
62
|
-
|
|
87
|
+
console.log(`[HttpConnection:${connectionId}] TIMEOUT TRIGGERED (5 minutes)`);
|
|
88
|
+
timeoutTriggered = true;
|
|
89
|
+
if (!streamClosed) {
|
|
90
|
+
console.log(`[HttpConnection:${connectionId}] Closing stream due to timeout`);
|
|
91
|
+
response.close();
|
|
92
|
+
streamClosed = true;
|
|
93
|
+
}
|
|
63
94
|
}, 5 * 60 * 1000);
|
|
95
|
+
|
|
96
|
+
let messageCount = 0;
|
|
97
|
+
|
|
64
98
|
response
|
|
65
99
|
.next(data => {
|
|
66
|
-
|
|
100
|
+
messageCount++;
|
|
101
|
+
const writeStart = Date.now();
|
|
102
|
+
|
|
103
|
+
if (clientDisconnected) {
|
|
104
|
+
console.warn(`[HttpConnection:${connectionId}] Attempted to write to disconnected client - Message #${messageCount}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const jsonData = JSON.stringify(data) + "\n\n";
|
|
110
|
+
res.write(jsonData);
|
|
111
|
+
const writeDuration = Date.now() - writeStart;
|
|
112
|
+
|
|
113
|
+
console.log(`[HttpConnection:${connectionId}] Message #${messageCount} sent - Size: ${jsonData.length} bytes, Duration: ${writeDuration}ms`);
|
|
114
|
+
|
|
115
|
+
if (writeDuration > 50) {
|
|
116
|
+
console.warn(`[HttpConnection:${connectionId}] SLOW write operation - Duration: ${writeDuration}ms`);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[HttpConnection:${connectionId}] ERROR writing message #${messageCount}: ${error}`);
|
|
120
|
+
}
|
|
67
121
|
})
|
|
68
122
|
.done(() => {
|
|
123
|
+
console.log(`[HttpConnection:${connectionId}] STREAM DONE - Messages sent: ${messageCount}, Client disconnected: ${clientDisconnected}, Timeout triggered: ${timeoutTriggered}`);
|
|
124
|
+
|
|
69
125
|
clearTimeout(timeout);
|
|
70
|
-
|
|
126
|
+
streamClosed = true;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
if (!clientDisconnected) {
|
|
130
|
+
console.log(`[HttpConnection:${connectionId}] Ending socket connection`);
|
|
131
|
+
res.socket?.end();
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`[HttpConnection:${connectionId}] Client already disconnected - skipping socket end`);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`[HttpConnection:${connectionId}] ERROR ending socket: ${error}`);
|
|
137
|
+
}
|
|
71
138
|
});
|
|
139
|
+
|
|
140
|
+
console.log(`[HttpConnection:${connectionId}] Stream handlers configured`);
|
|
72
141
|
}
|
|
73
142
|
})
|
|
74
|
-
.catch(error =>
|
|
143
|
+
.catch(error => {
|
|
144
|
+
console.error(`[HttpConnection:${connectionId}] ERROR in stream method: ${error}`);
|
|
145
|
+
handleError(error, req, res, next);
|
|
146
|
+
});
|
|
75
147
|
}
|
|
76
148
|
else {
|
|
149
|
+
console.log(`[HttpConnection:${connectionId}] Regular GET request`);
|
|
77
150
|
getMethod(user, req.params, req.query)
|
|
78
151
|
.then(response => {
|
|
79
152
|
if (!response) {
|
|
153
|
+
console.log(`[HttpConnection:${connectionId}] Get method returned null - sending 404`);
|
|
80
154
|
res.sendStatus(404);
|
|
81
155
|
next();
|
|
82
156
|
}
|
|
83
157
|
else {
|
|
158
|
+
console.log(`[HttpConnection:${connectionId}] Sending JSON response`);
|
|
84
159
|
res.type("json");
|
|
85
160
|
res.send(JSON.stringify(response));
|
|
86
161
|
next();
|
|
87
162
|
}
|
|
88
163
|
})
|
|
89
|
-
.catch(error =>
|
|
164
|
+
.catch(error => {
|
|
165
|
+
console.error(`[HttpConnection:${connectionId}] ERROR in get method: ${error}`);
|
|
166
|
+
handleError(error, req, res, next);
|
|
167
|
+
});
|
|
90
168
|
}
|
|
91
169
|
};
|
|
92
170
|
}
|
|
@@ -466,46 +544,113 @@ export class HttpRouter {
|
|
|
466
544
|
}
|
|
467
545
|
|
|
468
546
|
private async streamFeed(user: RequestUser | null, params: { [key: string]: string }, query: qs.ParsedQs): Promise<Stream<FeedResponse> | null> {
|
|
547
|
+
const connectionId = Math.random().toString(36).substring(2, 10);
|
|
548
|
+
const startTime = Date.now();
|
|
549
|
+
|
|
550
|
+
console.log(`[StreamFeed:${connectionId}] NEW CONNECTION - User: ${user?.id || 'anonymous'}, Hash: ${params["hash"]}`);
|
|
551
|
+
|
|
469
552
|
const feedHash = params["hash"];
|
|
470
553
|
if (!feedHash) {
|
|
554
|
+
console.log(`[StreamFeed:${connectionId}] No feed hash provided`);
|
|
471
555
|
return null;
|
|
472
556
|
}
|
|
473
557
|
|
|
474
558
|
const feedDefinition = await this.feedCache.getFeed(feedHash);
|
|
475
559
|
if (!feedDefinition) {
|
|
560
|
+
console.log(`[StreamFeed:${connectionId}] Feed definition not found for hash: ${feedHash}`);
|
|
476
561
|
return null;
|
|
477
562
|
}
|
|
478
563
|
|
|
479
564
|
let bookmark = query["b"] as string ?? "";
|
|
565
|
+
console.log(`[StreamFeed:${connectionId}] Initial bookmark: ${bookmark || 'empty'}`);
|
|
480
566
|
|
|
481
567
|
const userIdentity = serializeUserIdentity(user);
|
|
482
568
|
const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
|
|
483
569
|
const givenHash = computeObjectHash(feedDefinition.namedStart);
|
|
484
570
|
|
|
571
|
+
console.log(`[StreamFeed:${connectionId}] Feed setup - Given hash: ${givenHash.substring(0, 8)}..., Start facts: ${start.length}`);
|
|
572
|
+
|
|
485
573
|
const stream = new Stream<FeedResponse>();
|
|
486
574
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
575
|
+
try {
|
|
576
|
+
// Continuous initial query until exhausted
|
|
577
|
+
console.log(`[StreamFeed:${connectionId}] Starting initial data streaming...`);
|
|
578
|
+
const initialStart = Date.now();
|
|
579
|
+
bookmark = await this.streamAllInitialResults(userIdentity, feedDefinition, start, bookmark, stream);
|
|
580
|
+
const initialDuration = Date.now() - initialStart;
|
|
581
|
+
console.log(`[StreamFeed:${connectionId}] Initial data streaming complete - Duration: ${initialDuration}ms, Final bookmark: ${bookmark || 'empty'}`);
|
|
582
|
+
|
|
583
|
+
// Set up real-time listeners after initial data is complete
|
|
584
|
+
console.log(`[StreamFeed:${connectionId}] Setting up real-time listeners...`);
|
|
585
|
+
const inverses = invertSpecification(feedDefinition.feed);
|
|
586
|
+
console.log(`[StreamFeed:${connectionId}] Created ${inverses.length} inverse specifications`);
|
|
587
|
+
|
|
588
|
+
const listeners = inverses.map((inverse, index) => {
|
|
589
|
+
console.log(`[StreamFeed:${connectionId}] Adding listener ${index + 1}/${inverses.length}`);
|
|
590
|
+
|
|
591
|
+
return this.factManager.addSpecificationListener(
|
|
592
|
+
inverse.inverseSpecification,
|
|
593
|
+
async (results) => {
|
|
594
|
+
const eventStart = Date.now();
|
|
595
|
+
console.log(`[StreamFeed:${connectionId}] EVENT RECEIVED - Listener ${index + 1}, Results: ${results.length}`);
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
// Filter out results that do not match the given.
|
|
599
|
+
const matchingResults = results.filter(pr =>
|
|
600
|
+
givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
|
|
601
|
+
|
|
602
|
+
console.log(`[StreamFeed:${connectionId}] Filtered results - Matching: ${matchingResults.length}/${results.length}`);
|
|
603
|
+
|
|
604
|
+
if (matchingResults.length != 0) {
|
|
605
|
+
console.log(`[StreamFeed:${connectionId}] Processing matching results...`);
|
|
606
|
+
const responseStart = Date.now();
|
|
607
|
+
bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
|
|
608
|
+
const responseDuration = Date.now() - responseStart;
|
|
609
|
+
console.log(`[StreamFeed:${connectionId}] Feed response sent - Duration: ${responseDuration}ms, New bookmark: ${bookmark || 'empty'}`);
|
|
610
|
+
} else {
|
|
611
|
+
console.log(`[StreamFeed:${connectionId}] No matching results - skipping response`);
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
console.error(`[StreamFeed:${connectionId}] ERROR processing event: ${error}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const eventDuration = Date.now() - eventStart;
|
|
618
|
+
if (eventDuration > 100) {
|
|
619
|
+
console.warn(`[StreamFeed:${connectionId}] SLOW event processing - Duration: ${eventDuration}ms`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
console.log(`[StreamFeed:${connectionId}] All listeners registered - Count: ${listeners.length}`);
|
|
626
|
+
|
|
627
|
+
stream.done(() => {
|
|
628
|
+
const cleanupStart = Date.now();
|
|
629
|
+
console.log(`[StreamFeed:${connectionId}] CLEANUP STARTED - Removing ${listeners.length} listeners`);
|
|
630
|
+
|
|
631
|
+
for (let i = 0; i < listeners.length; i++) {
|
|
632
|
+
try {
|
|
633
|
+
this.factManager.removeSpecificationListener(listeners[i]);
|
|
634
|
+
console.log(`[StreamFeed:${connectionId}] Removed listener ${i + 1}/${listeners.length}`);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error(`[StreamFeed:${connectionId}] ERROR removing listener ${i + 1}: ${error}`);
|
|
637
|
+
}
|
|
500
638
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
639
|
+
|
|
640
|
+
const cleanupDuration = Date.now() - cleanupStart;
|
|
641
|
+
const totalDuration = Date.now() - startTime;
|
|
642
|
+
console.log(`[StreamFeed:${connectionId}] CLEANUP COMPLETE - Cleanup duration: ${cleanupDuration}ms, Total connection duration: ${totalDuration}ms`);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const setupDuration = Date.now() - startTime;
|
|
646
|
+
console.log(`[StreamFeed:${connectionId}] Stream setup complete - Duration: ${setupDuration}ms`);
|
|
647
|
+
|
|
648
|
+
return stream;
|
|
649
|
+
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error(`[StreamFeed:${connectionId}] ERROR during setup: ${error}`);
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
509
654
|
}
|
|
510
655
|
|
|
511
656
|
private async streamAllInitialResults(
|
|
@@ -515,37 +660,59 @@ export class HttpRouter {
|
|
|
515
660
|
initialBookmark: string,
|
|
516
661
|
stream: Stream<FeedResponse>
|
|
517
662
|
): Promise<string> {
|
|
663
|
+
const streamId = Math.random().toString(36).substring(2, 8);
|
|
664
|
+
console.log(`[InitialResults:${streamId}] Starting initial data fetch - Bookmark: ${initialBookmark || 'empty'}`);
|
|
665
|
+
|
|
518
666
|
let bookmark = initialBookmark;
|
|
519
667
|
let hasMoreResults = true;
|
|
520
668
|
let pageCount = 0;
|
|
669
|
+
let totalReferences = 0;
|
|
521
670
|
const maxPages = 1000; // Safety limit to prevent infinite loops
|
|
671
|
+
const startTime = Date.now();
|
|
522
672
|
|
|
523
673
|
while (hasMoreResults && pageCount < maxPages) {
|
|
674
|
+
const pageStart = Date.now();
|
|
675
|
+
console.log(`[InitialResults:${streamId}] Fetching page ${pageCount + 1} - Bookmark: ${bookmark || 'empty'}`);
|
|
676
|
+
|
|
524
677
|
const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
|
|
678
|
+
const fetchDuration = Date.now() - pageStart;
|
|
679
|
+
|
|
680
|
+
console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} fetched - Tuples: ${results.tuples.length}, Duration: ${fetchDuration}ms`);
|
|
525
681
|
|
|
526
682
|
// Check if we got results
|
|
527
683
|
if (results.tuples.length === 0) {
|
|
684
|
+
console.log(`[InitialResults:${streamId}] No more results - ending pagination`);
|
|
528
685
|
hasMoreResults = false;
|
|
529
686
|
break;
|
|
530
687
|
}
|
|
531
688
|
|
|
532
689
|
// Process and send results
|
|
690
|
+
const processStart = Date.now();
|
|
533
691
|
const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
|
|
534
692
|
self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
|
|
535
693
|
);
|
|
694
|
+
const processDuration = Date.now() - processStart;
|
|
695
|
+
|
|
696
|
+
console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} processed - References: ${references.length}, Process duration: ${processDuration}ms`);
|
|
536
697
|
|
|
537
698
|
if (references.length > 0) {
|
|
538
699
|
const response: FeedResponse = {
|
|
539
700
|
references,
|
|
540
701
|
bookmark: results.bookmark
|
|
541
702
|
};
|
|
703
|
+
|
|
704
|
+
const feedStart = Date.now();
|
|
542
705
|
stream.feed(response);
|
|
706
|
+
const feedDuration = Date.now() - feedStart;
|
|
707
|
+
|
|
708
|
+
totalReferences += references.length;
|
|
709
|
+
console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} sent to stream - Feed duration: ${feedDuration}ms`);
|
|
543
710
|
}
|
|
544
711
|
|
|
545
712
|
// Update bookmark for next iteration
|
|
546
713
|
const newBookmark = results.bookmark;
|
|
547
714
|
if (newBookmark === bookmark) {
|
|
548
|
-
|
|
715
|
+
console.log(`[InitialResults:${streamId}] Bookmark unchanged - ending pagination`);
|
|
549
716
|
hasMoreResults = false;
|
|
550
717
|
} else {
|
|
551
718
|
bookmark = newBookmark;
|
|
@@ -553,6 +720,7 @@ export class HttpRouter {
|
|
|
553
720
|
|
|
554
721
|
// Check if we got fewer results than the page size (indicates end)
|
|
555
722
|
if (results.tuples.length < 100) {
|
|
723
|
+
console.log(`[InitialResults:${streamId}] Partial page received (${results.tuples.length} < 100) - ending pagination`);
|
|
556
724
|
hasMoreResults = false;
|
|
557
725
|
}
|
|
558
726
|
|
|
@@ -560,25 +728,60 @@ export class HttpRouter {
|
|
|
560
728
|
|
|
561
729
|
// Add small delay to prevent overwhelming the database
|
|
562
730
|
if (hasMoreResults && pageCount % 10 === 0) {
|
|
731
|
+
console.log(`[InitialResults:${streamId}] Adding delay after ${pageCount} pages`);
|
|
563
732
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
564
733
|
}
|
|
565
734
|
}
|
|
566
735
|
|
|
736
|
+
const totalDuration = Date.now() - startTime;
|
|
737
|
+
console.log(`[InitialResults:${streamId}] Initial data streaming complete - Pages: ${pageCount}, Total references: ${totalReferences}, Duration: ${totalDuration}ms, Final bookmark: ${bookmark || 'empty'}`);
|
|
738
|
+
|
|
739
|
+
if (pageCount >= maxPages) {
|
|
740
|
+
console.warn(`[InitialResults:${streamId}] Hit maximum page limit (${maxPages}) - possible infinite loop`);
|
|
741
|
+
}
|
|
742
|
+
|
|
567
743
|
return bookmark;
|
|
568
744
|
}
|
|
569
745
|
|
|
570
746
|
private async streamFeedResponse(userIdentity: UserIdentity | null, feedDefinition: FeedObject, start: FactReference[], bookmark: string, stream: Stream<FeedResponse>, skipIfEmpty = false): Promise<string> {
|
|
747
|
+
const responseId = Math.random().toString(36).substring(2, 6);
|
|
748
|
+
const startTime = Date.now();
|
|
749
|
+
|
|
750
|
+
console.log(`[FeedResponse:${responseId}] Starting feed response - Bookmark: ${bookmark || 'empty'}, Skip if empty: ${skipIfEmpty}`);
|
|
751
|
+
|
|
752
|
+
const feedStart = Date.now();
|
|
571
753
|
const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
|
|
754
|
+
const feedDuration = Date.now() - feedStart;
|
|
755
|
+
|
|
756
|
+
console.log(`[FeedResponse:${responseId}] Authorization feed complete - Tuples: ${results.tuples.length}, Duration: ${feedDuration}ms`);
|
|
757
|
+
|
|
572
758
|
// Return distinct fact references from all the tuples.
|
|
573
|
-
const
|
|
759
|
+
const processStart = Date.now();
|
|
760
|
+
const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
|
|
761
|
+
self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
|
|
574
762
|
);
|
|
763
|
+
const processDuration = Date.now() - processStart;
|
|
764
|
+
|
|
765
|
+
console.log(`[FeedResponse:${responseId}] References processed - Count: ${references.length}, Duration: ${processDuration}ms`);
|
|
766
|
+
|
|
575
767
|
if (!skipIfEmpty || references.length > 0) {
|
|
576
768
|
const response: FeedResponse = {
|
|
577
769
|
references,
|
|
578
770
|
bookmark: results.bookmark
|
|
579
771
|
};
|
|
772
|
+
|
|
773
|
+
const streamStart = Date.now();
|
|
580
774
|
stream.feed(response);
|
|
775
|
+
const streamDuration = Date.now() - streamStart;
|
|
776
|
+
|
|
777
|
+
console.log(`[FeedResponse:${responseId}] Response sent to stream - Duration: ${streamDuration}ms`);
|
|
778
|
+
} else {
|
|
779
|
+
console.log(`[FeedResponse:${responseId}] Skipping empty response`);
|
|
581
780
|
}
|
|
781
|
+
|
|
782
|
+
const totalDuration = Date.now() - startTime;
|
|
783
|
+
console.log(`[FeedResponse:${responseId}] Feed response complete - New bookmark: ${results.bookmark || 'empty'}, Total duration: ${totalDuration}ms`);
|
|
784
|
+
|
|
582
785
|
return results.bookmark;
|
|
583
786
|
}
|
|
584
787
|
|
package/src/http/stream.ts
CHANGED
|
@@ -3,30 +3,122 @@ export class Stream<T> {
|
|
|
3
3
|
private handlers: ((data: T) => void)[] = [];
|
|
4
4
|
private doneHandlers: (() => void)[] = [];
|
|
5
5
|
private closed = false;
|
|
6
|
+
private readonly streamId: string;
|
|
7
|
+
private feedCount = 0;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.streamId = Math.random().toString(36).substring(2, 10);
|
|
11
|
+
console.log(`[Stream:${this.streamId}] Created new stream`);
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
next(handler: (data: T) => void): Stream<T> {
|
|
15
|
+
if (this.closed) {
|
|
16
|
+
console.warn(`[Stream:${this.streamId}] Attempted to add handler to closed stream`);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
this.handlers.push(handler);
|
|
9
|
-
this.queue.
|
|
21
|
+
const queueLength = this.queue.length;
|
|
22
|
+
console.log(`[Stream:${this.streamId}] Added handler - Total handlers: ${this.handlers.length}, Queued items: ${queueLength}`);
|
|
23
|
+
|
|
24
|
+
// Replay queued data to new handler
|
|
25
|
+
this.queue.forEach((data, index) => {
|
|
26
|
+
try {
|
|
27
|
+
handler(data);
|
|
28
|
+
console.log(`[Stream:${this.streamId}] Replayed queued item ${index + 1}/${queueLength} to new handler`);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`[Stream:${this.streamId}] Error replaying queued item ${index + 1}: ${error}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
10
34
|
return this;
|
|
11
35
|
}
|
|
12
36
|
|
|
13
37
|
done(handler: () => void): Stream<T> {
|
|
38
|
+
if (this.closed) {
|
|
39
|
+
console.warn(`[Stream:${this.streamId}] Attempted to add done handler to closed stream - executing immediately`);
|
|
40
|
+
try {
|
|
41
|
+
handler();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`[Stream:${this.streamId}] Error in immediate done handler: ${error}`);
|
|
44
|
+
}
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
this.doneHandlers.push(handler);
|
|
49
|
+
console.log(`[Stream:${this.streamId}] Added done handler - Total done handlers: ${this.doneHandlers.length}`);
|
|
15
50
|
return this;
|
|
16
51
|
}
|
|
17
52
|
|
|
18
53
|
close(): void {
|
|
54
|
+
if (this.closed) {
|
|
55
|
+
console.warn(`[Stream:${this.streamId}] Attempted to close already closed stream`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`[Stream:${this.streamId}] CLOSING stream - Handlers: ${this.handlers.length}, Done handlers: ${this.doneHandlers.length}, Queue: ${this.queue.length}, Feed count: ${this.feedCount}`);
|
|
60
|
+
|
|
19
61
|
this.closed = true;
|
|
20
|
-
|
|
62
|
+
|
|
63
|
+
// Execute done handlers
|
|
64
|
+
const doneHandlerCount = this.doneHandlers.length;
|
|
65
|
+
this.doneHandlers.forEach((handler, index) => {
|
|
66
|
+
try {
|
|
67
|
+
handler();
|
|
68
|
+
console.log(`[Stream:${this.streamId}] Executed done handler ${index + 1}/${doneHandlerCount}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`[Stream:${this.streamId}] Error in done handler ${index + 1}: ${error}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Clear all references
|
|
21
75
|
this.queue = [];
|
|
22
76
|
this.handlers = [];
|
|
23
77
|
this.doneHandlers = [];
|
|
78
|
+
|
|
79
|
+
console.log(`[Stream:${this.streamId}] Stream closed and cleaned up`);
|
|
24
80
|
}
|
|
25
81
|
|
|
26
82
|
feed(data: any): void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
83
|
+
const feedStart = Date.now();
|
|
84
|
+
|
|
85
|
+
if (this.closed) {
|
|
86
|
+
console.warn(`[Stream:${this.streamId}] Attempted to feed data to closed stream - Data type: ${typeof data}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.feedCount++;
|
|
91
|
+
const handlerCount = this.handlers.length;
|
|
92
|
+
|
|
93
|
+
console.log(`[Stream:${this.streamId}] FEEDING data - Feed #${this.feedCount}, Handlers: ${handlerCount}, Queue size before: ${this.queue.length}`);
|
|
94
|
+
|
|
95
|
+
this.queue.push(data);
|
|
96
|
+
|
|
97
|
+
// Notify all handlers
|
|
98
|
+
let successCount = 0;
|
|
99
|
+
let errorCount = 0;
|
|
100
|
+
|
|
101
|
+
this.handlers.forEach((handler, index) => {
|
|
102
|
+
try {
|
|
103
|
+
const handlerStart = Date.now();
|
|
104
|
+
handler(data);
|
|
105
|
+
const handlerDuration = Date.now() - handlerStart;
|
|
106
|
+
successCount++;
|
|
107
|
+
|
|
108
|
+
if (handlerDuration > 50) {
|
|
109
|
+
console.warn(`[Stream:${this.streamId}] SLOW handler - Handler ${index + 1}/${handlerCount}, Duration: ${handlerDuration}ms`);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
errorCount++;
|
|
113
|
+
console.error(`[Stream:${this.streamId}] ERROR in handler ${index + 1}/${handlerCount}: ${error}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const totalDuration = Date.now() - feedStart;
|
|
118
|
+
console.log(`[Stream:${this.streamId}] Feed complete - Success: ${successCount}, Errors: ${errorCount}, Queue size after: ${this.queue.length}, Duration: ${totalDuration}ms`);
|
|
119
|
+
|
|
120
|
+
if (totalDuration > 100) {
|
|
121
|
+
console.warn(`[Stream:${this.streamId}] SLOW feed operation - Duration: ${totalDuration}ms`);
|
|
30
122
|
}
|
|
31
123
|
}
|
|
32
124
|
}
|