replica-bridge 0.3.14 → 0.3.15
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/package.json +1 -1
- package/src/bridge/board.js +40 -5
package/package.json
CHANGED
package/src/bridge/board.js
CHANGED
|
@@ -87,8 +87,16 @@ export function boardPortFrom(env = process.env) {
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
90
94
|
|
|
91
95
|
|
|
96
|
+
/** Three missed heartbeats. */
|
|
97
|
+
const DEFAULT_STREAM_IDLE_SECONDS = 45;
|
|
98
|
+
const DEFAULT_STREAM_RETRY_SECONDS = 30;
|
|
99
|
+
|
|
92
100
|
|
|
93
101
|
|
|
94
102
|
/** One subscriber and how far through the change log it has been told. */
|
|
@@ -128,10 +136,16 @@ export class BoardReader {
|
|
|
128
136
|
/** The backend's push stream: open, unknown (try it), or not there (an older backend answers 404). */
|
|
129
137
|
stream = null;
|
|
130
138
|
streamSupported = null;
|
|
139
|
+
/** Not before this time (ms) after the backend refused a stream for a passing reason. */
|
|
140
|
+
streamRetryAt = 0;
|
|
141
|
+
streamIdleMs ;
|
|
142
|
+
streamRetryMs ;
|
|
131
143
|
|
|
132
144
|
constructor(options = {}) {
|
|
133
145
|
this.port = options.port ?? boardPortFrom();
|
|
134
146
|
this.pollMs = (options.pollSeconds ?? DEFAULT_POLL_SECONDS) * 1000;
|
|
147
|
+
this.streamIdleMs = (options.streamIdleSeconds ?? DEFAULT_STREAM_IDLE_SECONDS) * 1000;
|
|
148
|
+
this.streamRetryMs = (options.streamRetrySeconds ?? DEFAULT_STREAM_RETRY_SECONDS) * 1000;
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
/** How many phones are subscribed. */
|
|
@@ -197,6 +211,7 @@ export class BoardReader {
|
|
|
197
211
|
this.stream?.abort();
|
|
198
212
|
this.stream = null;
|
|
199
213
|
this.streamSupported = null;
|
|
214
|
+
this.streamRetryAt = 0;
|
|
200
215
|
this.end = null;
|
|
201
216
|
this.up = null;
|
|
202
217
|
this.polling = false;
|
|
@@ -237,7 +252,10 @@ export class BoardReader {
|
|
|
237
252
|
cursor = next;
|
|
238
253
|
}
|
|
239
254
|
} catch {
|
|
240
|
-
sub.admitting = false;
|
|
255
|
+
sub.admitting = false;
|
|
256
|
+
// Try again shortly. Polling would admit it on its next read, but while the backend pushes nothing polls.
|
|
257
|
+
const retry = setTimeout(() => { if (round === this.round && this.subs.has(sub)) void this.admit(sub); }, this.pollMs);
|
|
258
|
+
retry.unref();
|
|
241
259
|
return;
|
|
242
260
|
}
|
|
243
261
|
if (round !== this.round || !this.subs.has(sub)) return;
|
|
@@ -296,7 +314,7 @@ export class BoardReader {
|
|
|
296
314
|
if (!up) return;
|
|
297
315
|
if (items.length) this.deliver(items, end );
|
|
298
316
|
for (const sub of this.subs) if (sub.cursor === null) void this.admit(sub);
|
|
299
|
-
if (this.streamSupported !== false) void this.openStream(round);
|
|
317
|
+
if (this.streamSupported !== false && Date.now() >= this.streamRetryAt) void this.openStream(round);
|
|
300
318
|
}
|
|
301
319
|
|
|
302
320
|
/**
|
|
@@ -307,21 +325,36 @@ export class BoardReader {
|
|
|
307
325
|
if (this.stream || this.end === null) return;
|
|
308
326
|
const controller = new AbortController();
|
|
309
327
|
this.stream = controller;
|
|
328
|
+
// A backend that takes the connection and then says nothing must not hold the bridge: while a stream is open
|
|
329
|
+
// nothing polls, so silence would also hide the backend going away. Its heartbeat keeps this from firing.
|
|
330
|
+
let idle = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
|
|
331
|
+
idle.unref();
|
|
332
|
+
const heard = () => {
|
|
333
|
+
clearTimeout(idle);
|
|
334
|
+
idle = setTimeout(() => controller.abort(), this.streamIdleMs);
|
|
335
|
+
idle.unref();
|
|
336
|
+
};
|
|
310
337
|
let response ;
|
|
311
338
|
try {
|
|
312
339
|
response = await this.fetch(`/v1/changes/stream?after=${this.end}`, undefined, controller.signal);
|
|
313
340
|
} catch {
|
|
341
|
+
clearTimeout(idle);
|
|
314
342
|
if (this.stream === controller) this.stream = null;
|
|
315
343
|
return;
|
|
316
344
|
}
|
|
317
|
-
if (round !== this.round || this.stream !== controller) { controller.abort(); return; }
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
if (round !== this.round || this.stream !== controller) { clearTimeout(idle); controller.abort(); return; }
|
|
346
|
+
const eventStream = (response.headers.get("content-type") ?? "").startsWith("text/event-stream");
|
|
347
|
+
if (!response.ok || !eventStream || !response.body) {
|
|
348
|
+
clearTimeout(idle);
|
|
349
|
+
// Only an older backend lacks it (404, or a 200 that is not an event stream); busy (429) or starting (5xx) passes.
|
|
350
|
+
if (response.status === 404 || (response.ok && !eventStream)) this.streamSupported = false;
|
|
351
|
+
else this.streamRetryAt = Date.now() + this.streamRetryMs;
|
|
320
352
|
this.stream = null;
|
|
321
353
|
controller.abort();
|
|
322
354
|
return;
|
|
323
355
|
}
|
|
324
356
|
this.streamSupported = true;
|
|
357
|
+
heard();
|
|
325
358
|
let buffer = "", pending = [], flush = null;
|
|
326
359
|
const send = () => {
|
|
327
360
|
flush = null;
|
|
@@ -335,6 +368,7 @@ export class BoardReader {
|
|
|
335
368
|
try {
|
|
336
369
|
const decoder = new TextDecoder();
|
|
337
370
|
for await (const chunk of response.body ) {
|
|
371
|
+
heard();
|
|
338
372
|
buffer += decoder.decode(chunk, { stream: true });
|
|
339
373
|
let split ;
|
|
340
374
|
while ((split = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
@@ -351,6 +385,7 @@ export class BoardReader {
|
|
|
351
385
|
}
|
|
352
386
|
}
|
|
353
387
|
} catch { /* aborted, or the backend went away */ }
|
|
388
|
+
clearTimeout(idle);
|
|
354
389
|
if (flush) { clearTimeout(flush); send(); }
|
|
355
390
|
if (this.stream === controller) this.stream = null;
|
|
356
391
|
}
|