cry-synced-db-client 0.1.179 → 0.1.181

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/CHANGELOG.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # Versions
2
2
 
3
+ ## 0.1.181 (2026-05-14)
4
+
5
+ ### `measureEndToEndRtt` now rides `callWorker` over the WebSocket
6
+
7
+ The old HTTP echo path was a workaround for the missing WS
8
+ request/response correlation in ebus-proxy 1.x. Now that
9
+ `callWorker` (0.1.180) provides it, `measureEndToEndRtt` becomes a
10
+ 5-line wrapper:
11
+
12
+ ```ts
13
+ async measureEndToEndRtt(timeoutMs = 5000): Promise<number> {
14
+ const sentAt = Date.now();
15
+ const t0 = performance.now();
16
+ const echoed = await this.callWorker<number>("echo", sentAt, { timeoutMs });
17
+ if (echoed !== sentAt) throw payload-mismatch error;
18
+ return performance.now() - t0;
19
+ }
20
+ ```
21
+
22
+ Dropped HTTP-specific code: base URL derivation (`ws://`→`http://`),
23
+ base64 encoding (Buffer + btoa fallback), URL query-string assembly,
24
+ `AbortSignal.timeout` shim, `fetch` + `arrayBuffer` + `unpack` pipeline,
25
+ HTTP status error branch. Bundle: 356.9 KB → 356.0 KB.
26
+
27
+ **Semantic shift**: now measures the WebSocket round-trip rather than
28
+ the HTTP round-trip. For this codebase that's more representative —
29
+ real notifications arrive on the WS, not via HTTP. The old HTTP path
30
+ was characterizing a transport the app barely uses (sync uploads go to
31
+ cry-db directly, not through ebus-proxy).
32
+
33
+ Public signature is unchanged; existing callers keep working. The
34
+ diagnostic interpretation table in CLAUDE.md still applies — the
35
+ labels just refer to the WS chain instead of the HTTP chain.
36
+
37
+ Tests: 736 bun + 18 vitest, all green.
38
+
39
+ ## 0.1.180 (2026-05-14)
40
+
41
+ ### `SyncedDb.callWorker` — invoke ebus2 worker services over the WS
42
+
43
+ New API for calling any registered cry-ebus2 worker directly from the
44
+ client, riding the same WebSocket connection the notifier already uses
45
+ for pub/sub. Built on ebus-proxy 2.0's WS `request` / `response`
46
+ correlation frames, so there's no extra TCP/TLS handshake and no HTTP
47
+ overhead — the worker reply arrives on the open socket.
48
+
49
+ ```typescript
50
+ // echo round-trip (any msgpack-serializable value)
51
+ const echoed = await syncedDb.callWorker<string>("echo", "hello");
52
+
53
+ // pass an object payload, type the reply
54
+ const result = await syncedDb.callWorker<{ ok: boolean; jobId: string }>(
55
+ "billing",
56
+ { customerId, amount: 1990 },
57
+ );
58
+
59
+ // per-call timeout (default 5000 ms) + caller AbortSignal
60
+ const ctrl = new AbortController();
61
+ setTimeout(() => ctrl.abort(), 200);
62
+ const fast = await syncedDb.callWorker("priority-queue", null, {
63
+ timeoutMs: 200,
64
+ signal: ctrl.signal,
65
+ });
66
+ ```
67
+
68
+ API contract:
69
+
70
+ | Argument | Type | Notes |
71
+ |---|---|---|
72
+ | `service` | `string` | Worker service name registered on the broker. |
73
+ | `payload` | `unknown?` | Anything `msgpack+structuredClone` can serialize. BSON `ObjectId` / `Timestamp` preprocessed automatically. Omit (or pass `undefined`) to call without a payload. |
74
+ | `options.timeoutMs` | `number?` | Per-call timeout; also forwarded to the proxy. Default `5000`. |
75
+ | `options.signal` | `AbortSignal?` | External cancellation. |
76
+ | Returns | `Promise<T>` | Worker's reply, typed as caller-asserted `T`. |
77
+
78
+ Error semantics:
79
+
80
+ - WebSocket not OPEN → synchronous reject (no implicit reconnect).
81
+ - Worker error / proxy error → rejects with the proxy's error message.
82
+ - Per-call `timeoutMs` elapses → rejects with timeout error and frees
83
+ the pending entry (the proxy may still complete the call server-side;
84
+ client-side state is freed regardless).
85
+ - WebSocket closes mid-flight → all in-flight calls reject with
86
+ "WebSocket closed before reply".
87
+ - Caller's `signal` aborts → rejects with "aborted"; pending entry is
88
+ freed and the abort listener is removed.
89
+
90
+ Multiple concurrent calls are safe — each request uses a unique
91
+ correlation id and the response/error dispatch keys off it. The
92
+ existing pub/sub channel and keepalive ping/pong are unaffected.
93
+
94
+ Implementation:
95
+
96
+ - `I_ServerUpdateNotifier.callWorker?` — optional interface method, so
97
+ REST-only / polling notifier alternatives can omit it.
98
+ - `Ebus2ProxyServerUpdateNotifier.callWorker` — sends `{type:"request",
99
+ service, payload, timeout, id}` and registers the resolver in a
100
+ `_pendingRequests` Map keyed by id (same pattern as `_pendingRttPings`
101
+ for `measureWsRtt`).
102
+ - `handleMessage` adds `case "response"` to dispatch replies and routes
103
+ correlated `case "error"` to the matching pending promise instead of
104
+ module-level `console.error`.
105
+ - `handleClose` rejects every in-flight call with a clear "WebSocket
106
+ closed before reply" message — the socket they were sent over is
107
+ gone; the client can't observe replies on a different connection.
108
+ - `dispose` is a defensive backstop (`handleClose` normally drains
109
+ first) and rejects any stragglers with "notifier disposed".
110
+ - AbortSignal listeners are cleaned up on every exit path (resolve,
111
+ reject, timeout, dispose) so callers don't leak listeners.
112
+
113
+ Peer-dep bump: `cry-ebus-proxy` now `^2.0.0` (needed for
114
+ `WsRequestMessage` / `WsResponseMessage` types and the proxy-side
115
+ request/response handling).
116
+
117
+ Tests: 8 new mock-based cases in `test/callWorker.test.ts` covering
118
+ delegation, generic return, payload+options forwarding, undefined
119
+ payload preservation, error propagation, missing-notifier guard,
120
+ concurrent independence. 736 bun + 18 vitest pass (was 728 + 18 — 8 new
121
+ bun cases).
122
+
3
123
  ## 0.1.179 (2026-05-13)
4
124
 
5
125
  ### Non-error `console.*` calls now pass numbers as separate arguments
package/dist/index.js CHANGED
@@ -4682,6 +4682,24 @@ var _SyncedDb = class _SyncedDb {
4682
4682
  }
4683
4683
  return fn.call(this.serverUpdateNotifier, timeoutMs);
4684
4684
  }
4685
+ /**
4686
+ * Call an arbitrary worker service via the configured server-update
4687
+ * notifier (typically over its WebSocket). Delegates to
4688
+ * `serverUpdateNotifier.callWorker`. Throws if no notifier is
4689
+ * configured or the notifier doesn't support worker invocation.
4690
+ *
4691
+ * @see I_ServerUpdateNotifier.callWorker
4692
+ */
4693
+ async callWorker(service, payload, options) {
4694
+ var _a;
4695
+ const fn = (_a = this.serverUpdateNotifier) == null ? void 0 : _a.callWorker;
4696
+ if (!fn) {
4697
+ throw new Error(
4698
+ "[SyncedDb] callWorker: no serverUpdateNotifier or notifier does not support callWorker"
4699
+ );
4700
+ }
4701
+ return fn.call(this.serverUpdateNotifier, service, payload, options);
4702
+ }
4685
4703
  /**
4686
4704
  * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
4687
4705
  */
@@ -9713,6 +9731,13 @@ var Ebus2ProxyServerUpdateNotifier = class {
9713
9731
  * after if any are still pending).
9714
9732
  */
9715
9733
  this._pendingRttPings = /* @__PURE__ */ new Map();
9734
+ /**
9735
+ * Pending `callWorker` promises keyed by request id. The matching
9736
+ * `response` (or correlated `error`) frame resolves/rejects them.
9737
+ * Cleared on disconnect — pending callers are rejected with a
9738
+ * "WebSocket closed" error and the per-call timeout is freed.
9739
+ */
9740
+ this._pendingRequests = /* @__PURE__ */ new Map();
9716
9741
  var _a, _b, _c, _d, _e;
9717
9742
  this.endpoint = config.wsUrl;
9718
9743
  this.wsUrl = config.wsUrl;
@@ -9769,6 +9794,18 @@ var Ebus2ProxyServerUpdateNotifier = class {
9769
9794
  this.onWsDisconnectCallbacks.length = 0;
9770
9795
  this.onWsReconnectCallbacks.length = 0;
9771
9796
  this._pendingRttPings.clear();
9797
+ for (const [, pending] of this._pendingRequests) {
9798
+ clearTimeout(pending.timer);
9799
+ if (pending.abortListener && pending.signal) {
9800
+ pending.signal.removeEventListener("abort", pending.abortListener);
9801
+ }
9802
+ pending.reject(
9803
+ new Error(
9804
+ `[Ebus2ProxyNotifier] callWorker(${pending.service}): notifier disposed`
9805
+ )
9806
+ );
9807
+ }
9808
+ this._pendingRequests.clear();
9772
9809
  }
9773
9810
  isConnected() {
9774
9811
  return this.connected && !this.forcedOffline;
@@ -9808,41 +9845,22 @@ var Ebus2ProxyServerUpdateNotifier = class {
9808
9845
  }
9809
9846
  /**
9810
9847
  * End-to-end RTT (client → proxy → broker → echo worker → broker →
9811
- * proxy → client). Sends an HTTP request to ebus-proxy's `echo`
9812
- * service with `Date.now()` as the msgpack payload; the worker
9813
- * returns the payload unchanged, and we verify byte-equality before
9814
- * reporting RTT.
9848
+ * proxy → client). Invokes the `echo` worker via `callWorker` with
9849
+ * `Date.now()` as the payload; the worker returns it unchanged and
9850
+ * we verify byte-equality before reporting RTT.
9815
9851
  *
9816
- * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
9817
- * `wss://` `https://`). Throws on HTTP error, payload mismatch,
9818
- * or timeout.
9852
+ * Rides the existing WebSocket same transport real notifications
9853
+ * arrive on, so this is the more representative diagnostic of actual
9854
+ * app-traffic latency. Throws on payload mismatch, timeout, or any
9855
+ * `callWorker` failure path (WS not OPEN, proxy error, etc).
9819
9856
  *
9820
- * @param timeoutMs Max wait for HTTP response (default: 5000)
9857
+ * @param timeoutMs Max wait for echo reply (default: 5000)
9821
9858
  * @returns RTT in milliseconds (full round-trip)
9822
9859
  */
9823
9860
  async measureEndToEndRtt(timeoutMs = 5e3) {
9824
- const httpBase = this.wsUrl.replace(/^ws:\/\//, "http://").replace(/^wss:\/\//, "https://").replace(/\/+$/, "");
9825
9861
  const sentAt = Date.now();
9826
- const packed = packr2.pack(sentAt);
9827
- let payloadB64;
9828
- const bufCtor = globalThis.Buffer;
9829
- if (bufCtor) {
9830
- payloadB64 = bufCtor.from(packed).toString("base64");
9831
- } else {
9832
- let bin = "";
9833
- for (let i = 0; i < packed.length; i++) bin += String.fromCharCode(packed[i]);
9834
- payloadB64 = btoa(bin);
9835
- }
9836
- const keyParam = this.ebusProxyApiKey ? `&apikey=${encodeURIComponent(this.ebusProxyApiKey)}` : "";
9837
- const url = `${httpBase}/?service=echo&payload=${encodeURIComponent(payloadB64)}${keyParam}&timeout=${timeoutMs}`;
9838
9862
  const t0 = performance.now();
9839
- const signal = typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : void 0;
9840
- const res = await fetch(url, signal ? { signal } : void 0);
9841
- if (!res.ok) {
9842
- throw new Error(`[Ebus2ProxyNotifier] measureEndToEndRtt: HTTP ${res.status}`);
9843
- }
9844
- const buf = new Uint8Array(await res.arrayBuffer());
9845
- const echoed = unpackr2.unpack(buf);
9863
+ const echoed = await this.callWorker("echo", sentAt, { timeoutMs });
9846
9864
  if (echoed !== sentAt) {
9847
9865
  throw new Error(
9848
9866
  `[Ebus2ProxyNotifier] measureEndToEndRtt: payload mismatch (sent ${sentAt}, got ${JSON.stringify(echoed)})`
@@ -9850,6 +9868,111 @@ var Ebus2ProxyServerUpdateNotifier = class {
9850
9868
  }
9851
9869
  return performance.now() - t0;
9852
9870
  }
9871
+ /**
9872
+ * Call an arbitrary ebus2 worker service over the existing
9873
+ * WebSocket and return its msgpack-decoded reply.
9874
+ *
9875
+ * Uses the WS `request` / `response` correlation introduced in
9876
+ * ebus-proxy 2.0 — the whole frame is msgpack, so the payload is
9877
+ * sent as a raw value (no base64, no JSON envelope). Lower latency
9878
+ * than HTTP (no TCP/TLS handshake, no per-call connection setup)
9879
+ * and reuses the same auth + connection that the pub/sub channel
9880
+ * is already using.
9881
+ *
9882
+ * Errors:
9883
+ * - WebSocket not OPEN → throws synchronously (rejects).
9884
+ * - Worker error / proxy error → rejects with the proxy's `error`
9885
+ * frame message.
9886
+ * - Per-call `timeoutMs` elapses → rejects with timeout error and
9887
+ * removes the pending entry. The proxy may still process the
9888
+ * call server-side; client-side state is freed regardless.
9889
+ * - WebSocket closes mid-flight → rejects with "WebSocket closed
9890
+ * before reply".
9891
+ * - Caller `options.signal` aborts → rejects with "aborted".
9892
+ *
9893
+ * Multiple concurrent calls are safe — each request uses a unique
9894
+ * correlation id and the `response`/`error` dispatch keys off it.
9895
+ *
9896
+ * @param service Worker service name registered on cry-ebus2.
9897
+ * @param payload Anything msgpack+structuredClone can serialize.
9898
+ * BSON ObjectId/Timestamp are preprocessed like all other WS
9899
+ * frames. Pass `undefined` to call without a payload.
9900
+ * @param options.timeoutMs Per-call timeout (default 5000). Also
9901
+ * forwarded as the proxy-side `timeout` field.
9902
+ * @param options.signal External AbortSignal for cancellation.
9903
+ * @returns Worker's reply, msgpack-decoded as T.
9904
+ */
9905
+ async callWorker(service, payload, options) {
9906
+ var _a;
9907
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
9908
+ throw new Error(
9909
+ `[Ebus2ProxyNotifier] callWorker(${service}): WebSocket not OPEN`
9910
+ );
9911
+ }
9912
+ const timeoutMs = (_a = options == null ? void 0 : options.timeoutMs) != null ? _a : 5e3;
9913
+ const id = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
9914
+ return new Promise((resolve, reject) => {
9915
+ var _a2;
9916
+ if ((_a2 = options == null ? void 0 : options.signal) == null ? void 0 : _a2.aborted) {
9917
+ reject(new Error(`[Ebus2ProxyNotifier] callWorker(${service}): aborted`));
9918
+ return;
9919
+ }
9920
+ const timer = setTimeout(() => {
9921
+ const pending = this._pendingRequests.get(id);
9922
+ if (!pending) return;
9923
+ this._pendingRequests.delete(id);
9924
+ if (pending.abortListener && pending.signal) {
9925
+ pending.signal.removeEventListener("abort", pending.abortListener);
9926
+ }
9927
+ reject(
9928
+ new Error(
9929
+ `[Ebus2ProxyNotifier] callWorker(${service}): timeout after ${timeoutMs}ms`
9930
+ )
9931
+ );
9932
+ }, timeoutMs);
9933
+ const abortListener = (options == null ? void 0 : options.signal) ? () => {
9934
+ const pending = this._pendingRequests.get(id);
9935
+ if (!pending) return;
9936
+ clearTimeout(pending.timer);
9937
+ this._pendingRequests.delete(id);
9938
+ reject(
9939
+ new Error(`[Ebus2ProxyNotifier] callWorker(${service}): aborted`)
9940
+ );
9941
+ } : void 0;
9942
+ if (abortListener && options.signal) {
9943
+ options.signal.addEventListener("abort", abortListener, { once: true });
9944
+ }
9945
+ this._pendingRequests.set(id, {
9946
+ resolve,
9947
+ reject,
9948
+ timer,
9949
+ service,
9950
+ abortListener,
9951
+ signal: options == null ? void 0 : options.signal
9952
+ });
9953
+ const req = {
9954
+ type: "request",
9955
+ service,
9956
+ payload: payload !== void 0 ? preprocessForPack2(payload) : void 0,
9957
+ timeout: timeoutMs,
9958
+ id
9959
+ };
9960
+ try {
9961
+ this.ws.send(packr2.pack(req));
9962
+ } catch (err) {
9963
+ clearTimeout(timer);
9964
+ this._pendingRequests.delete(id);
9965
+ if (abortListener && options.signal) {
9966
+ options.signal.removeEventListener("abort", abortListener);
9967
+ }
9968
+ reject(
9969
+ new Error(
9970
+ `[Ebus2ProxyNotifier] callWorker(${service}): send failed: ${err}`
9971
+ )
9972
+ );
9973
+ }
9974
+ });
9975
+ }
9853
9976
  /**
9854
9977
  * Set connection lifecycle callbacks.
9855
9978
  * These are merged with any callbacks provided in the constructor config.
@@ -9961,6 +10084,20 @@ var Ebus2ProxyServerUpdateNotifier = class {
9961
10084
  this.connected = false;
9962
10085
  this.subscribedChannels.clear();
9963
10086
  this.clearTimers();
10087
+ if (this._pendingRequests.size > 0) {
10088
+ for (const [, pending] of this._pendingRequests) {
10089
+ clearTimeout(pending.timer);
10090
+ if (pending.abortListener && pending.signal) {
10091
+ pending.signal.removeEventListener("abort", pending.abortListener);
10092
+ }
10093
+ pending.reject(
10094
+ new Error(
10095
+ `[Ebus2ProxyNotifier] callWorker(${pending.service}): WebSocket closed before reply`
10096
+ )
10097
+ );
10098
+ }
10099
+ this._pendingRequests.clear();
10100
+ }
9964
10101
  const reason = event.reason || `Code: ${event.code}`;
9965
10102
  if (wasConnected) {
9966
10103
  for (const callback of this.onWsDisconnectCallbacks) {
@@ -10004,7 +10141,37 @@ var Ebus2ProxyServerUpdateNotifier = class {
10004
10141
  }
10005
10142
  }
10006
10143
  break;
10144
+ case "response": {
10145
+ if (message.id !== void 0) {
10146
+ const pending = this._pendingRequests.get(message.id);
10147
+ if (pending) {
10148
+ clearTimeout(pending.timer);
10149
+ this._pendingRequests.delete(message.id);
10150
+ if (pending.abortListener && pending.signal) {
10151
+ pending.signal.removeEventListener("abort", pending.abortListener);
10152
+ }
10153
+ pending.resolve(message.data);
10154
+ }
10155
+ }
10156
+ break;
10157
+ }
10007
10158
  case "error":
10159
+ if (message.id !== void 0) {
10160
+ const pending = this._pendingRequests.get(message.id);
10161
+ if (pending) {
10162
+ clearTimeout(pending.timer);
10163
+ this._pendingRequests.delete(message.id);
10164
+ if (pending.abortListener && pending.signal) {
10165
+ pending.signal.removeEventListener("abort", pending.abortListener);
10166
+ }
10167
+ pending.reject(
10168
+ new Error(
10169
+ `[Ebus2ProxyNotifier] callWorker(${pending.service}): ${message.error}`
10170
+ )
10171
+ );
10172
+ break;
10173
+ }
10174
+ }
10008
10175
  console.error(`[Ebus2ProxyNotifier] WebSocket server error: ${message.error}`, message.error);
10009
10176
  break;
10010
10177
  }
@@ -69,6 +69,13 @@ export declare class Ebus2ProxyServerUpdateNotifier implements I_ServerUpdateNot
69
69
  * after if any are still pending).
70
70
  */
71
71
  private _pendingRttPings;
72
+ /**
73
+ * Pending `callWorker` promises keyed by request id. The matching
74
+ * `response` (or correlated `error`) frame resolves/rejects them.
75
+ * Cleared on disconnect — pending callers are rejected with a
76
+ * "WebSocket closed" error and the per-call timeout is freed.
77
+ */
78
+ private _pendingRequests;
72
79
  constructor(config: Ebus2ProxyServerUpdateNotifierConfig);
73
80
  subscribe(callback: ServerUpdateCallback): () => void;
74
81
  connect(): Promise<void>;
@@ -97,19 +104,57 @@ export declare class Ebus2ProxyServerUpdateNotifier implements I_ServerUpdateNot
97
104
  measureWsRtt(timeoutMs?: number): Promise<number>;
98
105
  /**
99
106
  * End-to-end RTT (client → proxy → broker → echo worker → broker →
100
- * proxy → client). Sends an HTTP request to ebus-proxy's `echo`
101
- * service with `Date.now()` as the msgpack payload; the worker
102
- * returns the payload unchanged, and we verify byte-equality before
103
- * reporting RTT.
107
+ * proxy → client). Invokes the `echo` worker via `callWorker` with
108
+ * `Date.now()` as the payload; the worker returns it unchanged and
109
+ * we verify byte-equality before reporting RTT.
104
110
  *
105
- * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
106
- * `wss://` `https://`). Throws on HTTP error, payload mismatch,
107
- * or timeout.
111
+ * Rides the existing WebSocket same transport real notifications
112
+ * arrive on, so this is the more representative diagnostic of actual
113
+ * app-traffic latency. Throws on payload mismatch, timeout, or any
114
+ * `callWorker` failure path (WS not OPEN, proxy error, etc).
108
115
  *
109
- * @param timeoutMs Max wait for HTTP response (default: 5000)
116
+ * @param timeoutMs Max wait for echo reply (default: 5000)
110
117
  * @returns RTT in milliseconds (full round-trip)
111
118
  */
112
119
  measureEndToEndRtt(timeoutMs?: number): Promise<number>;
120
+ /**
121
+ * Call an arbitrary ebus2 worker service over the existing
122
+ * WebSocket and return its msgpack-decoded reply.
123
+ *
124
+ * Uses the WS `request` / `response` correlation introduced in
125
+ * ebus-proxy 2.0 — the whole frame is msgpack, so the payload is
126
+ * sent as a raw value (no base64, no JSON envelope). Lower latency
127
+ * than HTTP (no TCP/TLS handshake, no per-call connection setup)
128
+ * and reuses the same auth + connection that the pub/sub channel
129
+ * is already using.
130
+ *
131
+ * Errors:
132
+ * - WebSocket not OPEN → throws synchronously (rejects).
133
+ * - Worker error / proxy error → rejects with the proxy's `error`
134
+ * frame message.
135
+ * - Per-call `timeoutMs` elapses → rejects with timeout error and
136
+ * removes the pending entry. The proxy may still process the
137
+ * call server-side; client-side state is freed regardless.
138
+ * - WebSocket closes mid-flight → rejects with "WebSocket closed
139
+ * before reply".
140
+ * - Caller `options.signal` aborts → rejects with "aborted".
141
+ *
142
+ * Multiple concurrent calls are safe — each request uses a unique
143
+ * correlation id and the `response`/`error` dispatch keys off it.
144
+ *
145
+ * @param service Worker service name registered on cry-ebus2.
146
+ * @param payload Anything msgpack+structuredClone can serialize.
147
+ * BSON ObjectId/Timestamp are preprocessed like all other WS
148
+ * frames. Pass `undefined` to call without a payload.
149
+ * @param options.timeoutMs Per-call timeout (default 5000). Also
150
+ * forwarded as the proxy-side `timeout` field.
151
+ * @param options.signal External AbortSignal for cancellation.
152
+ * @returns Worker's reply, msgpack-decoded as T.
153
+ */
154
+ callWorker<T = unknown>(service: string, payload?: unknown, options?: {
155
+ timeoutMs?: number;
156
+ signal?: AbortSignal;
157
+ }): Promise<T>;
113
158
  /**
114
159
  * Set connection lifecycle callbacks.
115
160
  * These are merged with any callbacks provided in the constructor config.
@@ -78,6 +78,18 @@ export declare class SyncedDb implements I_SyncedDb {
78
78
  * notifier is configured or the notifier doesn't support it.
79
79
  */
80
80
  measureEndToEndRtt(timeoutMs?: number): Promise<number>;
81
+ /**
82
+ * Call an arbitrary worker service via the configured server-update
83
+ * notifier (typically over its WebSocket). Delegates to
84
+ * `serverUpdateNotifier.callWorker`. Throws if no notifier is
85
+ * configured or the notifier doesn't support worker invocation.
86
+ *
87
+ * @see I_ServerUpdateNotifier.callWorker
88
+ */
89
+ callWorker<T = unknown>(service: string, payload?: unknown, options?: {
90
+ timeoutMs?: number;
91
+ signal?: AbortSignal;
92
+ }): Promise<T>;
81
93
  /**
82
94
  * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
83
95
  */
@@ -83,4 +83,29 @@ export interface I_ServerUpdateNotifier {
83
83
  * @param timeoutMs Max wait for response (default: 5000)
84
84
  */
85
85
  measureEndToEndRtt?(timeoutMs?: number): Promise<number>;
86
+ /**
87
+ * Call an arbitrary worker service via the notifier's transport and
88
+ * return its decoded reply.
89
+ *
90
+ * For WS-capable notifiers, this typically rides the same WebSocket
91
+ * connection as pub/sub (lower latency than spinning up an HTTP
92
+ * fetch) and uses transport-native correlation (`request` /
93
+ * `response` frames). Implementations may fall back to HTTP if the
94
+ * transport doesn't support inline RPC.
95
+ *
96
+ * Optional. Throws if transport is not connected, the worker errors,
97
+ * or the per-call timeout elapses. Implementations that don't
98
+ * support worker invocation may omit this method.
99
+ *
100
+ * @param service Worker service name (e.g. `"echo"`, `"print"`).
101
+ * @param payload Service-specific payload — must be serializable by
102
+ * the transport's codec (msgpack with `structuredClone: true` for
103
+ * the WS implementation).
104
+ * @param options.timeoutMs Per-call timeout in milliseconds.
105
+ * @param options.signal External AbortSignal for cancellation.
106
+ */
107
+ callWorker?<T = unknown>(service: string, payload?: unknown, options?: {
108
+ timeoutMs?: number;
109
+ signal?: AbortSignal;
110
+ }): Promise<T>;
86
111
  }
@@ -1118,6 +1118,26 @@ export interface I_SyncedDb {
1118
1118
  * @returns RTT in milliseconds (full round-trip)
1119
1119
  */
1120
1120
  measureEndToEndRtt(timeoutMs?: number): Promise<number>;
1121
+ /**
1122
+ * Call an arbitrary ebus2 worker service via the configured server-
1123
+ * update notifier and return the worker's decoded reply. Routes
1124
+ * through the existing WebSocket on the
1125
+ * `Ebus2ProxyServerUpdateNotifier` implementation (ebus-proxy ≥ 2.0
1126
+ * `request` / `response` frames). Throws if no notifier is
1127
+ * configured, the transport is not connected, the worker errors, or
1128
+ * the per-call timeout elapses.
1129
+ *
1130
+ * @param service Worker service name (e.g. "echo", "print").
1131
+ * @param payload Anything msgpack+structuredClone can serialize.
1132
+ * BSON ObjectId/Timestamp preprocessed automatically.
1133
+ * @param options.timeoutMs Per-call timeout (default 5000).
1134
+ * @param options.signal External AbortSignal for cancellation.
1135
+ * @returns Worker's reply typed as T (caller-asserted).
1136
+ */
1137
+ callWorker<T = unknown>(service: string, payload?: unknown, options?: {
1138
+ timeoutMs?: number;
1139
+ signal?: AbortSignal;
1140
+ }): Promise<T>;
1121
1141
  /**
1122
1142
  * Get metadata for a single object.
1123
1143
  * @param collection Collection name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.179",
3
+ "version": "0.1.181",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -29,12 +29,12 @@
29
29
  "devDependencies": {
30
30
  "@types/bun": "latest",
31
31
  "bson": "^7.2.0",
32
- "cry-ebus-proxy": "^1.0.3",
32
+ "cry-ebus-proxy": "^2.0.0",
33
33
  "dexie": "^4.4.2",
34
34
  "esbuild": "^0.28.0",
35
35
  "fake-indexeddb": "^6.2.5",
36
36
  "typescript": "^6",
37
- "vitest": "^4.1.5"
37
+ "vitest": "^4.1.6"
38
38
  },
39
39
  "dependencies": {
40
40
  "cry-helpers": "^2.1.194",