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.
@@ -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
  }
@@ -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
+ });