jinaga-server 3.5.1 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/authorization/authorization-keystore.js +2 -2
  2. package/dist/authorization/authorization-keystore.js.map +1 -1
  3. package/dist/http/csv-metadata.d.ts +24 -0
  4. package/dist/http/csv-metadata.js +3 -0
  5. package/dist/http/csv-metadata.js.map +1 -0
  6. package/dist/http/csv-validator.d.ts +23 -0
  7. package/dist/http/csv-validator.js +83 -0
  8. package/dist/http/csv-validator.js.map +1 -0
  9. package/dist/http/output-formatters.d.ts +28 -0
  10. package/dist/http/output-formatters.js +197 -0
  11. package/dist/http/output-formatters.js.map +1 -0
  12. package/dist/http/result-stream.d.ts +35 -0
  13. package/dist/http/result-stream.js +78 -0
  14. package/dist/http/result-stream.js.map +1 -0
  15. package/dist/http/router.d.ts +7 -1
  16. package/dist/http/router.js +276 -70
  17. package/dist/http/router.js.map +1 -1
  18. package/dist/http/stream.d.ts +3 -0
  19. package/dist/http/stream.js +79 -5
  20. package/dist/http/stream.js.map +1 -1
  21. package/dist/postgres/postgres-store.js +7 -5
  22. package/dist/postgres/postgres-store.js.map +1 -1
  23. package/dist/postgres/purge-sql.js +15 -2
  24. package/dist/postgres/purge-sql.js.map +1 -1
  25. package/dist/postgres/specification-result-sql.d.ts +2 -0
  26. package/dist/postgres/specification-result-sql.js +49 -5
  27. package/dist/postgres/specification-result-sql.js.map +1 -1
  28. package/dist/postgres/specification-sql.js +28 -6
  29. package/dist/postgres/specification-sql.js.map +1 -1
  30. package/docs/analysis/DIAGNOSTIC_LOGGING_README.md +201 -0
  31. package/docs/analysis/security-audit-feeds-hash-handler.md +407 -0
  32. package/docs/design/feeds-persistent-proxy-plan.md +294 -0
  33. package/integration-test/integration-test/jinaga-test/package-lock.json +6 -0
  34. package/integration-test/jinaga-test/Dockerfile +1 -6
  35. package/integration-test/jinaga-test/http-test-helpers.js +77 -0
  36. package/integration-test/jinaga-test/jinaga.test.js +9 -9
  37. package/integration-test/jinaga-test/package-lock.json +329 -32
  38. package/integration-test/jinaga-test/package.json +5 -1
  39. package/integration-test/jinaga-test/read-csv.test.js +330 -0
  40. package/integration-test/jinaga-test/read-endpoint.test.js +442 -0
  41. package/integration-test/jinaga-test/read-performance.test.js +221 -0
  42. package/integration-test/jinaga-test/read-streaming.test.js +199 -0
  43. package/integration-test/jinaga-test/specification-conditions.test.js +147 -0
  44. package/integration-test/jinaga-test/test-helpers.js +12 -0
  45. package/integration-test/jinaga-test/time-projection.test.js +235 -0
  46. package/package.json +3 -2
  47. package/src/authorization/authorization-keystore.ts +4 -4
  48. package/src/http/csv-metadata.ts +34 -0
  49. package/src/http/csv-validator.ts +90 -0
  50. package/src/http/output-formatters.ts +196 -0
  51. package/src/http/result-stream.ts +69 -0
  52. package/src/http/router.ts +344 -62
  53. package/src/http/stream.ts +97 -5
  54. package/src/postgres/postgres-store.ts +13 -6
  55. package/src/postgres/purge-sql.ts +18 -2
  56. package/src/postgres/specification-result-sql.ts +70 -8
  57. package/src/postgres/specification-sql.ts +36 -8
  58. package/test/http/csv-validatorSpec.ts +205 -0
  59. package/test/http/result-streamSpec.ts +151 -0
  60. package/test/postgres/postgresReadSpec.ts +192 -14
  61. package/test/postgres/postgresSpecificationSpec.ts +3 -3
  62. package/test/postgres/specificationConditionsSpec.ts +268 -0
  63. package/test-concurrent-streams.js +219 -0
  64. package/documentation/streaming-feeds-full-results-solution.md +0 -230
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test script to simulate multiple concurrent clients connecting to streaming feeds
5
+ * This will help validate the diagnostic logging and reproduce the push notification issues
6
+ */
7
+
8
+ const http = require('http');
9
+ const { performance } = require('perf_hooks');
10
+
11
+ // Configuration
12
+ const SERVER_HOST = 'localhost';
13
+ const SERVER_PORT = 8080;
14
+ const FEED_HASH = 'your-feed-hash-here'; // Replace with actual feed hash
15
+ const NUM_CLIENTS = 5;
16
+ const CONNECTION_DURATION = 30000; // 30 seconds
17
+ const STAGGER_DELAY = 1000; // 1 second between client connections
18
+
19
+ class StreamingClient {
20
+ constructor(clientId, feedHash) {
21
+ this.clientId = clientId;
22
+ this.feedHash = feedHash;
23
+ this.messageCount = 0;
24
+ this.connected = false;
25
+ this.startTime = null;
26
+ this.lastMessageTime = null;
27
+ }
28
+
29
+ connect() {
30
+ return new Promise((resolve, reject) => {
31
+ console.log(`[Client-${this.clientId}] Connecting to feed: ${this.feedHash}`);
32
+ this.startTime = performance.now();
33
+
34
+ const options = {
35
+ hostname: SERVER_HOST,
36
+ port: SERVER_PORT,
37
+ path: `/feeds/${this.feedHash}`,
38
+ method: 'GET',
39
+ headers: {
40
+ 'Accept': 'application/x-jinaga-feed-stream',
41
+ 'Connection': 'keep-alive',
42
+ 'Cache-Control': 'no-cache'
43
+ }
44
+ };
45
+
46
+ const req = http.request(options, (res) => {
47
+ console.log(`[Client-${this.clientId}] Connected - Status: ${res.statusCode}`);
48
+
49
+ if (res.statusCode !== 200) {
50
+ console.error(`[Client-${this.clientId}] Connection failed with status: ${res.statusCode}`);
51
+ reject(new Error(`HTTP ${res.statusCode}`));
52
+ return;
53
+ }
54
+
55
+ this.connected = true;
56
+ resolve();
57
+
58
+ res.on('data', (chunk) => {
59
+ const now = performance.now();
60
+ this.messageCount++;
61
+ this.lastMessageTime = now;
62
+
63
+ const timeSinceStart = Math.round(now - this.startTime);
64
+ const timeSinceLastMessage = this.lastMessageTime ? Math.round(now - this.lastMessageTime) : 0;
65
+
66
+ console.log(`[Client-${this.clientId}] Message #${this.messageCount} received - Time: ${timeSinceStart}ms, Gap: ${timeSinceLastMessage}ms, Size: ${chunk.length} bytes`);
67
+
68
+ try {
69
+ const data = chunk.toString().trim();
70
+ if (data) {
71
+ const parsed = JSON.parse(data);
72
+ console.log(`[Client-${this.clientId}] Parsed message - References: ${parsed.references?.length || 0}, Bookmark: ${parsed.bookmark?.substring(0, 8) || 'none'}...`);
73
+ }
74
+ } catch (error) {
75
+ console.warn(`[Client-${this.clientId}] Failed to parse message: ${error.message}`);
76
+ }
77
+ });
78
+
79
+ res.on('end', () => {
80
+ const duration = Math.round(performance.now() - this.startTime);
81
+ console.log(`[Client-${this.clientId}] Stream ended - Duration: ${duration}ms, Messages: ${this.messageCount}`);
82
+ this.connected = false;
83
+ });
84
+
85
+ res.on('error', (error) => {
86
+ console.error(`[Client-${this.clientId}] Stream error: ${error.message}`);
87
+ this.connected = false;
88
+ });
89
+ });
90
+
91
+ req.on('error', (error) => {
92
+ console.error(`[Client-${this.clientId}] Request error: ${error.message}`);
93
+ reject(error);
94
+ });
95
+
96
+ req.setTimeout(CONNECTION_DURATION + 5000, () => {
97
+ console.log(`[Client-${this.clientId}] Request timeout`);
98
+ req.destroy();
99
+ });
100
+
101
+ req.end();
102
+
103
+ // Auto-disconnect after specified duration
104
+ setTimeout(() => {
105
+ if (this.connected) {
106
+ console.log(`[Client-${this.clientId}] Auto-disconnecting after ${CONNECTION_DURATION}ms`);
107
+ req.destroy();
108
+ }
109
+ }, CONNECTION_DURATION);
110
+ });
111
+ }
112
+
113
+ getStats() {
114
+ const duration = this.startTime ? Math.round(performance.now() - this.startTime) : 0;
115
+ return {
116
+ clientId: this.clientId,
117
+ messageCount: this.messageCount,
118
+ connected: this.connected,
119
+ duration: duration
120
+ };
121
+ }
122
+ }
123
+
124
+ async function runConcurrentTest() {
125
+ console.log(`Starting concurrent streaming test with ${NUM_CLIENTS} clients`);
126
+ console.log(`Server: ${SERVER_HOST}:${SERVER_PORT}`);
127
+ console.log(`Feed Hash: ${FEED_HASH}`);
128
+ console.log(`Connection Duration: ${CONNECTION_DURATION}ms`);
129
+ console.log(`Stagger Delay: ${STAGGER_DELAY}ms`);
130
+ console.log('---');
131
+
132
+ const clients = [];
133
+ const connectionPromises = [];
134
+
135
+ // Create and connect clients with staggered timing
136
+ for (let i = 1; i <= NUM_CLIENTS; i++) {
137
+ const client = new StreamingClient(i, FEED_HASH);
138
+ clients.push(client);
139
+
140
+ // Stagger the connections
141
+ setTimeout(async () => {
142
+ try {
143
+ await client.connect();
144
+ } catch (error) {
145
+ console.error(`[Client-${i}] Connection failed: ${error.message}`);
146
+ }
147
+ }, (i - 1) * STAGGER_DELAY);
148
+ }
149
+
150
+ // Monitor client statistics
151
+ const statsInterval = setInterval(() => {
152
+ console.log('\n=== CLIENT STATISTICS ===');
153
+ clients.forEach(client => {
154
+ const stats = client.getStats();
155
+ console.log(`Client-${stats.clientId}: Messages=${stats.messageCount}, Connected=${stats.connected}, Duration=${stats.duration}ms`);
156
+ });
157
+ console.log('========================\n');
158
+ }, 10000); // Every 10 seconds
159
+
160
+ // Wait for test completion
161
+ setTimeout(() => {
162
+ clearInterval(statsInterval);
163
+
164
+ console.log('\n=== FINAL STATISTICS ===');
165
+ let totalMessages = 0;
166
+ let connectedClients = 0;
167
+
168
+ clients.forEach(client => {
169
+ const stats = client.getStats();
170
+ totalMessages += stats.messageCount;
171
+ if (stats.connected) connectedClients++;
172
+ console.log(`Client-${stats.clientId}: Messages=${stats.messageCount}, Connected=${stats.connected}, Duration=${stats.duration}ms`);
173
+ });
174
+
175
+ console.log(`\nSummary:`);
176
+ console.log(`- Total Messages Received: ${totalMessages}`);
177
+ console.log(`- Still Connected: ${connectedClients}/${NUM_CLIENTS}`);
178
+ console.log(`- Average Messages per Client: ${(totalMessages / NUM_CLIENTS).toFixed(1)}`);
179
+
180
+ // Check for potential issues
181
+ const messageCounts = clients.map(c => c.getStats().messageCount);
182
+ const minMessages = Math.min(...messageCounts);
183
+ const maxMessages = Math.max(...messageCounts);
184
+ const variance = maxMessages - minMessages;
185
+
186
+ if (variance > 5) {
187
+ console.log(`\n⚠️ POTENTIAL ISSUE DETECTED:`);
188
+ console.log(` Message count variance: ${variance} (min: ${minMessages}, max: ${maxMessages})`);
189
+ console.log(` This suggests some clients may not be receiving all notifications`);
190
+ } else {
191
+ console.log(`\n✅ Message distribution appears consistent`);
192
+ }
193
+
194
+ console.log('\nTest completed. Check server logs for detailed diagnostic information.');
195
+ process.exit(0);
196
+ }, CONNECTION_DURATION + (NUM_CLIENTS * STAGGER_DELAY) + 5000);
197
+ }
198
+
199
+ // Handle command line arguments
200
+ if (process.argv.length > 2) {
201
+ const feedHash = process.argv[2];
202
+ if (feedHash && feedHash !== 'your-feed-hash-here') {
203
+ FEED_HASH = feedHash;
204
+ console.log(`Using feed hash from command line: ${FEED_HASH}`);
205
+ }
206
+ }
207
+
208
+ if (FEED_HASH === 'your-feed-hash-here') {
209
+ console.error('Please provide a valid feed hash as a command line argument:');
210
+ console.error('node test-concurrent-streams.js <feed-hash>');
211
+ console.error('\nOr edit the FEED_HASH constant in this script.');
212
+ process.exit(1);
213
+ }
214
+
215
+ // Start the test
216
+ runConcurrentTest().catch(error => {
217
+ console.error('Test failed:', error);
218
+ process.exit(1);
219
+ });
@@ -1,230 +0,0 @@
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.