@syncular/client 0.15.12 → 0.15.14
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/README.md +66 -2
- package/dist/availability.d.ts +17 -0
- package/dist/availability.js +48 -0
- package/dist/client.js +3 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/invalidation.d.ts +1 -0
- package/dist/multi-tab.d.ts +24 -1
- package/dist/multi-tab.js +113 -4
- package/dist/reactive-store.d.ts +10 -1
- package/dist/reactive-store.js +107 -5
- package/dist/state.d.ts +3 -2
- package/dist/state.js +23 -13
- package/dist/worker-host.d.ts +33 -1
- package/dist/worker-host.js +107 -7
- package/package.json +3 -3
- package/src/availability.ts +70 -0
- package/src/client.ts +7 -4
- package/src/index.ts +1 -0
- package/src/invalidation.ts +1 -0
- package/src/multi-tab.ts +158 -0
- package/src/reactive-store.ts +138 -7
- package/src/state.ts +25 -13
- package/src/worker-host.ts +148 -6
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SyncStatusSnapshot } from './invalidation';
|
|
2
|
+
import type { LeadershipState } from './multi-tab';
|
|
3
|
+
|
|
4
|
+
export type SyncAvailability =
|
|
5
|
+
| { readonly state: 'ready' }
|
|
6
|
+
| { readonly state: 'migrating'; readonly currentSchemaVersion: number }
|
|
7
|
+
| {
|
|
8
|
+
readonly state: 'blocked';
|
|
9
|
+
readonly reason:
|
|
10
|
+
| 'client-upgrade-required'
|
|
11
|
+
| 'server-behind'
|
|
12
|
+
| 'incompatible-schema'
|
|
13
|
+
| 'leader-unreachable';
|
|
14
|
+
readonly currentSchemaVersion: number;
|
|
15
|
+
readonly requiredSchemaVersion?: number;
|
|
16
|
+
readonly latestServerSchemaVersion?: number;
|
|
17
|
+
readonly retryable: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Classify schema and browser-ownership state without parsing diagnostics. */
|
|
21
|
+
export function classifySyncAvailability(
|
|
22
|
+
status: SyncStatusSnapshot,
|
|
23
|
+
leadership?: LeadershipState,
|
|
24
|
+
): SyncAvailability {
|
|
25
|
+
const currentSchemaVersion = status.currentSchemaVersion;
|
|
26
|
+
if (leadership?.state === 'blocked') {
|
|
27
|
+
return {
|
|
28
|
+
state: 'blocked',
|
|
29
|
+
reason: 'leader-unreachable',
|
|
30
|
+
currentSchemaVersion,
|
|
31
|
+
retryable: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const required = status.schemaFloor?.requiredSchemaVersion;
|
|
35
|
+
const latest = status.schemaFloor?.latestSchemaVersion;
|
|
36
|
+
if (required !== undefined && required > currentSchemaVersion) {
|
|
37
|
+
return {
|
|
38
|
+
state: 'blocked',
|
|
39
|
+
reason: 'client-upgrade-required',
|
|
40
|
+
currentSchemaVersion,
|
|
41
|
+
requiredSchemaVersion: required,
|
|
42
|
+
...(latest !== undefined ? { latestServerSchemaVersion: latest } : {}),
|
|
43
|
+
retryable: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (latest !== undefined && latest < currentSchemaVersion) {
|
|
47
|
+
return {
|
|
48
|
+
state: 'blocked',
|
|
49
|
+
reason: 'server-behind',
|
|
50
|
+
currentSchemaVersion,
|
|
51
|
+
...(required !== undefined ? { requiredSchemaVersion: required } : {}),
|
|
52
|
+
latestServerSchemaVersion: latest,
|
|
53
|
+
retryable: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (status.schemaFloor !== undefined) {
|
|
57
|
+
return {
|
|
58
|
+
state: 'blocked',
|
|
59
|
+
reason: 'incompatible-schema',
|
|
60
|
+
currentSchemaVersion,
|
|
61
|
+
...(required !== undefined ? { requiredSchemaVersion: required } : {}),
|
|
62
|
+
...(latest !== undefined ? { latestServerSchemaVersion: latest } : {}),
|
|
63
|
+
retryable: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (status.upgrading) {
|
|
67
|
+
return { state: 'migrating', currentSchemaVersion };
|
|
68
|
+
}
|
|
69
|
+
return { state: 'ready' };
|
|
70
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -597,9 +597,11 @@ export class SyncClient {
|
|
|
597
597
|
this.#detectAndResetSchema();
|
|
598
598
|
// A registration remains app intent only while its table exists in the
|
|
599
599
|
// running schema. Removed-table registrations would poison every pull.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
600
|
+
const subscriptions = pruneUnknownSubscriptions(
|
|
601
|
+
this.#db,
|
|
602
|
+
loadSubscriptions(this.#db),
|
|
603
|
+
new Set(this.#schema.tables.keys()),
|
|
604
|
+
);
|
|
603
605
|
this.#started = true;
|
|
604
606
|
// A persisted active subscription needs one catch-up round on every open:
|
|
605
607
|
// realtime only covers changes after the socket connects, and an
|
|
@@ -610,7 +612,7 @@ export class SyncClient {
|
|
|
610
612
|
const startupWork =
|
|
611
613
|
this.#schemaFloor === undefined &&
|
|
612
614
|
(listOutbox(this.#db).length > 0 ||
|
|
613
|
-
|
|
615
|
+
subscriptions.some((sub) => sub.status === 'active'));
|
|
614
616
|
if (startupWork) {
|
|
615
617
|
this.#needsPull = true;
|
|
616
618
|
this.#config.onSyncNeeded?.('startup');
|
|
@@ -833,6 +835,7 @@ export class SyncClient {
|
|
|
833
835
|
|
|
834
836
|
#statusSnapshot(): SyncStatusSnapshot {
|
|
835
837
|
return {
|
|
838
|
+
currentSchemaVersion: this.#config.schema.version,
|
|
836
839
|
outbox: listOutbox(this.#db).length,
|
|
837
840
|
upgrading: this.#upgrading,
|
|
838
841
|
leaseState: this.#leaseState,
|
package/src/index.ts
CHANGED
package/src/invalidation.ts
CHANGED
package/src/multi-tab.ts
CHANGED
|
@@ -46,6 +46,24 @@ export const DEFAULT_FOLLOWER_CALL_TIMEOUT_MS = 10_000;
|
|
|
46
46
|
/** Max follower calls queued across a handover before we fail loudly. */
|
|
47
47
|
export const DEFAULT_FOLLOWER_QUEUE_LIMIT = 256;
|
|
48
48
|
|
|
49
|
+
export type LeadershipState =
|
|
50
|
+
| { readonly state: 'leader'; readonly clientId: string }
|
|
51
|
+
| {
|
|
52
|
+
readonly state: 'follower';
|
|
53
|
+
readonly leaderClientId: string;
|
|
54
|
+
readonly epoch: number;
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
readonly state: 'waiting';
|
|
58
|
+
readonly reason: 'handover' | 'leader-announcement';
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
readonly state: 'blocked';
|
|
62
|
+
readonly reason: 'leader-unreachable';
|
|
63
|
+
readonly code: typeof FOLLOWER_TIMEOUT_CODE;
|
|
64
|
+
readonly retryable: true;
|
|
65
|
+
};
|
|
66
|
+
|
|
49
67
|
// ---------------------------------------------------------------------------
|
|
50
68
|
// Wire messages
|
|
51
69
|
// ---------------------------------------------------------------------------
|
|
@@ -63,6 +81,11 @@ interface ByeMessage {
|
|
|
63
81
|
readonly fromId: string;
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
interface BoundMessage {
|
|
85
|
+
readonly t: 'bound';
|
|
86
|
+
readonly fromId: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
interface ReqMessage {
|
|
67
90
|
readonly t: 'req';
|
|
68
91
|
readonly epoch: number;
|
|
@@ -96,6 +119,7 @@ interface EventMessage {
|
|
|
96
119
|
export type MultiTabMessage =
|
|
97
120
|
| HelloMessage
|
|
98
121
|
| ByeMessage
|
|
122
|
+
| BoundMessage
|
|
99
123
|
| ReqMessage
|
|
100
124
|
| AnnounceMessage
|
|
101
125
|
| ResMessage
|
|
@@ -157,6 +181,9 @@ export class LeaderBridge {
|
|
|
157
181
|
args: readonly unknown[],
|
|
158
182
|
) => Promise<unknown>;
|
|
159
183
|
readonly #onMessage: (event: { data: MultiTabMessage }) => void;
|
|
184
|
+
readonly #heartbeatMs: number;
|
|
185
|
+
readonly #followers = new Set<string>();
|
|
186
|
+
#heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
160
187
|
#closed = false;
|
|
161
188
|
|
|
162
189
|
constructor(options: {
|
|
@@ -164,11 +191,15 @@ export class LeaderBridge {
|
|
|
164
191
|
epoch: number;
|
|
165
192
|
clientId: string;
|
|
166
193
|
invoke: (method: string, args: readonly unknown[]) => Promise<unknown>;
|
|
194
|
+
heartbeatMs?: number;
|
|
167
195
|
}) {
|
|
168
196
|
this.#channel = options.channel;
|
|
169
197
|
this.#epoch = options.epoch;
|
|
170
198
|
this.#clientId = options.clientId;
|
|
171
199
|
this.#invoke = options.invoke;
|
|
200
|
+
this.#heartbeatMs =
|
|
201
|
+
options.heartbeatMs ??
|
|
202
|
+
Math.max(50, Math.floor(DEFAULT_FOLLOWER_CALL_TIMEOUT_MS / 3));
|
|
172
203
|
this.#onMessage = (event) => this.#handle(event.data);
|
|
173
204
|
this.#channel.addEventListener('message', this.#onMessage);
|
|
174
205
|
this.announce();
|
|
@@ -194,9 +225,22 @@ export class LeaderBridge {
|
|
|
194
225
|
if (this.#closed) return;
|
|
195
226
|
if (message.t === 'hello') {
|
|
196
227
|
// A follower joined (or is contesting) — tell it who leads.
|
|
228
|
+
this.#trackFollower(message.fromId);
|
|
197
229
|
this.announce();
|
|
198
230
|
return;
|
|
199
231
|
}
|
|
232
|
+
if (message.t === 'bound') {
|
|
233
|
+
this.#trackFollower(message.fromId);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (message.t === 'bye') {
|
|
237
|
+
this.#followers.delete(message.fromId);
|
|
238
|
+
if (this.#followers.size === 0 && this.#heartbeat !== undefined) {
|
|
239
|
+
clearInterval(this.#heartbeat);
|
|
240
|
+
this.#heartbeat = undefined;
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
200
244
|
if (message.t !== 'req') return;
|
|
201
245
|
// Ignore requests stamped for a different (dead/older) leader; that
|
|
202
246
|
// follower will re-stamp once it sees our announce.
|
|
@@ -237,10 +281,19 @@ export class LeaderBridge {
|
|
|
237
281
|
);
|
|
238
282
|
}
|
|
239
283
|
|
|
284
|
+
#trackFollower(fromId: string): void {
|
|
285
|
+
this.#followers.add(fromId);
|
|
286
|
+
if (this.#heartbeat !== undefined) return;
|
|
287
|
+
this.#heartbeat = setInterval(() => this.announce(), this.#heartbeatMs);
|
|
288
|
+
}
|
|
289
|
+
|
|
240
290
|
close(): void {
|
|
241
291
|
if (this.#closed) return;
|
|
242
292
|
this.#closed = true;
|
|
293
|
+
if (this.#heartbeat !== undefined) clearInterval(this.#heartbeat);
|
|
294
|
+
this.#heartbeat = undefined;
|
|
243
295
|
this.#channel.removeEventListener('message', this.#onMessage);
|
|
296
|
+
this.#channel.close();
|
|
244
297
|
}
|
|
245
298
|
}
|
|
246
299
|
|
|
@@ -274,6 +327,7 @@ export class FollowerLink {
|
|
|
274
327
|
readonly #fromId: string;
|
|
275
328
|
readonly #onEvent: (event: SyncWorkerEvent) => void;
|
|
276
329
|
readonly #onLeaderChange: (clientId: string) => void;
|
|
330
|
+
readonly #onStateChange: (state: LeadershipState) => void;
|
|
277
331
|
readonly #callTimeoutMs: number;
|
|
278
332
|
readonly #queueLimit: number;
|
|
279
333
|
readonly #onMessage: (event: { data: MultiTabMessage }) => void;
|
|
@@ -287,6 +341,12 @@ export class FollowerLink {
|
|
|
287
341
|
readonly #inFlight = new Map<number, InFlight>();
|
|
288
342
|
#queue: QueuedCall[] = [];
|
|
289
343
|
#closed = false;
|
|
344
|
+
#state: LeadershipState = {
|
|
345
|
+
state: 'waiting',
|
|
346
|
+
reason: 'leader-announcement',
|
|
347
|
+
};
|
|
348
|
+
#waitingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
349
|
+
#blockedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
290
350
|
/** Resolvers waiting for the first `announce` to bind a leader. */
|
|
291
351
|
#bindWaiters: Array<() => void> = [];
|
|
292
352
|
|
|
@@ -295,6 +355,7 @@ export class FollowerLink {
|
|
|
295
355
|
fromId: string;
|
|
296
356
|
onEvent: (event: SyncWorkerEvent) => void;
|
|
297
357
|
onLeaderChange: (clientId: string) => void;
|
|
358
|
+
onStateChange?: (state: LeadershipState) => void;
|
|
298
359
|
callTimeoutMs?: number;
|
|
299
360
|
queueLimit?: number;
|
|
300
361
|
}) {
|
|
@@ -302,6 +363,7 @@ export class FollowerLink {
|
|
|
302
363
|
this.#fromId = options.fromId;
|
|
303
364
|
this.#onEvent = options.onEvent;
|
|
304
365
|
this.#onLeaderChange = options.onLeaderChange;
|
|
366
|
+
this.#onStateChange = options.onStateChange ?? (() => {});
|
|
305
367
|
this.#callTimeoutMs =
|
|
306
368
|
options.callTimeoutMs ?? DEFAULT_FOLLOWER_CALL_TIMEOUT_MS;
|
|
307
369
|
this.#queueLimit = options.queueLimit ?? DEFAULT_FOLLOWER_QUEUE_LIMIT;
|
|
@@ -309,6 +371,7 @@ export class FollowerLink {
|
|
|
309
371
|
this.#channel.addEventListener('message', this.#onMessage);
|
|
310
372
|
// Ask the current leader to announce itself.
|
|
311
373
|
this.#channel.postMessage({ t: 'hello', fromId: this.#fromId });
|
|
374
|
+
this.#armUnboundDeadline();
|
|
312
375
|
}
|
|
313
376
|
|
|
314
377
|
get epoch(): number {
|
|
@@ -323,6 +386,10 @@ export class FollowerLink {
|
|
|
323
386
|
return this.#leaderClientId;
|
|
324
387
|
}
|
|
325
388
|
|
|
389
|
+
get leadershipState(): LeadershipState {
|
|
390
|
+
return this.#state;
|
|
391
|
+
}
|
|
392
|
+
|
|
326
393
|
/** Whether a leader is currently bound (an announce has been heard). */
|
|
327
394
|
get bound(): boolean {
|
|
328
395
|
return this.#epoch >= 0;
|
|
@@ -348,10 +415,12 @@ export class FollowerLink {
|
|
|
348
415
|
return new Promise((resolve, reject) => {
|
|
349
416
|
const timer = setTimeout(() => {
|
|
350
417
|
this.#dropBindWaiter(settle);
|
|
418
|
+
this.#setBlocked();
|
|
351
419
|
reject(
|
|
352
420
|
new ClientSyncError(
|
|
353
421
|
FOLLOWER_TIMEOUT_CODE,
|
|
354
422
|
'no leader announced within the follower bind timeout',
|
|
423
|
+
true,
|
|
355
424
|
),
|
|
356
425
|
);
|
|
357
426
|
}, timeoutMs);
|
|
@@ -382,6 +451,15 @@ export class FollowerLink {
|
|
|
382
451
|
new ClientSyncError(WORKER_FAILED_CODE, 'the follower link is closed'),
|
|
383
452
|
);
|
|
384
453
|
}
|
|
454
|
+
if (this.#state.state === 'blocked') {
|
|
455
|
+
return Promise.reject(
|
|
456
|
+
new ClientSyncError(
|
|
457
|
+
FOLLOWER_TIMEOUT_CODE,
|
|
458
|
+
'the follower cannot reach the tab that owns the database',
|
|
459
|
+
true,
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
385
463
|
return new Promise((resolve, reject) => {
|
|
386
464
|
const queued: QueuedCall = {
|
|
387
465
|
method,
|
|
@@ -393,20 +471,24 @@ export class FollowerLink {
|
|
|
393
471
|
if (this.#epoch < 0) {
|
|
394
472
|
// No leader bound yet — queue with a deadline so we never hang.
|
|
395
473
|
if (this.#queue.length >= this.#queueLimit) {
|
|
474
|
+
this.#setBlocked();
|
|
396
475
|
reject(
|
|
397
476
|
new ClientSyncError(
|
|
398
477
|
FOLLOWER_TIMEOUT_CODE,
|
|
399
478
|
'follower call queue overflow while awaiting a leader',
|
|
479
|
+
true,
|
|
400
480
|
),
|
|
401
481
|
);
|
|
402
482
|
return;
|
|
403
483
|
}
|
|
404
484
|
queued.timer = setTimeout(() => {
|
|
405
485
|
this.#dropQueued(queued);
|
|
486
|
+
this.#setBlocked();
|
|
406
487
|
reject(
|
|
407
488
|
new ClientSyncError(
|
|
408
489
|
FOLLOWER_TIMEOUT_CODE,
|
|
409
490
|
'no leader answered within the follower call timeout',
|
|
491
|
+
true,
|
|
410
492
|
),
|
|
411
493
|
);
|
|
412
494
|
}, this.#callTimeoutMs);
|
|
@@ -424,10 +506,12 @@ export class FollowerLink {
|
|
|
424
506
|
reject: queued.reject,
|
|
425
507
|
timer: setTimeout(() => {
|
|
426
508
|
this.#inFlight.delete(reqId);
|
|
509
|
+
this.#setBlocked();
|
|
427
510
|
queued.reject(
|
|
428
511
|
new ClientSyncError(
|
|
429
512
|
FOLLOWER_TIMEOUT_CODE,
|
|
430
513
|
'the leader did not answer within the follower call timeout',
|
|
514
|
+
true,
|
|
431
515
|
),
|
|
432
516
|
);
|
|
433
517
|
}, this.#callTimeoutMs),
|
|
@@ -462,6 +546,15 @@ export class FollowerLink {
|
|
|
462
546
|
this.#epoch = message.epoch;
|
|
463
547
|
this.#leaderClientId = message.clientId;
|
|
464
548
|
if (changed) this.#onLeaderChange(message.clientId);
|
|
549
|
+
this.#setState({
|
|
550
|
+
state: 'follower',
|
|
551
|
+
leaderClientId: message.clientId,
|
|
552
|
+
epoch: message.epoch,
|
|
553
|
+
});
|
|
554
|
+
this.#armBoundDeadline();
|
|
555
|
+
if (changed) {
|
|
556
|
+
this.#channel.postMessage({ t: 'bound', fromId: this.#fromId });
|
|
557
|
+
}
|
|
465
558
|
this.#resolveBindWaiters();
|
|
466
559
|
this.#flushQueue();
|
|
467
560
|
return;
|
|
@@ -516,6 +609,8 @@ export class FollowerLink {
|
|
|
516
609
|
if (this.#closed) return;
|
|
517
610
|
this.#epoch = -1;
|
|
518
611
|
this.#leaderClientId = '';
|
|
612
|
+
this.#setState({ state: 'waiting', reason: 'handover' });
|
|
613
|
+
this.#armUnboundDeadline();
|
|
519
614
|
this.#channel.postMessage({
|
|
520
615
|
t: 'hello',
|
|
521
616
|
fromId: this.#fromId,
|
|
@@ -526,6 +621,7 @@ export class FollowerLink {
|
|
|
526
621
|
close(): void {
|
|
527
622
|
if (this.#closed) return;
|
|
528
623
|
this.#closed = true;
|
|
624
|
+
this.#clearReachabilityTimers();
|
|
529
625
|
this.#channel.removeEventListener('message', this.#onMessage);
|
|
530
626
|
this.#channel.postMessage({ t: 'bye', fromId: this.#fromId });
|
|
531
627
|
const closedError = new ClientSyncError(
|
|
@@ -547,4 +643,66 @@ export class FollowerLink {
|
|
|
547
643
|
this.#resolveBindWaiters();
|
|
548
644
|
this.#channel.close();
|
|
549
645
|
}
|
|
646
|
+
|
|
647
|
+
#setState(state: LeadershipState): void {
|
|
648
|
+
const previous = this.#state;
|
|
649
|
+
if (
|
|
650
|
+
previous.state === state.state &&
|
|
651
|
+
(state.state === 'blocked' ||
|
|
652
|
+
(state.state === 'waiting' &&
|
|
653
|
+
previous.state === 'waiting' &&
|
|
654
|
+
previous.reason === state.reason) ||
|
|
655
|
+
(state.state === 'follower' &&
|
|
656
|
+
previous.state === 'follower' &&
|
|
657
|
+
previous.epoch === state.epoch &&
|
|
658
|
+
previous.leaderClientId === state.leaderClientId))
|
|
659
|
+
) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.#state = state;
|
|
663
|
+
try {
|
|
664
|
+
this.#onStateChange(state);
|
|
665
|
+
} catch {
|
|
666
|
+
// A status listener must never break cross-tab coordination.
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
#setBlocked(): void {
|
|
671
|
+
this.#clearReachabilityTimers();
|
|
672
|
+
this.#setState({
|
|
673
|
+
state: 'blocked',
|
|
674
|
+
reason: 'leader-unreachable',
|
|
675
|
+
code: FOLLOWER_TIMEOUT_CODE,
|
|
676
|
+
retryable: true,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#armUnboundDeadline(): void {
|
|
681
|
+
this.#clearReachabilityTimers();
|
|
682
|
+
this.#blockedTimer = setTimeout(
|
|
683
|
+
() => this.#setBlocked(),
|
|
684
|
+
this.#callTimeoutMs,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#armBoundDeadline(): void {
|
|
689
|
+
this.#clearReachabilityTimers();
|
|
690
|
+
this.#waitingTimer = setTimeout(
|
|
691
|
+
() => {
|
|
692
|
+
this.#setState({ state: 'waiting', reason: 'leader-announcement' });
|
|
693
|
+
},
|
|
694
|
+
Math.max(1, Math.floor((this.#callTimeoutMs * 2) / 3)),
|
|
695
|
+
);
|
|
696
|
+
this.#blockedTimer = setTimeout(
|
|
697
|
+
() => this.#setBlocked(),
|
|
698
|
+
this.#callTimeoutMs,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#clearReachabilityTimers(): void {
|
|
703
|
+
if (this.#waitingTimer !== undefined) clearTimeout(this.#waitingTimer);
|
|
704
|
+
if (this.#blockedTimer !== undefined) clearTimeout(this.#blockedTimer);
|
|
705
|
+
this.#waitingTimer = undefined;
|
|
706
|
+
this.#blockedTimer = undefined;
|
|
707
|
+
}
|
|
550
708
|
}
|