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 +102 -0
- package/dist/index.js +171 -7
- package/dist/src/db/Ebus2ProxyServerUpdateNotifier.d.ts +37 -0
- package/dist/src/db/RestProxy.d.ts +3 -0
- package/dist/src/db/SyncedDb.d.ts +12 -0
- package/dist/src/types/I_RestInterface.d.ts +16 -0
- package/dist/src/types/I_ServerUpdateNotifier.d.ts +29 -0
- package/dist/src/types/I_SyncedDb.d.ts +48 -0
- package/package.json +1 -1
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(
|
|
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(
|
|
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(
|
|
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
|