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.
@@ -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
- response.close();
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
- response.close();
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
- res.write(JSON.stringify(data) + "\n\n");
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
- res.socket?.end();
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 => handleError(error, req, res, next));
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 => handleError(error, req, res, next));
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
- // Continuous initial query until exhausted
488
- bookmark = await this.streamAllInitialResults(userIdentity, feedDefinition, start, bookmark, stream);
489
-
490
- // Set up real-time listeners after initial data is complete
491
- const inverses = invertSpecification(feedDefinition.feed);
492
- const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(
493
- inverse.inverseSpecification,
494
- async (results) => {
495
- // Filter out results that do not match the given.
496
- const matchingResults = results.filter(pr =>
497
- givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
498
- if (matchingResults.length != 0) {
499
- bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
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
- stream.done(() => {
504
- for (const listener of listeners) {
505
- this.factManager.removeSpecificationListener(listener);
506
- }
507
- });
508
- return stream;
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
- // No progress made, avoid infinite loop
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 references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
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
 
@@ -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.forEach(data => handler(data));
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
- this.doneHandlers.forEach(handler => handler());
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
- if (!this.closed) {
28
- this.queue.push(data);
29
- this.handlers.forEach(handler => handler(data));
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
  }