@rindle/remote 0.4.4 → 0.6.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.
- package/dist/affinity.d.ts +49 -0
- package/dist/affinity.d.ts.map +1 -0
- package/dist/affinity.js +95 -0
- package/dist/affinity.js.map +1 -0
- package/dist/backend.d.ts +5 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +44 -3
- package/dist/backend.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/optimistic-source.d.ts +48 -3
- package/dist/optimistic-source.d.ts.map +1 -1
- package/dist/optimistic-source.js +169 -7
- package/dist/optimistic-source.js.map +1 -1
- package/dist/protocol.d.ts +13 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/query-error.d.ts +17 -0
- package/dist/query-error.d.ts.map +1 -0
- package/dist/query-error.js +29 -0
- package/dist/query-error.js.map +1 -0
- package/dist/remote-source.d.ts +5 -0
- package/dist/remote-source.d.ts.map +1 -1
- package/dist/remote-source.js +44 -3
- package/dist/remote-source.js.map +1 -1
- package/dist/transport.d.ts +6 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +10 -1
- package/dist/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/affinity.ts +137 -0
- package/src/backend.ts +46 -3
- package/src/index.ts +4 -0
- package/src/optimistic-source.ts +184 -7
- package/src/protocol.ts +17 -1
- package/src/query-error.ts +42 -0
- package/src/remote-source.ts +46 -3
- package/src/transport.ts +11 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The base ws subprotocol the browser offers alongside its `aff.*` ticket; the fleet follower
|
|
2
|
+
* echoes it on the 101 (a strict browser closes a socket whose selected subprotocol it never
|
|
3
|
+
* offered, so the base is always offered in affinity mode). Inlined — NOT imported from
|
|
4
|
+
* `@rindle/affinity`'s `WS_SUBPROTOCOL` — to keep that crate's `node:crypto` out of the browser
|
|
5
|
+
* bundle (the same duplicate-to-stay-bundle-clean discipline the lease wire types use). Keep the
|
|
6
|
+
* two spellings in lock-step. */
|
|
7
|
+
export declare const WS_SUBPROTOCOL = "rindle.v1";
|
|
8
|
+
/** A mutable holder for the current opaque affinity ticket, shared by the transport, the source,
|
|
9
|
+
* and the lease POST (see the module header). */
|
|
10
|
+
export interface AffinityTicketStore {
|
|
11
|
+
/** The ticket to offer on the next ws handshake, or undefined — ticketless, so the edge
|
|
12
|
+
* anycasts + mints a fresh one. A persisted/previous-connection ticket may be returned here
|
|
13
|
+
* even while it is NOT yet safe for a lease; see {@link leaseTicket}. */
|
|
14
|
+
get(): string | undefined;
|
|
15
|
+
/** Record a freshly minted/refreshed ticket (from a connection's `{t:"affinity"}` frame). */
|
|
16
|
+
set(ticket: string): void;
|
|
17
|
+
/** Drop the ticket so the next connect goes ticketless (the pinned follower is gone, §8). */
|
|
18
|
+
clear(): void;
|
|
19
|
+
/** Mark the held ticket handshake-only until this connection emits a fresh mint frame. Called
|
|
20
|
+
* before the first connection and every reconnect, after the held ticket was already selected
|
|
21
|
+
* for the ws subprotocol offer. This prevents a restored/rotated/expired persisted ticket from
|
|
22
|
+
* racing an HTTP lease onto an independently-anycast follower. */
|
|
23
|
+
connectionPending(): void;
|
|
24
|
+
/** Resolve with a ticket minted/refreshed on the CURRENT connection, or the next one
|
|
25
|
+
* {@link set}. A persisted ticket deliberately does not resolve this wait. */
|
|
26
|
+
waitForTicket(): Promise<string>;
|
|
27
|
+
/** Obtain the current connection's ticket for an HTTP lease. The first missing-ticket wait is
|
|
28
|
+
* bounded; its timeout removes the waiter and latches ticketless fallback, so later leases
|
|
29
|
+
* return immediately until a mint frame arrives. Concurrent callers share the one timer.
|
|
30
|
+
* `timedOut` is true only for that transition, allowing one warning per fallback episode. */
|
|
31
|
+
leaseTicket(timeoutMs: number): Promise<{
|
|
32
|
+
ticket?: string;
|
|
33
|
+
timedOut: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
/** Where a store persists the ticket across reloads. The browser backs this with sessionStorage
|
|
37
|
+
* (per-TAB, so two tabs can pin two regions — design §13); tests pass none (pure in-memory). */
|
|
38
|
+
export interface TicketPersistence {
|
|
39
|
+
load(): string | undefined;
|
|
40
|
+
save(ticket: string): void;
|
|
41
|
+
clear(): void;
|
|
42
|
+
}
|
|
43
|
+
/** Build an {@link AffinityTicketStore}, optionally persisted. Pure/in-memory when `persist` is
|
|
44
|
+
* omitted. */
|
|
45
|
+
export declare function createAffinityTicketStore(persist?: TicketPersistence): AffinityTicketStore;
|
|
46
|
+
/** The subprotocol list to offer for one connection: the base protocol always, plus the held ticket
|
|
47
|
+
* (already an `aff.<…>` token) when one exists. */
|
|
48
|
+
export declare function offerSubprotocols(store: AffinityTicketStore): string[];
|
|
49
|
+
//# sourceMappingURL=affinity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affinity.d.ts","sourceRoot":"","sources":["../src/affinity.ts"],"names":[],"mappings":"AAYA;;;;;kCAKkC;AAClC,eAAO,MAAM,cAAc,cAAc,CAAC;AAE1C;kDACkD;AAClD,MAAM,WAAW,mBAAmB;IAClC;;8EAE0E;IAC1E,GAAG,IAAI,MAAM,GAAG,SAAS,CAAC;IAC1B,6FAA6F;IAC7F,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6FAA6F;IAC7F,KAAK,IAAI,IAAI,CAAC;IACd;;;uEAGmE;IACnE,iBAAiB,IAAI,IAAI,CAAC;IAC1B;mFAC+E;IAC/E,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC;;;kGAG8F;IAC9F,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACjF;AAED;iGACiG;AACjG,MAAM,WAAW,iBAAiB;IAChC,IAAI,IAAI,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,IAAI,IAAI,CAAC;CACf;AAED;eACe;AACf,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,mBAAmB,CAyE1F;AAED;oDACoD;AACpD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAGtE"}
|
package/dist/affinity.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Browser-side follower-affinity ticket transport (FOLLOWER-AFFINITY-DESIGN.md §3, §5). The ticket
|
|
2
|
+
// is OPAQUE here — the browser never mints or verifies it (placement, not authorization; §3, §9),
|
|
3
|
+
// so this module carries no crypto. It is the thin seam three pieces share:
|
|
4
|
+
//
|
|
5
|
+
// - {@link WsTransport} offers the held ticket as a ws subprotocol at each connect (so the Fly
|
|
6
|
+
// edge `fly-replay`s to the pinned follower);
|
|
7
|
+
// - {@link RemoteOptimisticSource} writes the follower's minted ticket from the `{t:"affinity"}`
|
|
8
|
+
// frame, and CLEARS it on a sustained outage (the pinned follower is gone → the next reconnect
|
|
9
|
+
// anycasts to a live one and re-pins, §8);
|
|
10
|
+
// - the lease POST forwards the held ticket as `affinity`, sequenced AFTER a fresh connect's mint
|
|
11
|
+
// frame ({@link AffinityTicketStore.waitForTicket}) so both legs land on the same follower (§4.1).
|
|
12
|
+
/** The base ws subprotocol the browser offers alongside its `aff.*` ticket; the fleet follower
|
|
13
|
+
* echoes it on the 101 (a strict browser closes a socket whose selected subprotocol it never
|
|
14
|
+
* offered, so the base is always offered in affinity mode). Inlined — NOT imported from
|
|
15
|
+
* `@rindle/affinity`'s `WS_SUBPROTOCOL` — to keep that crate's `node:crypto` out of the browser
|
|
16
|
+
* bundle (the same duplicate-to-stay-bundle-clean discipline the lease wire types use). Keep the
|
|
17
|
+
* two spellings in lock-step. */
|
|
18
|
+
export const WS_SUBPROTOCOL = "rindle.v1";
|
|
19
|
+
/** Build an {@link AffinityTicketStore}, optionally persisted. Pure/in-memory when `persist` is
|
|
20
|
+
* omitted. */
|
|
21
|
+
export function createAffinityTicketStore(persist) {
|
|
22
|
+
let current = persist?.load();
|
|
23
|
+
// A loaded ticket is useful for the ws handshake, but is not lease-safe until the follower on
|
|
24
|
+
// THIS connection confirms it by minting a fresh frame. The same distinction is re-armed on
|
|
25
|
+
// every reconnect via `connectionPending`.
|
|
26
|
+
let currentConnectionFresh = false;
|
|
27
|
+
let ticketlessFallback = false;
|
|
28
|
+
let waiters = [];
|
|
29
|
+
let leaseWait;
|
|
30
|
+
const finishLeaseWait = (result) => {
|
|
31
|
+
const pending = leaseWait;
|
|
32
|
+
if (!pending)
|
|
33
|
+
return;
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
leaseWait = undefined;
|
|
36
|
+
pending.resolve(result);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
get: () => current,
|
|
40
|
+
set: (ticket) => {
|
|
41
|
+
current = ticket;
|
|
42
|
+
currentConnectionFresh = true;
|
|
43
|
+
ticketlessFallback = false;
|
|
44
|
+
persist?.save(ticket);
|
|
45
|
+
// Resolve everyone waiting for the (now-arrived) ticket, in FIFO order.
|
|
46
|
+
const pending = waiters;
|
|
47
|
+
waiters = [];
|
|
48
|
+
for (const resolve of pending)
|
|
49
|
+
resolve(ticket);
|
|
50
|
+
finishLeaseWait({ ticket, timedOut: false });
|
|
51
|
+
},
|
|
52
|
+
clear: () => {
|
|
53
|
+
// Drop the ticket but leave any waiters pending — the next connect's mint frame resolves them
|
|
54
|
+
// (a dead-follower reassignment must still be able to lease once the new ticket arrives).
|
|
55
|
+
current = undefined;
|
|
56
|
+
currentConnectionFresh = false;
|
|
57
|
+
persist?.clear();
|
|
58
|
+
},
|
|
59
|
+
connectionPending: () => {
|
|
60
|
+
currentConnectionFresh = false;
|
|
61
|
+
},
|
|
62
|
+
waitForTicket: () => currentConnectionFresh && current !== undefined
|
|
63
|
+
? Promise.resolve(current)
|
|
64
|
+
: new Promise((resolve) => waiters.push(resolve)),
|
|
65
|
+
leaseTicket: (timeoutMs) => {
|
|
66
|
+
if (currentConnectionFresh && current !== undefined) {
|
|
67
|
+
return Promise.resolve({ ticket: current, timedOut: false });
|
|
68
|
+
}
|
|
69
|
+
if (ticketlessFallback)
|
|
70
|
+
return Promise.resolve({ timedOut: false });
|
|
71
|
+
if (leaseWait)
|
|
72
|
+
return leaseWait.promise;
|
|
73
|
+
let resolve;
|
|
74
|
+
const promise = new Promise((r) => {
|
|
75
|
+
resolve = r;
|
|
76
|
+
});
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
// Clear the shared waiter before resolving callers. A later mint starts a new, fresh
|
|
79
|
+
// episode; no abandoned resolver accumulates per lease while affinity is off.
|
|
80
|
+
leaseWait = undefined;
|
|
81
|
+
ticketlessFallback = true;
|
|
82
|
+
resolve({ timedOut: true });
|
|
83
|
+
}, Math.max(0, timeoutMs));
|
|
84
|
+
leaseWait = { promise, resolve, timer };
|
|
85
|
+
return promise;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** The subprotocol list to offer for one connection: the base protocol always, plus the held ticket
|
|
90
|
+
* (already an `aff.<…>` token) when one exists. */
|
|
91
|
+
export function offerSubprotocols(store) {
|
|
92
|
+
const ticket = store.get();
|
|
93
|
+
return ticket ? [WS_SUBPROTOCOL, ticket] : [WS_SUBPROTOCOL];
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=affinity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affinity.js","sourceRoot":"","sources":["../src/affinity.ts"],"names":[],"mappings":"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"}
|
package/dist/backend.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export declare class RemoteBackend implements Backend {
|
|
|
25
25
|
__attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void;
|
|
26
26
|
private subscribe;
|
|
27
27
|
private onServerMsg;
|
|
28
|
+
/** Route a `queryError` by its 101 §5 classification: retryable ⇒ keep the QState (and the
|
|
29
|
+
* rows already folded downstream — 101 §6) and re-subscribe after a jittered backoff
|
|
30
|
+
* honoring `retryAfterMs`; terminal (or pre-classification servers) ⇒ drop the
|
|
31
|
+
* subscription, exactly as before (FOLLOWER-LAG-SHED §6.3). */
|
|
32
|
+
private onQueryError;
|
|
28
33
|
private openSubscriber;
|
|
29
34
|
private applyBatch;
|
|
30
35
|
private emitServerEvent;
|
package/dist/backend.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAIhI,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAIhI,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAiBhD,MAAM,WAAW,oBAAoB;IACnC,wFAAwF;IACxF,gBAAgB,CAAC,EAAE,iBAAiB,CAAC;IACrC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAClC;AAED,qBAAa,aAAc,YAAW,OAAO;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAoB;IACrD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAoB;IAClD,OAAO,CAAC,OAAO,CAAqD;IACpE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAC9D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;gBAEvC,SAAS,EAAE,SAAS,EAAE,IAAI,GAAE,oBAAyB;IAOjE,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI;IAStE,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAOnC;+FAC2F;IAC3F,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5C,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAI/D,4BAA4B,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAStE,OAAO,CAAC,SAAS;IA+BjB,OAAO,CAAC,WAAW;IAanB;;;oEAGgE;IAChE,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,cAAc;IAgBtB,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,eAAe;CAMxB;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EACjD,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,IAAI,GAAE,oBAAyB,GAC9B,KAAK,CAAC,CAAC,CAAC,CAGV"}
|
package/dist/backend.js
CHANGED
|
@@ -10,6 +10,7 @@ import { Store } from "@rindle/client";
|
|
|
10
10
|
import { ProtocolError, Subscriber } from "./protocol.js";
|
|
11
11
|
import { defaultSubscribeTarget, isThenable, subscribeMessage, } from "./subscribe.js";
|
|
12
12
|
import { WsTransport } from "./transport.js";
|
|
13
|
+
import { retryDelayMs } from "./query-error.js";
|
|
13
14
|
export class RemoteBackend {
|
|
14
15
|
transport;
|
|
15
16
|
resolveSubscribe;
|
|
@@ -28,10 +29,13 @@ export class RemoteBackend {
|
|
|
28
29
|
console.error(`[rindle-remote] flat query ${qid} has no named remote identity`);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
this.subs.set(qid, { remote, subscriber: null, epoch: 0, resubscribing: false, subscribeTicket: 0 });
|
|
32
|
+
this.subs.set(qid, { remote, subscriber: null, epoch: 0, resubscribing: false, subscribeTicket: 0, retryTimer: undefined, retryAttempt: 0 });
|
|
32
33
|
this.subscribe(qid, remote);
|
|
33
34
|
}
|
|
34
35
|
unregisterQuery(qid) {
|
|
36
|
+
const s = this.subs.get(qid);
|
|
37
|
+
if (s?.retryTimer !== undefined)
|
|
38
|
+
clearTimeout(s.retryTimer);
|
|
35
39
|
this.subs.delete(qid);
|
|
36
40
|
this.transport.send({ t: "unsubscribe", queryId: qid });
|
|
37
41
|
}
|
|
@@ -57,6 +61,12 @@ export class RemoteBackend {
|
|
|
57
61
|
const s = this.subs.get(qid);
|
|
58
62
|
if (!s)
|
|
59
63
|
return;
|
|
64
|
+
// A fresh subscribe (gap recovery, the retry timer itself) supersedes any scheduled
|
|
65
|
+
// retryable-error retry — never leave two subscribe paths racing for one query.
|
|
66
|
+
if (s.retryTimer !== undefined) {
|
|
67
|
+
clearTimeout(s.retryTimer);
|
|
68
|
+
s.retryTimer = undefined;
|
|
69
|
+
}
|
|
60
70
|
const request = { queryId: qid, remote, mode: "flat" };
|
|
61
71
|
const ticket = ++s.subscribeTicket;
|
|
62
72
|
const send = (target) => {
|
|
@@ -85,8 +95,7 @@ export class RemoteBackend {
|
|
|
85
95
|
}
|
|
86
96
|
onServerMsg(msg) {
|
|
87
97
|
if (msg.t === "queryError") {
|
|
88
|
-
this.
|
|
89
|
-
console.error(`[rindle-remote] query ${msg.queryId} subscription rejected: ${msg.message}`);
|
|
98
|
+
this.onQueryError(msg.queryId, msg);
|
|
90
99
|
return;
|
|
91
100
|
}
|
|
92
101
|
// This backend is flat-only; it ignores normalized frames (`nhello`/`nbatch`).
|
|
@@ -100,6 +109,37 @@ export class RemoteBackend {
|
|
|
100
109
|
else
|
|
101
110
|
this.applyBatch(msg.queryId, s, msg.batch);
|
|
102
111
|
}
|
|
112
|
+
/** Route a `queryError` by its 101 §5 classification: retryable ⇒ keep the QState (and the
|
|
113
|
+
* rows already folded downstream — 101 §6) and re-subscribe after a jittered backoff
|
|
114
|
+
* honoring `retryAfterMs`; terminal (or pre-classification servers) ⇒ drop the
|
|
115
|
+
* subscription, exactly as before (FOLLOWER-LAG-SHED §6.3). */
|
|
116
|
+
onQueryError(qid, err) {
|
|
117
|
+
const s = this.subs.get(qid);
|
|
118
|
+
if (!s)
|
|
119
|
+
return;
|
|
120
|
+
if (err.retryable !== true) {
|
|
121
|
+
if (s.retryTimer !== undefined)
|
|
122
|
+
clearTimeout(s.retryTimer);
|
|
123
|
+
this.subs.delete(qid);
|
|
124
|
+
console.error(`[rindle-remote] query ${qid} subscription rejected: ${err.message}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (s.retryTimer !== undefined)
|
|
128
|
+
return; // a retry is already scheduled — don't stack them
|
|
129
|
+
s.subscriber = null; // stop validating the dead epoch; recovery is a fresh seq-0 hydrate
|
|
130
|
+
s.resubscribing = true;
|
|
131
|
+
const delay = retryDelayMs(s.retryAttempt++, err.retryAfterMs);
|
|
132
|
+
console.warn(`[rindle-remote] query ${qid} ${err.code ?? "error"} (retryable): re-subscribing in ${delay}ms: ${err.message}`);
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
const cur = this.subs.get(qid);
|
|
135
|
+
if (cur !== s)
|
|
136
|
+
return;
|
|
137
|
+
s.retryTimer = undefined;
|
|
138
|
+
this.subscribe(qid, s.remote);
|
|
139
|
+
}, delay);
|
|
140
|
+
timer.unref?.();
|
|
141
|
+
s.retryTimer = timer;
|
|
142
|
+
}
|
|
103
143
|
openSubscriber(qid, s, hello) {
|
|
104
144
|
try {
|
|
105
145
|
// The Subscriber ctor validates the comparator + fingerprint and emits the `hello`
|
|
@@ -107,6 +147,7 @@ export class RemoteBackend {
|
|
|
107
147
|
s.subscriber = new Subscriber(hello, (ev) => this.emitServerEvent(qid, ev));
|
|
108
148
|
s.epoch = hello.epoch;
|
|
109
149
|
s.resubscribing = false;
|
|
150
|
+
s.retryAttempt = 0; // a successful hello resets the retryable-error backoff
|
|
110
151
|
}
|
|
111
152
|
catch (e) {
|
|
112
153
|
// A comparator/schema mismatch at hello is unrecoverable (a code-contract divergence) —
|
package/dist/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,iFAAiF;AACjF,0FAA0F;AAC1F,kGAAkG;AAClG,EAAE;AACF,6FAA6F;AAC7F,6FAA6F;AAC7F,2FAA2F;AAE3F,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE1D,OAAO,EACL,sBAAsB,EACtB,UAAU,EACV,gBAAgB,GAGjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { RemoteBackend, createRemoteStore } from "./backend.ts";
|
|
|
3
3
|
export type { RemoteBackendOptions } from "./backend.ts";
|
|
4
4
|
export { WsTransport } from "./transport.ts";
|
|
5
5
|
export type { Transport } from "./transport.ts";
|
|
6
|
+
export { WS_SUBPROTOCOL, createAffinityTicketStore, offerSubprotocols } from "./affinity.ts";
|
|
7
|
+
export type { AffinityTicketStore, TicketPersistence } from "./affinity.ts";
|
|
6
8
|
export type { MutationEnvelopeSender, RawMutationSender, SubscribeMode, SubscribeRequest, SubscribeResolver, SubscribeTarget, } from "./subscribe.ts";
|
|
7
9
|
export { COMPARATOR_VERSION, Publisher, ProtocolError, Subscriber, schemaFp } from "./protocol.ts";
|
|
8
10
|
export type { Batch, ClientMsg, Hello, ProtocolErrorKind, ServerMsg, SubscribeClientMsg } from "./protocol.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChE,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC7F,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC5E,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnG,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAG/G,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACrE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAC1F,YAAY,EAAE,6BAA6B,EAAE,MAAM,oBAAoB,CAAC;AAGxE,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAC9F,YAAY,EACV,0BAA0B,EAC1B,6BAA6B,EAC7B,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export * from "@rindle/client";
|
|
6
6
|
export { RemoteBackend, createRemoteStore } from "./backend.js";
|
|
7
7
|
export { WsTransport } from "./transport.js";
|
|
8
|
+
// Browser-side follower-affinity ticket transport (FOLLOWER-AFFINITY-DESIGN.md §3, §5).
|
|
9
|
+
export { WS_SUBPROTOCOL, createAffinityTicketStore, offerSubprotocols } from "./affinity.js";
|
|
8
10
|
export { COMPARATOR_VERSION, Publisher, ProtocolError, Subscriber, schemaFp } from "./protocol.js";
|
|
9
11
|
// The normalized subscription protocol (the path-free twin) + the client-side source.
|
|
10
12
|
export { NormalizedSubscriber, normalizedFp } from "./normalized.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,0FAA0F;AAC1F,8FAA8F;AAC9F,uEAAuE;AAEvE,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,0FAA0F;AAC1F,8FAA8F;AAC9F,uEAAuE;AAEvE,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,wFAAwF;AACxF,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAW7F,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGnG,sFAAsF;AACtF,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAG1F,0FAA0F;AAC1F,OAAO,EAAE,sBAAsB,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAO9F,yFAAyF;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { MutationEnvelope, NormalizedEvent, NormalizedTableSchema, OptimisticSource, ProgressFrame, QueryId, RemoteQuery } from "@rindle/client";
|
|
1
|
+
import type { MutationEnvelope, MutationOutcomeFrame, NormalizedEvent, NormalizedTableSchema, OptimisticSource, ProgressFrame, QueryId, RemoteQuery } from "@rindle/client";
|
|
2
|
+
import type { AffinityTicketStore } from "./affinity.ts";
|
|
2
3
|
import { type MutationEnvelopeSender, type SubscribeResolver } from "./subscribe.ts";
|
|
3
4
|
import type { Transport } from "./transport.ts";
|
|
4
5
|
/** Build a transport to a follower's public ws endpoint (READ-ROUTER-DESIGN.md §2.3). Default
|
|
@@ -22,6 +23,11 @@ export interface RemoteOptimisticSourceOptions {
|
|
|
22
23
|
resolveSubscribe?: SubscribeResolver;
|
|
23
24
|
/** Override named-mutator delivery, e.g. POST envelopes to the app API server. */
|
|
24
25
|
pushMutation?: MutationEnvelopeSender;
|
|
26
|
+
/** Follower-affinity mode (FOLLOWER-AFFINITY-DESIGN.md §3): the shared ticket store. When set, the
|
|
27
|
+
* source records the follower's minted ticket from the `{t:"affinity"}` frame and CLEARS it on a
|
|
28
|
+
* sustained outage so the next (ticketless) reconnect anycasts to a live follower and re-pins
|
|
29
|
+
* (§8). Absent ⇒ affinity off (today's behavior). */
|
|
30
|
+
affinity?: AffinityTicketStore;
|
|
25
31
|
}
|
|
26
32
|
export declare class RemoteOptimisticSource implements OptimisticSource {
|
|
27
33
|
/** The CURRENT transport (undefined in pure-lazy mode until the first lease opens one). */
|
|
@@ -33,9 +39,23 @@ export declare class RemoteOptimisticSource implements OptimisticSource {
|
|
|
33
39
|
private readonly clientID;
|
|
34
40
|
private readonly resolveSubscribe;
|
|
35
41
|
private readonly pushMutationSender?;
|
|
42
|
+
/** The affinity ticket store (undefined ⇒ affinity off). See {@link RemoteOptimisticSourceOptions.affinity}. */
|
|
43
|
+
private readonly affinityStore?;
|
|
36
44
|
private handler;
|
|
37
45
|
private progressHandler;
|
|
38
46
|
private restartHandler;
|
|
47
|
+
private outcomeHandler;
|
|
48
|
+
private resyncHandler;
|
|
49
|
+
/** Set by {@link resync} when this is a LEASE-AUTH session (some sub presented a `leaseToken`):
|
|
50
|
+
* transport pushes queue in {@link pendingPushes} until the first authenticated re-subscribe's
|
|
51
|
+
* hello re-establishes the socket's subject, then flush (see QState.authed). Never set on a
|
|
52
|
+
* token-less (embedded/rindled) session — its pushes need no subject and go straight out. */
|
|
53
|
+
private awaitingAuthedHello;
|
|
54
|
+
/** Envelopes held while {@link awaitingAuthedHello} (H-v §7.5 rule 3). Every entry corresponds
|
|
55
|
+
* to a still-pending backend mutation (the re-send reconstructs from pending entries; app
|
|
56
|
+
* invokes in the window are pending by definition), so a superseding resync may CLEAR this —
|
|
57
|
+
* its own re-send regenerates whatever still matters. */
|
|
58
|
+
private pendingPushes;
|
|
39
59
|
private readonly subs;
|
|
40
60
|
/** Queries whose subscribe is waiting for a transport to exist — endpoint-less subscribes issued
|
|
41
61
|
* in pure-lazy mode before any lease opens a transport (the lmid system query is registered by
|
|
@@ -91,13 +111,38 @@ export declare class RemoteOptimisticSource implements OptimisticSource {
|
|
|
91
111
|
pushMutation(envelope: MutationEnvelope): Promise<void>;
|
|
92
112
|
onNormalized(handler: (qid: QueryId, ev: NormalizedEvent) => void): void;
|
|
93
113
|
onProgress(handler: (frame: ProgressFrame) => void): void;
|
|
114
|
+
/** The room deopt handshake's verdict stream (H-v). Dispatched OUT-OF-BAND on arrival — see
|
|
115
|
+
* {@link onServerMsg}'s `mutationOutcome` arm for why it must never wait behind the cv buffer. */
|
|
116
|
+
onMutationOutcome(handler: (frame: MutationOutcomeFrame) => void): void;
|
|
117
|
+
/** Fired once per re-established session, SYNCHRONOUSLY inside {@link resync} — before any
|
|
118
|
+
* post-reconnect frame can release (the §7.5 rule-3 window: a replayed lmid snapshot must not
|
|
119
|
+
* retire an entry whose outcome frame died with the old socket before the re-send captured
|
|
120
|
+
* it). The backend re-sends the domain's unconfirmed pending envelopes with their original
|
|
121
|
+
* mids; on a lease-auth session their DELIVERY is deferred until the first token hello
|
|
122
|
+
* re-authenticates the socket ({@link pendingPushes}). */
|
|
123
|
+
onResync(handler: () => void): void;
|
|
94
124
|
private subscribe;
|
|
95
125
|
private onServerMsg;
|
|
96
|
-
/** On reconnect: re-announce identity and re-subscribe every live
|
|
97
|
-
* lease, so a restarted daemon re-materializes + re-leases on the
|
|
126
|
+
/** On reconnect: re-announce identity, fire the `onResync` re-send, and re-subscribe every live
|
|
127
|
+
* query (each re-resolves its lease, so a restarted daemon re-materializes + re-leases on the
|
|
128
|
+
* fly). The re-send fires HERE — synchronously, before any post-reconnect frame can be
|
|
129
|
+
* processed — because the §7.5 rule-3 window closes fast: the re-subscribed lmid stream's
|
|
130
|
+
* fresh snapshot may cover a mid whose outcome frame died with the OLD socket, and once the
|
|
131
|
+
* release retires that entry as an apparent success there is nothing left to re-send (the
|
|
132
|
+
* lost-deopt write would silently vanish). Firing now captures the in-flight set intact; on a
|
|
133
|
+
* lease-auth session the envelopes themselves are HELD ({@link pendingPushes}) until the first
|
|
134
|
+
* token re-subscribe's hello re-authenticates the socket, then flush in order — so the shell's
|
|
135
|
+
* subject gate never refuses them, and its re-answer (a recorded outcome for any non-applied
|
|
136
|
+
* mid) resolves even an already-retired entry via the handshake's not-found arm. */
|
|
98
137
|
private resync;
|
|
99
138
|
/** Track the daemon's boot id; a change (after the first) means it restarted — fire onRestart. */
|
|
100
139
|
private observeBootId;
|
|
140
|
+
/** Route a `queryError` by its 101 §5 classification: retryable ⇒ keep the QState (and the
|
|
141
|
+
* rows already folded downstream — 101 §6) and re-subscribe after a jittered backoff
|
|
142
|
+
* honoring `retryAfterMs`; terminal (or pre-classification servers) ⇒ drop the
|
|
143
|
+
* subscription, exactly as before. Fixes the stranded-client gap: a worker fault's or a
|
|
144
|
+
* shedding follower's error now heals end-to-end (FOLLOWER-LAG-SHED §6.3). */
|
|
145
|
+
private onQueryError;
|
|
101
146
|
private openSubscriber;
|
|
102
147
|
private applyBatch;
|
|
103
148
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optimistic-source.d.ts","sourceRoot":"","sources":["../src/optimistic-source.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,OAAO,EACP,WAAW,EACZ,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"optimistic-source.d.ts","sourceRoot":"","sources":["../src/optimistic-source.ts"],"names":[],"mappings":"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"}
|