jinaga-server 3.5.0 → 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.
@@ -0,0 +1,230 @@
1
+ # Solution: Full Results Delivery for Streamed Feeds Endpoint
2
+
3
+ ## Problem Analysis
4
+
5
+ The current streaming feeds endpoint has a critical limitation: it only delivers the first 100 results from the initial query and then switches to push-only mode. This means clients never receive complete historical data for feeds with more than 100 matching results.
6
+
7
+ ## Proposed Solution: Continuous Initial Query with Pagination
8
+
9
+ ### Core Design Principle
10
+
11
+ Modify the streaming implementation to **continue fetching and delivering all historical results** before transitioning to real-time push mode, while maintaining the streaming connection throughout the process.
12
+
13
+ ### Implementation Strategy
14
+
15
+ #### 1. Enhanced `streamFeed` Method
16
+
17
+ ```typescript
18
+ private async streamFeed(user: RequestUser | null, params: { [key: string]: string }, query: qs.ParsedQs): Promise<Stream<FeedResponse> | null> {
19
+ const feedHash = params["hash"];
20
+ if (!feedHash) {
21
+ return null;
22
+ }
23
+
24
+ const feedDefinition = await this.feedCache.getFeed(feedHash);
25
+ if (!feedDefinition) {
26
+ return null;
27
+ }
28
+
29
+ let bookmark = query["b"] as string ?? "";
30
+ const userIdentity = serializeUserIdentity(user);
31
+ const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
32
+ const givenHash = computeObjectHash(feedDefinition.namedStart);
33
+
34
+ const stream = new Stream<FeedResponse>();
35
+
36
+ // NEW: Continuous initial query until exhausted
37
+ bookmark = await this.streamAllInitialResults(userIdentity, feedDefinition, start, bookmark, stream);
38
+
39
+ // Set up real-time listeners after initial data is complete
40
+ const inverses = invertSpecification(feedDefinition.feed);
41
+ const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(
42
+ inverse.inverseSpecification,
43
+ async (results) => {
44
+ const matchingResults = results.filter(pr =>
45
+ givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
46
+ if (matchingResults.length != 0) {
47
+ bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
48
+ }
49
+ }
50
+ ));
51
+
52
+ stream.done(() => {
53
+ for (const listener of listeners) {
54
+ this.factManager.removeSpecificationListener(listener);
55
+ }
56
+ });
57
+
58
+ return stream;
59
+ }
60
+ ```
61
+
62
+ #### 2. New `streamAllInitialResults` Method
63
+
64
+ ```typescript
65
+ private async streamAllInitialResults(
66
+ userIdentity: UserIdentity | null,
67
+ feedDefinition: FeedObject,
68
+ start: FactReference[],
69
+ initialBookmark: string,
70
+ stream: Stream<FeedResponse>
71
+ ): Promise<string> {
72
+ let bookmark = initialBookmark;
73
+ let hasMoreResults = true;
74
+ let pageCount = 0;
75
+ const maxPages = 1000; // Safety limit to prevent infinite loops
76
+
77
+ while (hasMoreResults && pageCount < maxPages) {
78
+ const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
79
+
80
+ // Check if we got results
81
+ if (results.tuples.length === 0) {
82
+ hasMoreResults = false;
83
+ break;
84
+ }
85
+
86
+ // Process and send results
87
+ const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
88
+ self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
89
+ );
90
+
91
+ if (references.length > 0) {
92
+ const response: FeedResponse = {
93
+ references,
94
+ bookmark: results.bookmark
95
+ };
96
+ stream.feed(response);
97
+ }
98
+
99
+ // Update bookmark for next iteration
100
+ const newBookmark = results.bookmark;
101
+ if (newBookmark === bookmark) {
102
+ // No progress made, avoid infinite loop
103
+ hasMoreResults = false;
104
+ } else {
105
+ bookmark = newBookmark;
106
+ }
107
+
108
+ // Check if we got fewer results than the page size (indicates end)
109
+ if (results.tuples.length < 100) {
110
+ hasMoreResults = false;
111
+ }
112
+
113
+ pageCount++;
114
+
115
+ // Add small delay to prevent overwhelming the database
116
+ if (hasMoreResults && pageCount % 10 === 0) {
117
+ await new Promise(resolve => setTimeout(resolve, 10));
118
+ }
119
+ }
120
+
121
+ return bookmark;
122
+ }
123
+ ```
124
+
125
+ ### Key Design Features
126
+
127
+ #### 1. **Continuous Pagination**
128
+ - Loops through all available pages until no more results
129
+ - Uses bookmark progression to detect completion
130
+ - Includes safety limits to prevent infinite loops
131
+
132
+ #### 2. **Stream Integrity**
133
+ - Maintains single persistent connection throughout
134
+ - Delivers results incrementally as pages are fetched
135
+ - No interruption to client connection
136
+
137
+ #### 3. **Performance Safeguards**
138
+ - Maximum page limit (1000 pages = 100,000 results max)
139
+ - Periodic delays to prevent database overload
140
+ - Early termination on duplicate bookmarks
141
+
142
+ #### 4. **Backward Compatibility**
143
+ - Preserves existing API contract
144
+ - Maintains same response format
145
+ - No changes required to client implementations
146
+
147
+ ### Alternative Approaches Considered
148
+
149
+ #### Option A: Async Background Fetching
150
+ **Rejected**: Would complicate error handling and resource cleanup
151
+
152
+ #### Option B: Configurable Page Limits
153
+ **Rejected**: Adds complexity without solving the core issue
154
+
155
+ #### Option C: Separate Historical vs Real-time Endpoints
156
+ **Rejected**: Breaks the unified streaming model
157
+
158
+ ### Implementation Considerations
159
+
160
+ #### 1. **Database Impact**
161
+ - Multiple sequential queries instead of single query
162
+ - Potential increased load on connection pool
163
+ - Mitigation: Rate limiting and connection reuse
164
+
165
+ #### 2. **Memory Management**
166
+ - Stream queue may grow during rapid pagination
167
+ - Mitigation: Client processing should keep pace with delivery
168
+ - Consider: Optional memory pressure monitoring
169
+
170
+ #### 3. **Timeout Handling**
171
+ - 5-minute stream timeout may be insufficient for large datasets
172
+ - Consider: Dynamic timeout based on data volume
173
+ - Alternative: Configurable timeout per feed
174
+
175
+ #### 4. **Error Recovery**
176
+ - Database errors during pagination should not break stream
177
+ - Implement: Retry logic for individual page failures
178
+ - Fallback: Continue from last successful bookmark
179
+
180
+ ### Testing Strategy
181
+
182
+ #### 1. **Unit Tests**
183
+ - Test pagination logic with various result set sizes
184
+ - Verify bookmark progression and termination conditions
185
+ - Test error scenarios and recovery
186
+
187
+ #### 2. **Integration Tests**
188
+ - Large dataset streaming (1000+ results)
189
+ - Concurrent stream handling
190
+ - Database connection pool behavior under load
191
+
192
+ #### 3. **Performance Tests**
193
+ - Memory usage during large result streaming
194
+ - Database query performance impact
195
+ - Client processing capability validation
196
+
197
+ ### Deployment Considerations
198
+
199
+ #### 1. **Gradual Rollout**
200
+ - Feature flag to enable/disable full pagination
201
+ - Monitor database performance impact
202
+ - Rollback capability if issues arise
203
+
204
+ #### 2. **Monitoring**
205
+ - Track average pages per stream
206
+ - Monitor database connection pool utilization
207
+ - Alert on streams exceeding safety limits
208
+
209
+ #### 3. **Configuration**
210
+ - Make page limit configurable
211
+ - Allow timeout adjustment per deployment
212
+ - Enable/disable rate limiting
213
+
214
+ ## Expected Outcomes
215
+
216
+ ### Benefits
217
+ 1. **Complete Data Delivery**: Clients receive all historical results
218
+ 2. **Unified Experience**: Single endpoint for both historical and real-time data
219
+ 3. **Streaming Efficiency**: Maintains connection throughout data delivery
220
+ 4. **Backward Compatibility**: No breaking changes to existing clients
221
+
222
+ ### Trade-offs
223
+ 1. **Increased Database Load**: More queries per stream initialization
224
+ 2. **Longer Initial Response Time**: Complete data delivery takes longer
225
+ 3. **Memory Usage**: Larger stream queues during pagination
226
+ 4. **Complexity**: More sophisticated error handling required
227
+
228
+ ## Conclusion
229
+
230
+ This solution addresses the core limitation while maintaining the streaming architecture's benefits. The continuous pagination approach ensures complete data delivery without breaking the real-time streaming model, providing a robust foundation for applications requiring both historical completeness and live updates.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Data management for web and mobile applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "express": "^4.21.2",
51
- "jinaga": "^6.7.0",
51
+ "jinaga": "^6.7.9",
52
52
  "pg": "^8.13.1"
53
53
  }
54
54
  }
@@ -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,56 +544,244 @@ 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
- bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream);
487
- const inverses = invertSpecification(feedDefinition.feed);
488
- const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(
489
- inverse.inverseSpecification,
490
- async (results) => {
491
- // Filter out results that do not match the given.
492
- const matchingResults = results.filter(pr =>
493
- givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
494
- if (matchingResults.length != 0) {
495
- bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
574
+
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
+ }
496
638
  }
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
+ }
654
+ }
655
+
656
+ private async streamAllInitialResults(
657
+ userIdentity: UserIdentity | null,
658
+ feedDefinition: FeedObject,
659
+ start: FactReference[],
660
+ initialBookmark: string,
661
+ stream: Stream<FeedResponse>
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
+
666
+ let bookmark = initialBookmark;
667
+ let hasMoreResults = true;
668
+ let pageCount = 0;
669
+ let totalReferences = 0;
670
+ const maxPages = 1000; // Safety limit to prevent infinite loops
671
+ const startTime = Date.now();
672
+
673
+ while (hasMoreResults && pageCount < maxPages) {
674
+ const pageStart = Date.now();
675
+ console.log(`[InitialResults:${streamId}] Fetching page ${pageCount + 1} - Bookmark: ${bookmark || 'empty'}`);
676
+
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`);
681
+
682
+ // Check if we got results
683
+ if (results.tuples.length === 0) {
684
+ console.log(`[InitialResults:${streamId}] No more results - ending pagination`);
685
+ hasMoreResults = false;
686
+ break;
497
687
  }
498
- ));
499
- stream.done(() => {
500
- for (const listener of listeners) {
501
- this.factManager.removeSpecificationListener(listener);
688
+
689
+ // Process and send results
690
+ const processStart = Date.now();
691
+ const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
692
+ self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
693
+ );
694
+ const processDuration = Date.now() - processStart;
695
+
696
+ console.log(`[InitialResults:${streamId}] Page ${pageCount + 1} processed - References: ${references.length}, Process duration: ${processDuration}ms`);
697
+
698
+ if (references.length > 0) {
699
+ const response: FeedResponse = {
700
+ references,
701
+ bookmark: results.bookmark
702
+ };
703
+
704
+ const feedStart = Date.now();
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`);
502
710
  }
503
- });
504
- return stream;
711
+
712
+ // Update bookmark for next iteration
713
+ const newBookmark = results.bookmark;
714
+ if (newBookmark === bookmark) {
715
+ console.log(`[InitialResults:${streamId}] Bookmark unchanged - ending pagination`);
716
+ hasMoreResults = false;
717
+ } else {
718
+ bookmark = newBookmark;
719
+ }
720
+
721
+ // Check if we got fewer results than the page size (indicates end)
722
+ if (results.tuples.length < 100) {
723
+ console.log(`[InitialResults:${streamId}] Partial page received (${results.tuples.length} < 100) - ending pagination`);
724
+ hasMoreResults = false;
725
+ }
726
+
727
+ pageCount++;
728
+
729
+ // Add small delay to prevent overwhelming the database
730
+ if (hasMoreResults && pageCount % 10 === 0) {
731
+ console.log(`[InitialResults:${streamId}] Adding delay after ${pageCount} pages`);
732
+ await new Promise(resolve => setTimeout(resolve, 10));
733
+ }
734
+ }
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
+
743
+ return bookmark;
505
744
  }
506
745
 
507
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();
508
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
+
509
758
  // Return distinct fact references from all the tuples.
510
- 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
511
762
  );
763
+ const processDuration = Date.now() - processStart;
764
+
765
+ console.log(`[FeedResponse:${responseId}] References processed - Count: ${references.length}, Duration: ${processDuration}ms`);
766
+
512
767
  if (!skipIfEmpty || references.length > 0) {
513
768
  const response: FeedResponse = {
514
769
  references,
515
770
  bookmark: results.bookmark
516
771
  };
772
+
773
+ const streamStart = Date.now();
517
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`);
518
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
+
519
785
  return results.bookmark;
520
786
  }
521
787