cry-synced-db-client 0.1.175 → 0.1.176

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
@@ -2,6 +2,108 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Passive transport metrics on `findNewerManyStream` round-trip (rdb2)
6
+
7
+ `FindNewerManyResultInfo` (delivered to `onFindNewerManyResult`) now
8
+ carries three optional metrics captured passively during the sync
9
+ round-trip — no extra round-trip needed, no overhead when callbacks
10
+ are absent:
11
+
12
+ - `requestBytes` — msgpack-encoded request body size (upload payload).
13
+ - `responseBytes` — sum of wire bytes received across all streaming
14
+ chunks (download payload). Renamed from the short-lived
15
+ `bytesStreamed`.
16
+ - `ttfbMs` — time-to-first-byte: elapsed from request send until
17
+ response headers arrive. Conflates `upload travel + server work +
18
+ first-byte travel` (would need a server-timing header to split
19
+ further).
20
+
21
+ Combined with the existing `durationMs`, callers can derive download
22
+ throughput and a server-vs-network breakdown without instrumenting the
23
+ transport themselves:
24
+
25
+ ```typescript
26
+ new SyncedDb({
27
+ onFindNewerManyResult: (info) => {
28
+ if (!info.success || info.responseBytes == null) return;
29
+ const downloadMs = info.durationMs - (info.ttfbMs ?? 0);
30
+ const kBps = downloadMs > 0 ? (info.responseBytes / downloadMs) : 0;
31
+ metrics.record({
32
+ req: info.requestBytes,
33
+ resp: info.responseBytes,
34
+ ttfb: info.ttfbMs,
35
+ total: info.durationMs,
36
+ kBps,
37
+ });
38
+ },
39
+ });
40
+ ```
41
+
42
+ Wired through the transport layer:
43
+
44
+ - `I_RestInterface.findNewerManyStream` options gain three optional
45
+ byte/timing callbacks: `onRequestBytes(bytes)`,
46
+ `onTtfbMs(ms)`, `onResponseChunkBytes(bytes)`. All fire even on
47
+ failure paths where applicable. Old two-arg `onChunk` callbacks keep
48
+ working unchanged.
49
+ - `RestProxy` fires `onRequestBytes` just before `fetch`, `onTtfbMs`
50
+ immediately after `await fetch(...)` resolves (separate `fetchStart`
51
+ marker, independent of `timeRequests`), and `onResponseChunkBytes`
52
+ per `reader.read()` chunk in `parseStreamingResponse`.
53
+ - `SyncEngine.syncCore` accumulates bytes / captures TTFB and forwards
54
+ them into `callOnFindNewerManyResult` on both success and error
55
+ paths. Mocks and alternative transports that don't fire the
56
+ callbacks leave the fields `undefined` — graceful degradation.
57
+
58
+ ### RTT measurement: `measureWsRtt` + `measureEndToEndRtt` (ebus-proxy)
59
+
60
+ Two diagnostic methods on `SyncedDb` (and `I_ServerUpdateNotifier`) to
61
+ measure connection latency. Together they isolate where latency comes
62
+ from:
63
+
64
+ - **`measureWsRtt(timeoutMs?)`** — client → notifier server → client.
65
+ Sends a tagged WS ping (`{type: "ping", id: <unique>}`) and resolves
66
+ with `performance.now()` delta when the matching pong arrives. Pure
67
+ proxy responsiveness + network. Sub-ms on localhost.
68
+ - **`measureEndToEndRtt(timeoutMs?)`** — client → proxy → broker →
69
+ `echo` worker → broker → proxy → client. HTTP GET to ebus-proxy's
70
+ `/?service=echo` endpoint with `Date.now()` msgpack payload; worker
71
+ returns it unchanged. ~3-15 ms on localhost.
72
+
73
+ Both return RTT in milliseconds, both Promise-based (don't block thread).
74
+ Multiple concurrent `measureWsRtt()` calls work — each uses a unique
75
+ correlation id.
76
+
77
+ Diagnostic interpretation:
78
+
79
+ | WS RTT | E2E RTT | Likely cause |
80
+ |---|---|---|
81
+ | low | low | All good |
82
+ | low | high | Broker / echo worker overloaded |
83
+ | high | high (similar Δ) | Network or proxy slow |
84
+ | spikes | stable | WS frame issue (tab throttle, frozen socket) |
85
+
86
+ ```typescript
87
+ const wsRtt = await syncedDb.measureWsRtt();
88
+ const e2eRtt = await syncedDb.measureEndToEndRtt();
89
+ console.log(`network/proxy=${wsRtt.toFixed(1)}ms, full chain=${e2eRtt.toFixed(1)}ms`);
90
+ ```
91
+
92
+ The notifier-interface methods are **optional** (`measureWsRtt?` /
93
+ `measureEndToEndRtt?`) so custom `I_ServerUpdateNotifier`
94
+ implementations don't need to implement them. `SyncedDb` pass-throughs
95
+ throw a descriptive error if missing.
96
+
97
+ Implementation in `Ebus2ProxyServerUpdateNotifier`: tagged ping reuses
98
+ the existing WS handler (extends pong dispatch in `handleMessage`
99
+ with a `_pendingRttPings` Map). HTTP echo derives the base URL from
100
+ `wsUrl` (`ws://` → `http://`, `wss://` → `https://`), msgpack-encodes
101
+ the payload, hits `/?service=echo`, validates byte-equal echo.
102
+
103
+ Live test (localhost, warm, 20 samples): WS p50 0.30 ms, E2E p50
104
+ 4.11 ms. Mock-only unit tests in `test/measureRtt.test.ts` (7 cases —
105
+ delegation, propagated rejection, no-notifier error, shape sanity).
106
+
5
107
  ### `save(coll, id, {field: {}})` clears existing nested children
6
108
 
7
109
  `computeDiffInto` for plain objects iterated only `Object.keys(update)`,
package/dist/index.js CHANGED
@@ -3083,6 +3083,9 @@ var _SyncEngine = class _SyncEngine {
3083
3083
  source: isInitial ? "initial" : "incremental"
3084
3084
  });
3085
3085
  }
3086
+ let requestBytes;
3087
+ let responseBytes;
3088
+ let ttfbMs;
3086
3089
  try {
3087
3090
  const completedCollections = /* @__PURE__ */ new Set();
3088
3091
  const allSpecs = extras && extras.specs.length > 0 ? [...syncSpecs, ...extras.specs] : syncSpecs;
@@ -3118,6 +3121,17 @@ var _SyncEngine = class _SyncEngine {
3118
3121
  items: items.length
3119
3122
  });
3120
3123
  }
3124
+ },
3125
+ {
3126
+ onRequestBytes: (bytes) => {
3127
+ requestBytes = bytes;
3128
+ },
3129
+ onTtfbMs: (ms) => {
3130
+ ttfbMs = ms;
3131
+ },
3132
+ onResponseChunkBytes: (bytes) => {
3133
+ responseBytes = (responseBytes != null ? responseBytes : 0) + bytes;
3134
+ }
3121
3135
  }
3122
3136
  ),
3123
3137
  "findNewerManyStream"
@@ -3130,7 +3144,15 @@ var _SyncEngine = class _SyncEngine {
3130
3144
  sentCount: 0
3131
3145
  };
3132
3146
  }
3133
- this.callOnFindNewerManyResult(syncSpecs, {}, findNewerManyStartTime, true, calledFrom);
3147
+ this.callOnFindNewerManyResult(
3148
+ syncSpecs,
3149
+ {},
3150
+ findNewerManyStartTime,
3151
+ true,
3152
+ calledFrom,
3153
+ void 0,
3154
+ { requestBytes, responseBytes, ttfbMs }
3155
+ );
3134
3156
  this.callbackSafe(this.callbacks.onServerSyncEnd, {
3135
3157
  calledFrom,
3136
3158
  collectionCount: syncSpecs.length,
@@ -3139,7 +3161,15 @@ var _SyncEngine = class _SyncEngine {
3139
3161
  success: true
3140
3162
  });
3141
3163
  } catch (err) {
3142
- this.callOnFindNewerManyResult(syncSpecs, {}, findNewerManyStartTime, false, calledFrom, err);
3164
+ this.callOnFindNewerManyResult(
3165
+ syncSpecs,
3166
+ {},
3167
+ findNewerManyStartTime,
3168
+ false,
3169
+ calledFrom,
3170
+ err,
3171
+ { requestBytes, responseBytes, ttfbMs }
3172
+ );
3143
3173
  this.callbackSafe(this.callbacks.onServerSyncEnd, {
3144
3174
  calledFrom,
3145
3175
  collectionCount: syncSpecs.length,
@@ -3799,7 +3829,7 @@ var _SyncEngine = class _SyncEngine {
3799
3829
  }
3800
3830
  }
3801
3831
  }
3802
- callOnFindNewerManyResult(specs, results, startTime, success, calledFrom, error) {
3832
+ callOnFindNewerManyResult(specs, results, startTime, success, calledFrom, error, metrics) {
3803
3833
  if (this.callbacks.onFindNewerManyResult) {
3804
3834
  try {
3805
3835
  this.callbacks.onFindNewerManyResult({
@@ -3808,7 +3838,10 @@ var _SyncEngine = class _SyncEngine {
3808
3838
  durationMs: Date.now() - startTime,
3809
3839
  success,
3810
3840
  error: error instanceof Error ? error : error ? new Error(String(error)) : void 0,
3811
- calledFrom
3841
+ calledFrom,
3842
+ requestBytes: metrics == null ? void 0 : metrics.requestBytes,
3843
+ responseBytes: metrics == null ? void 0 : metrics.responseBytes,
3844
+ ttfbMs: metrics == null ? void 0 : metrics.ttfbMs
3812
3845
  });
3813
3846
  } catch (err) {
3814
3847
  console.error("[SyncEngine] onFindNewerManyResult callback failed:", err);
@@ -4571,6 +4604,36 @@ var _SyncedDb = class _SyncedDb {
4571
4604
  followerSince() {
4572
4605
  return this.leaderElection.followerSince();
4573
4606
  }
4607
+ /**
4608
+ * WS round-trip time (client → notifier server → client). Delegates
4609
+ * to `serverUpdateNotifier.measureWsRtt`. Throws if no notifier is
4610
+ * configured or the notifier implementation doesn't support RTT.
4611
+ */
4612
+ async measureWsRtt(timeoutMs) {
4613
+ var _a;
4614
+ const fn = (_a = this.serverUpdateNotifier) == null ? void 0 : _a.measureWsRtt;
4615
+ if (!fn) {
4616
+ throw new Error(
4617
+ "[SyncedDb] measureWsRtt: no serverUpdateNotifier or notifier does not support RTT"
4618
+ );
4619
+ }
4620
+ return fn.call(this.serverUpdateNotifier, timeoutMs);
4621
+ }
4622
+ /**
4623
+ * End-to-end RTT including downstream broker/worker hop. Delegates
4624
+ * to `serverUpdateNotifier.measureEndToEndRtt`. Throws if no
4625
+ * notifier is configured or the notifier doesn't support it.
4626
+ */
4627
+ async measureEndToEndRtt(timeoutMs) {
4628
+ var _a;
4629
+ const fn = (_a = this.serverUpdateNotifier) == null ? void 0 : _a.measureEndToEndRtt;
4630
+ if (!fn) {
4631
+ throw new Error(
4632
+ "[SyncedDb] measureEndToEndRtt: no serverUpdateNotifier or notifier does not support end-to-end RTT"
4633
+ );
4634
+ }
4635
+ return fn.call(this.serverUpdateNotifier, timeoutMs);
4636
+ }
4574
4637
  /**
4575
4638
  * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
4576
4639
  */
@@ -9347,11 +9410,12 @@ var RestProxy = class {
9347
9410
  * type=0x01 for data, type=0x00 for end-of-stream.
9348
9411
  */
9349
9412
  async findNewerManyStream(spec, onChunk, options) {
9350
- var _a, _b, _c;
9413
+ var _a, _b, _c, _d, _e;
9351
9414
  const connectTimeout = (_a = options == null ? void 0 : options.timeoutMs) != null ? _a : this.defaultTimeoutMs;
9352
9415
  const activityTimeout = (_b = options == null ? void 0 : options.activityTimeoutMs) != null ? _b : 3e4;
9353
9416
  const externalSignal = (_c = options == null ? void 0 : options.signal) != null ? _c : this.globalSignal;
9354
9417
  const startTime = this.timeRequests ? performance.now() : 0;
9418
+ const fetchStart = performance.now();
9355
9419
  const data = {
9356
9420
  payload: {
9357
9421
  db: this.tenant,
@@ -9365,6 +9429,7 @@ var RestProxy = class {
9365
9429
  }
9366
9430
  };
9367
9431
  const body = pack2(data);
9432
+ (_d = options == null ? void 0 : options.onRequestBytes) == null ? void 0 : _d.call(options, body.byteLength);
9368
9433
  const requestUrl = this.apiKey ? `${this.endpoint}?apikey=${this.apiKey}&stream=1` : `${this.endpoint}?stream=1`;
9369
9434
  const controller = new AbortController();
9370
9435
  let timeoutId = setTimeout(
@@ -9379,6 +9444,7 @@ var RestProxy = class {
9379
9444
  body,
9380
9445
  signal: combinedSignal
9381
9446
  });
9447
+ (_e = options == null ? void 0 : options.onTtfbMs) == null ? void 0 : _e.call(options, performance.now() - fetchStart);
9382
9448
  clearTimeout(timeoutId);
9383
9449
  timeoutId = void 0;
9384
9450
  if (!response.ok) {
@@ -9390,7 +9456,12 @@ var RestProxy = class {
9390
9456
  timeoutId = setTimeout(() => controller.abort(), activityTimeout);
9391
9457
  };
9392
9458
  resetActivity();
9393
- await this.parseStreamingResponse(response, onChunk, resetActivity);
9459
+ await this.parseStreamingResponse(
9460
+ response,
9461
+ onChunk,
9462
+ resetActivity,
9463
+ options == null ? void 0 : options.onResponseChunkBytes
9464
+ );
9394
9465
  if (timeoutId !== void 0) clearTimeout(timeoutId);
9395
9466
  timeoutId = void 0;
9396
9467
  if (this.timeRequests) {
@@ -9428,7 +9499,7 @@ var RestProxy = class {
9428
9499
  *
9429
9500
  * `onChunk` receives `specId` as the third arg for type-0x02 frames; `undefined` otherwise.
9430
9501
  */
9431
- async parseStreamingResponse(response, onChunk, onActivity) {
9502
+ async parseStreamingResponse(response, onChunk, onActivity, onChunkBytes) {
9432
9503
  const reader = response.body.getReader();
9433
9504
  const buffer = new StreamBuffer();
9434
9505
  const decoder2 = new TextDecoder();
@@ -9436,6 +9507,7 @@ var RestProxy = class {
9436
9507
  const { done, value } = await reader.read();
9437
9508
  if (done) return false;
9438
9509
  onActivity();
9510
+ if (onChunkBytes) onChunkBytes(value.byteLength);
9439
9511
  buffer.append(value);
9440
9512
  return true;
9441
9513
  };
@@ -9570,6 +9642,13 @@ var Ebus2ProxyServerUpdateNotifier = class {
9570
9642
  this.reconnectAttempt = 0;
9571
9643
  this.forcedOffline = false;
9572
9644
  this.subscribedChannels = /* @__PURE__ */ new Set();
9645
+ /**
9646
+ * Pending RTT measurement promises keyed by ping id. Each entry is a
9647
+ * resolver that the pong handler invokes once the matching pong
9648
+ * arrives. Disconnect clears the map (caller's timeout fires soon
9649
+ * after if any are still pending).
9650
+ */
9651
+ this._pendingRttPings = /* @__PURE__ */ new Map();
9573
9652
  var _a, _b, _c, _d, _e;
9574
9653
  this.endpoint = config.wsUrl;
9575
9654
  this.wsUrl = config.wsUrl;
@@ -9625,10 +9704,88 @@ var Ebus2ProxyServerUpdateNotifier = class {
9625
9704
  this.onWsConnectCallbacks.length = 0;
9626
9705
  this.onWsDisconnectCallbacks.length = 0;
9627
9706
  this.onWsReconnectCallbacks.length = 0;
9707
+ this._pendingRttPings.clear();
9628
9708
  }
9629
9709
  isConnected() {
9630
9710
  return this.connected && !this.forcedOffline;
9631
9711
  }
9712
+ /**
9713
+ * WS round-trip time (client → proxy → client). Sends a tagged ping
9714
+ * over the existing WebSocket and resolves with `performance.now()`
9715
+ * delta when the matching pong arrives. Does NOT touch the cry-ebus2
9716
+ * broker or any worker — measures pure proxy responsiveness +
9717
+ * network latency.
9718
+ *
9719
+ * Throws if the WebSocket is not OPEN. The keepalive ping/pong
9720
+ * watchdog is unaffected; multiple `measureWsRtt()` calls can be
9721
+ * in flight simultaneously (each uses a unique correlation id).
9722
+ *
9723
+ * @param timeoutMs Max wait for matching pong (default: 5000)
9724
+ * @returns RTT in milliseconds
9725
+ */
9726
+ async measureWsRtt(timeoutMs = 5e3) {
9727
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
9728
+ throw new Error("[Ebus2ProxyNotifier] measureWsRtt: WebSocket not OPEN");
9729
+ }
9730
+ const id = `rtt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
9731
+ const t0 = performance.now();
9732
+ return new Promise((resolve, reject) => {
9733
+ const timer = setTimeout(() => {
9734
+ this._pendingRttPings.delete(id);
9735
+ reject(new Error(`[Ebus2ProxyNotifier] measureWsRtt: timeout after ${timeoutMs}ms`));
9736
+ }, timeoutMs);
9737
+ this._pendingRttPings.set(id, () => {
9738
+ clearTimeout(timer);
9739
+ resolve(performance.now() - t0);
9740
+ });
9741
+ const pingMsg = { type: "ping", id };
9742
+ this.ws.send(packr2.pack(preprocessForPack2(pingMsg)));
9743
+ });
9744
+ }
9745
+ /**
9746
+ * End-to-end RTT (client → proxy → broker → echo worker → broker →
9747
+ * proxy → client). Sends an HTTP request to ebus-proxy's `echo`
9748
+ * service with `Date.now()` as the msgpack payload; the worker
9749
+ * returns the payload unchanged, and we verify byte-equality before
9750
+ * reporting RTT.
9751
+ *
9752
+ * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
9753
+ * `wss://` → `https://`). Throws on HTTP error, payload mismatch,
9754
+ * or timeout.
9755
+ *
9756
+ * @param timeoutMs Max wait for HTTP response (default: 5000)
9757
+ * @returns RTT in milliseconds (full round-trip)
9758
+ */
9759
+ async measureEndToEndRtt(timeoutMs = 5e3) {
9760
+ const httpBase = this.wsUrl.replace(/^ws:\/\//, "http://").replace(/^wss:\/\//, "https://").replace(/\/+$/, "");
9761
+ const sentAt = Date.now();
9762
+ const packed = packr2.pack(sentAt);
9763
+ let payloadB64;
9764
+ const bufCtor = globalThis.Buffer;
9765
+ if (bufCtor) {
9766
+ payloadB64 = bufCtor.from(packed).toString("base64");
9767
+ } else {
9768
+ let bin = "";
9769
+ for (let i = 0; i < packed.length; i++) bin += String.fromCharCode(packed[i]);
9770
+ payloadB64 = btoa(bin);
9771
+ }
9772
+ const keyParam = this.ebusProxyApiKey ? `&apikey=${encodeURIComponent(this.ebusProxyApiKey)}` : "";
9773
+ const url = `${httpBase}/?service=echo&payload=${encodeURIComponent(payloadB64)}${keyParam}&timeout=${timeoutMs}`;
9774
+ const t0 = performance.now();
9775
+ const signal = typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : void 0;
9776
+ const res = await fetch(url, signal ? { signal } : void 0);
9777
+ if (!res.ok) {
9778
+ throw new Error(`[Ebus2ProxyNotifier] measureEndToEndRtt: HTTP ${res.status}`);
9779
+ }
9780
+ const buf = new Uint8Array(await res.arrayBuffer());
9781
+ const echoed = unpackr2.unpack(buf);
9782
+ if (echoed !== sentAt) {
9783
+ throw new Error(
9784
+ `[Ebus2ProxyNotifier] measureEndToEndRtt: payload mismatch (sent ${sentAt}, got ${JSON.stringify(echoed)})`
9785
+ );
9786
+ }
9787
+ return performance.now() - t0;
9788
+ }
9632
9789
  /**
9633
9790
  * Set connection lifecycle callbacks.
9634
9791
  * These are merged with any callbacks provided in the constructor config.
@@ -9775,6 +9932,13 @@ var Ebus2ProxyServerUpdateNotifier = class {
9775
9932
  break;
9776
9933
  case "pong":
9777
9934
  this.handlePong();
9935
+ if (message.id !== void 0) {
9936
+ const resolver = this._pendingRttPings.get(message.id);
9937
+ if (resolver) {
9938
+ this._pendingRttPings.delete(message.id);
9939
+ resolver(message);
9940
+ }
9941
+ }
9778
9942
  break;
9779
9943
  case "error":
9780
9944
  console.error("[Ebus2ProxyNotifier] WebSocket server error:", message.error);
@@ -62,6 +62,13 @@ export declare class Ebus2ProxyServerUpdateNotifier implements I_ServerUpdateNot
62
62
  private pongTimer?;
63
63
  private forcedOffline;
64
64
  private subscribedChannels;
65
+ /**
66
+ * Pending RTT measurement promises keyed by ping id. Each entry is a
67
+ * resolver that the pong handler invokes once the matching pong
68
+ * arrives. Disconnect clears the map (caller's timeout fires soon
69
+ * after if any are still pending).
70
+ */
71
+ private _pendingRttPings;
65
72
  constructor(config: Ebus2ProxyServerUpdateNotifierConfig);
66
73
  subscribe(callback: ServerUpdateCallback): () => void;
67
74
  connect(): Promise<void>;
@@ -73,6 +80,36 @@ export declare class Ebus2ProxyServerUpdateNotifier implements I_ServerUpdateNot
73
80
  */
74
81
  dispose(): void;
75
82
  isConnected(): boolean;
83
+ /**
84
+ * WS round-trip time (client → proxy → client). Sends a tagged ping
85
+ * over the existing WebSocket and resolves with `performance.now()`
86
+ * delta when the matching pong arrives. Does NOT touch the cry-ebus2
87
+ * broker or any worker — measures pure proxy responsiveness +
88
+ * network latency.
89
+ *
90
+ * Throws if the WebSocket is not OPEN. The keepalive ping/pong
91
+ * watchdog is unaffected; multiple `measureWsRtt()` calls can be
92
+ * in flight simultaneously (each uses a unique correlation id).
93
+ *
94
+ * @param timeoutMs Max wait for matching pong (default: 5000)
95
+ * @returns RTT in milliseconds
96
+ */
97
+ measureWsRtt(timeoutMs?: number): Promise<number>;
98
+ /**
99
+ * 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.
104
+ *
105
+ * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
106
+ * `wss://` → `https://`). Throws on HTTP error, payload mismatch,
107
+ * or timeout.
108
+ *
109
+ * @param timeoutMs Max wait for HTTP response (default: 5000)
110
+ * @returns RTT in milliseconds (full round-trip)
111
+ */
112
+ measureEndToEndRtt(timeoutMs?: number): Promise<number>;
76
113
  /**
77
114
  * Set connection lifecycle callbacks.
78
115
  * These are merged with any callbacks provided in the constructor config.
@@ -119,6 +119,9 @@ export declare class RestProxy implements I_RestInterface {
119
119
  timeoutMs?: number;
120
120
  signal?: AbortSignal;
121
121
  activityTimeoutMs?: number;
122
+ onRequestBytes?: (bytes: number) => void;
123
+ onTtfbMs?: (ms: number) => void;
124
+ onResponseChunkBytes?: (bytes: number) => void;
122
125
  }): Promise<void>;
123
126
  /**
124
127
  * Parse streaming response. Auto-detects format:
@@ -65,6 +65,18 @@ export declare class SyncedDb implements I_SyncedDb {
65
65
  isLeaderTab(): boolean;
66
66
  leaderSince(): Date | undefined;
67
67
  followerSince(): Date | undefined;
68
+ /**
69
+ * WS round-trip time (client → notifier server → client). Delegates
70
+ * to `serverUpdateNotifier.measureWsRtt`. Throws if no notifier is
71
+ * configured or the notifier implementation doesn't support RTT.
72
+ */
73
+ measureWsRtt(timeoutMs?: number): Promise<number>;
74
+ /**
75
+ * End-to-end RTT including downstream broker/worker hop. Delegates
76
+ * to `serverUpdateNotifier.measureEndToEndRtt`. Throws if no
77
+ * notifier is configured or the notifier doesn't support it.
78
+ */
79
+ measureEndToEndRtt(timeoutMs?: number): Promise<number>;
68
80
  /**
69
81
  * Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
70
82
  */
@@ -106,10 +106,26 @@ export interface I_RestInterface {
106
106
  * Streaming variant of findNewerMany. Calls onChunk for each batch of items as they arrive.
107
107
  * `specId` is forwarded as the third arg when the originating spec set one — `undefined` otherwise.
108
108
  * Old two-arg `onChunk` callbacks keep working unchanged (the third arg is ignored).
109
+ *
110
+ * Optional metric callbacks (all fire even on failure paths where applicable):
111
+ * - `onRequestBytes(bytes)` — fires once just before fetch with the
112
+ * msgpack-encoded request body's byte length.
113
+ * - `onTtfbMs(ms)` — fires once when response headers are received,
114
+ * with elapsed time since the fetch started. TTFB conflates
115
+ * upload + server processing + first-byte travel — server-side
116
+ * timing header (if available) is required to split further.
117
+ * - `onResponseChunkBytes(bytes)` — fires per response chunk read
118
+ * with that chunk's byte length (summed = total response bytes).
119
+ *
120
+ * Combined, callers can compute upload/download throughput AND
121
+ * server-vs-network breakdown without extra round-trips.
109
122
  */
110
123
  findNewerManyStream<T>(spec: GetNewerSpec<T>[], onChunk: (collection: string, items: T[], specId?: string) => Promise<void>, options?: {
111
124
  timeoutMs?: number;
112
125
  signal?: AbortSignal;
126
+ onRequestBytes?: (bytes: number) => void;
127
+ onTtfbMs?: (ms: number) => void;
128
+ onResponseChunkBytes?: (bytes: number) => void;
113
129
  }): Promise<void>;
114
130
  deleteOne<T>(collection: string, query: QuerySpec<T>): Promise<T>;
115
131
  /** Izvede agregacijo na serverju */
@@ -54,4 +54,33 @@ export interface I_ServerUpdateNotifier {
54
54
  * Optional method.
55
55
  */
56
56
  dispose?(): void;
57
+ /**
58
+ * Measure round-trip time over the existing transport (e.g. WebSocket
59
+ * ping/pong). Returns RTT in milliseconds. Does NOT involve any
60
+ * downstream broker or worker — measures the client ↔ notifier
61
+ * server hop only.
62
+ *
63
+ * Useful for diagnosing connection quality. Compare with
64
+ * `measureEndToEndRtt` to isolate broker/worker overhead.
65
+ *
66
+ * Optional. Throws if transport is not connected or doesn't support
67
+ * RTT measurement.
68
+ *
69
+ * @param timeoutMs Max wait for response (default: 5000)
70
+ */
71
+ measureWsRtt?(timeoutMs?: number): Promise<number>;
72
+ /**
73
+ * Measure full round-trip time including any downstream broker /
74
+ * worker hop (e.g. ebus-proxy → cry-ebus2 broker → echo worker →
75
+ * back). Returns RTT in milliseconds.
76
+ *
77
+ * Implementation typically invokes a server-side `echo` service that
78
+ * returns the payload unchanged, so the measurement is end-to-end.
79
+ *
80
+ * Optional. Throws if not supported or if the round-trip fails
81
+ * (e.g. timeout, payload mismatch, transport error).
82
+ *
83
+ * @param timeoutMs Max wait for response (default: 5000)
84
+ */
85
+ measureEndToEndRtt?(timeoutMs?: number): Promise<number>;
57
86
  }
@@ -189,6 +189,27 @@ export interface FindNewerManyResultInfo {
189
189
  error?: Error;
190
190
  /** Where sync was called from (for debugging) */
191
191
  calledFrom?: string;
192
+ /**
193
+ * Msgpack-encoded request body size in bytes (upload payload).
194
+ * Undefined when the transport implementation didn't report it.
195
+ */
196
+ requestBytes?: number;
197
+ /**
198
+ * Sum of wire bytes received across all streaming chunks (download
199
+ * payload). For passive download-speed measurement compute
200
+ * `responseBytes / (durationMs - ttfbMs) * 1000` for bytes/sec
201
+ * (excludes server processing time).
202
+ * Undefined when the transport didn't report per-chunk byte counts.
203
+ */
204
+ responseBytes?: number;
205
+ /**
206
+ * Time-to-first-byte in ms — elapsed from request send until response
207
+ * headers are received. Conflates upload travel + server processing +
208
+ * first-byte travel. Together with `responseBytes` and `durationMs`
209
+ * enables passive throughput + server-vs-network breakdown estimates.
210
+ * Undefined when the transport didn't report it.
211
+ */
212
+ ttfbMs?: number;
192
213
  }
193
214
  /**
194
215
  * Callback payload for Dexie write requests (before writing)
@@ -1070,6 +1091,33 @@ export interface I_SyncedDb {
1070
1091
  * @returns Date of follower transition, or undefined if currently the leader
1071
1092
  */
1072
1093
  followerSince(): Date | undefined;
1094
+ /**
1095
+ * Measure WS round-trip time: client → notifier server → client.
1096
+ * Pure proxy / network latency, without any downstream broker or
1097
+ * worker hop. Pairs with `measureEndToEndRtt` for diagnostics:
1098
+ * a low WS RTT + high end-to-end RTT points at the broker/worker
1099
+ * as the bottleneck; a high WS RTT points at the network or proxy.
1100
+ *
1101
+ * Throws when no `serverUpdateNotifier` is configured or the
1102
+ * notifier implementation does not support RTT measurement.
1103
+ *
1104
+ * @param timeoutMs Max wait for response (default: 5000)
1105
+ * @returns RTT in milliseconds
1106
+ */
1107
+ measureWsRtt(timeoutMs?: number): Promise<number>;
1108
+ /**
1109
+ * Measure end-to-end RTT including the downstream broker/worker
1110
+ * hop (e.g. ebus-proxy → cry-ebus2 broker → echo worker → back).
1111
+ * The implementation typically calls a server-side `echo` service
1112
+ * that returns the payload unchanged.
1113
+ *
1114
+ * Throws when no `serverUpdateNotifier` is configured or the
1115
+ * notifier implementation does not support end-to-end RTT.
1116
+ *
1117
+ * @param timeoutMs Max wait for response (default: 5000)
1118
+ * @returns RTT in milliseconds (full round-trip)
1119
+ */
1120
+ measureEndToEndRtt(timeoutMs?: number): Promise<number>;
1073
1121
  /**
1074
1122
  * Get metadata for a single object.
1075
1123
  * @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.175",
3
+ "version": "0.1.176",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",