@pylonsync/sync 0.3.131 → 0.3.133
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/index.ts +120 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -70,6 +70,28 @@ export interface ClientChange {
|
|
|
70
70
|
op_id?: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Reactive subscription spec — what the server needs to replay a
|
|
75
|
+
* subscription if the client reconnects. Cached client-side so the
|
|
76
|
+
* `ws.onopen` reconnect sweep can re-register every active sub
|
|
77
|
+
* without the React hooks having to know about reconnect lifecycle.
|
|
78
|
+
*/
|
|
79
|
+
export interface ReactiveSpec {
|
|
80
|
+
fn_name: string;
|
|
81
|
+
args: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Push message routed to a reactive subscription handler. `result`
|
|
86
|
+
* fires on initial run + every time the server's re-run produces a
|
|
87
|
+
* value whose hash differs from the last push. `error` fires when
|
|
88
|
+
* the server can't execute the handler (function not registered,
|
|
89
|
+
* reactive runtime unavailable, runtime error in user code).
|
|
90
|
+
*/
|
|
91
|
+
export type ReactiveMessage =
|
|
92
|
+
| { kind: "result"; result: unknown }
|
|
93
|
+
| { kind: "error"; code: string; message: string };
|
|
94
|
+
|
|
73
95
|
// ---------------------------------------------------------------------------
|
|
74
96
|
// Local store — in-memory replica of server state
|
|
75
97
|
// ---------------------------------------------------------------------------
|
|
@@ -721,6 +743,22 @@ export class SyncEngine {
|
|
|
721
743
|
private crdtSubscriptions: Set<string> = new Set();
|
|
722
744
|
private crdtSubscribers: Map<string, number> = new Map();
|
|
723
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Reactive query subscriptions registered via `subscribeReactive`.
|
|
748
|
+
* Two maps:
|
|
749
|
+
* - `reactiveSpecs`: sub_id → {fn_name, args} for re-registration
|
|
750
|
+
* on WS reconnect. Server-side state evaporates on disconnect.
|
|
751
|
+
* - `reactiveHandlers`: sub_id → handler that receives result + error
|
|
752
|
+
* pushes. The React hook owns these handlers and unsubscribes on
|
|
753
|
+
* unmount.
|
|
754
|
+
*
|
|
755
|
+
* Both maps are keyed by the same client-minted `sub_id` so they
|
|
756
|
+
* stay in sync. Cleared together by `unsubscribeReactive`.
|
|
757
|
+
*/
|
|
758
|
+
private reactiveSpecs: Map<string, ReactiveSpec> = new Map();
|
|
759
|
+
private reactiveHandlers: Map<string, (msg: ReactiveMessage) => void> =
|
|
760
|
+
new Map();
|
|
761
|
+
|
|
724
762
|
/**
|
|
725
763
|
* Register a binary-frame handler. Returns an unsubscribe fn that
|
|
726
764
|
* pulls the handler back out — call on hook unmount / module
|
|
@@ -1003,6 +1041,18 @@ export class SyncEngine {
|
|
|
1003
1041
|
const [entity, rowId] = key.split("\x00");
|
|
1004
1042
|
this.sendWs({ type: "crdt-subscribe", entity, rowId });
|
|
1005
1043
|
}
|
|
1044
|
+
// Re-register every reactive subscription on the fresh socket.
|
|
1045
|
+
// The server's ReactiveRegistry tears down on disconnect (via
|
|
1046
|
+
// `disconnect_client`) so without this resync the handlers
|
|
1047
|
+
// would silently stop receiving result pushes.
|
|
1048
|
+
for (const [sub_id, spec] of this.reactiveSpecs) {
|
|
1049
|
+
this.sendWs({
|
|
1050
|
+
type: "reactive-subscribe",
|
|
1051
|
+
sub_id,
|
|
1052
|
+
fn_name: spec.fn_name,
|
|
1053
|
+
args: spec.args,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1006
1056
|
// Pull-on-open catches every event broadcast in the gap between
|
|
1007
1057
|
// the prior `pull()` returning and this socket actually opening.
|
|
1008
1058
|
// The WS has no replay-on-connect (it's just a fanout), so events
|
|
@@ -1062,6 +1112,29 @@ export class SyncEngine {
|
|
|
1062
1112
|
this.store.notify();
|
|
1063
1113
|
return;
|
|
1064
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
// Reactive query push: the server-side ReactiveRegistry re-ran
|
|
1117
|
+
// a subscribed handler and the result hash changed. Route to
|
|
1118
|
+
// the handler registered by `subscribeReactive` so the React
|
|
1119
|
+
// hook re-renders.
|
|
1120
|
+
if (msg.type === "reactive-result" && typeof msg.sub_id === "string") {
|
|
1121
|
+
const handler = this.reactiveHandlers.get(msg.sub_id);
|
|
1122
|
+
if (handler) {
|
|
1123
|
+
handler({ kind: "result", result: msg.result });
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (msg.type === "reactive-error" && typeof msg.sub_id === "string") {
|
|
1128
|
+
const handler = this.reactiveHandlers.get(msg.sub_id);
|
|
1129
|
+
if (handler) {
|
|
1130
|
+
handler({
|
|
1131
|
+
kind: "error",
|
|
1132
|
+
code: typeof msg.code === "string" ? msg.code : "REACTIVE_ERROR",
|
|
1133
|
+
message: typeof msg.message === "string" ? msg.message : "",
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1065
1138
|
} catch {
|
|
1066
1139
|
// Ignore malformed messages.
|
|
1067
1140
|
}
|
|
@@ -1864,6 +1937,53 @@ export class SyncEngine {
|
|
|
1864
1937
|
}
|
|
1865
1938
|
}
|
|
1866
1939
|
|
|
1940
|
+
// -----------------------------------------------------------------------
|
|
1941
|
+
// Reactive query subscriptions
|
|
1942
|
+
//
|
|
1943
|
+
// Convex-shaped: the client mounts `useReactiveQuery(fnName, args)`,
|
|
1944
|
+
// the server runs the handler with dep tracking, registers the sub,
|
|
1945
|
+
// and pushes the initial result. Whenever a future mutation touches
|
|
1946
|
+
// anything in the dep set, the server re-runs the handler and pushes
|
|
1947
|
+
// the new result. The handler always re-runs under the subscriber's
|
|
1948
|
+
// original auth context — not the mutating user's — so policy gates
|
|
1949
|
+
// applied at first run apply on every re-run.
|
|
1950
|
+
// -----------------------------------------------------------------------
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Register a reactive query subscription. The caller-minted `sub_id`
|
|
1954
|
+
* is used by the React hook to dispatch result/error pushes to the
|
|
1955
|
+
* right component. Returns nothing — push handling is async via the
|
|
1956
|
+
* registered handler.
|
|
1957
|
+
*
|
|
1958
|
+
* Idempotent: re-calling with the same `sub_id` replaces the prior
|
|
1959
|
+
* handler + spec. Useful when args change and the hook re-registers.
|
|
1960
|
+
*
|
|
1961
|
+
* The actual subscribe message goes over the WS — works only when
|
|
1962
|
+
* the socket is open. When called before the WS opens (initial
|
|
1963
|
+
* mount during start()), the spec is still recorded and gets sent
|
|
1964
|
+
* on `ws.onopen`'s re-registration sweep.
|
|
1965
|
+
*/
|
|
1966
|
+
subscribeReactive(
|
|
1967
|
+
sub_id: string,
|
|
1968
|
+
fn_name: string,
|
|
1969
|
+
args: unknown,
|
|
1970
|
+
handler: (msg: ReactiveMessage) => void,
|
|
1971
|
+
): void {
|
|
1972
|
+
this.reactiveSpecs.set(sub_id, { fn_name, args });
|
|
1973
|
+
this.reactiveHandlers.set(sub_id, handler);
|
|
1974
|
+
this.sendWs({ type: "reactive-subscribe", sub_id, fn_name, args });
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/** Tear down a reactive subscription. Sends the unsubscribe to the
|
|
1978
|
+
* server and clears local state. No-op for unknown sub_ids — React
|
|
1979
|
+
* StrictMode double-unmount won't error. */
|
|
1980
|
+
unsubscribeReactive(sub_id: string): void {
|
|
1981
|
+
if (!this.reactiveSpecs.has(sub_id)) return;
|
|
1982
|
+
this.reactiveSpecs.delete(sub_id);
|
|
1983
|
+
this.reactiveHandlers.delete(sub_id);
|
|
1984
|
+
this.sendWs({ type: "reactive-unsubscribe", sub_id });
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1867
1987
|
private sendWs(msg: unknown): void {
|
|
1868
1988
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1869
1989
|
this.ws.send(JSON.stringify(msg));
|