cry-synced-db-client 0.1.174 → 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 +174 -0
- package/dist/index.js +239 -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 +42 -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 +60 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,180 @@
|
|
|
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
|
+
|
|
107
|
+
### `save(coll, id, {field: {}})` clears existing nested children
|
|
108
|
+
|
|
109
|
+
`computeDiffInto` for plain objects iterated only `Object.keys(update)`,
|
|
110
|
+
so an update like `{cepljenja: {}}` against an existing
|
|
111
|
+
`{cepljenja: {<id>: {…}}}` emitted **zero diff entries** — children were
|
|
112
|
+
preserved silently. Empty-object value is now treated as a full replace
|
|
113
|
+
of the field (symmetric with `{field: []}` array replace and
|
|
114
|
+
`{field: undefined}` delete).
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// existing.cepljenja = { "<id>": { …data } }
|
|
118
|
+
await syncedDb.save("pacienti", id, { cepljenja: {} });
|
|
119
|
+
// after: pacient.cepljenja === {} — children physically removed in
|
|
120
|
+
// in-mem, Dexie, and the dirty payload
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Implementation in `src/utils/computeDiff.ts:computeDiffInto`: when both
|
|
124
|
+
sides are plain objects and `update` has zero keys while `existing` has
|
|
125
|
+
some, emit `diff[basePath] = {}` instead of falling through to the
|
|
126
|
+
"iterate update keys" loop. The earlier `deepEquals(existing, update)`
|
|
127
|
+
guard still short-circuits the `{} → {}` no-op case.
|
|
128
|
+
|
|
129
|
+
Regression test: `test/saveEmptyObjectClearsChildren.test.ts` (4 cases:
|
|
130
|
+
in-mem clear, Dexie clear, dirty payload emits wipe, sibling fields
|
|
131
|
+
preserved). 721 pass / 0 fail.
|
|
132
|
+
|
|
133
|
+
### `findById` / `findByIds` auto-register unconfigured collections as temporary
|
|
134
|
+
|
|
135
|
+
Calling `findById(collection, id)` or `findByIds(collection, ids)` for a
|
|
136
|
+
collection NOT in the runtime sync config (e.g. boot-time `collections: [...]`)
|
|
137
|
+
no longer throws. Instead the collection is auto-registered as
|
|
138
|
+
**temporary** with `syncConfig.query: () => ({_id: {$in: [<ids>]}})`. The
|
|
139
|
+
call then proceeds through the normal flow — `referToServer` (default
|
|
140
|
+
`true`) loads the row from the server on cache miss and returns it.
|
|
141
|
+
|
|
142
|
+
Behavior on subsequent calls (inspected against the existing config's
|
|
143
|
+
`syncConfig.query` shape, no extra bookkeeping state):
|
|
144
|
+
|
|
145
|
+
| Existing config | Action |
|
|
146
|
+
|---|---|
|
|
147
|
+
| none | install `{_id: {$in: [<ids>]}}` (static object) |
|
|
148
|
+
| temporary, query matches `{_id: {$in: [...]}}` | append novel ids to the existing `$in` array |
|
|
149
|
+
| temporary, query is a function / different shape / absent | skip — leave alone |
|
|
150
|
+
| permanent | skip — never touch a permanent config |
|
|
151
|
+
|
|
152
|
+
`replaceSyncCollection` naturally resets accumulation by overwriting the
|
|
153
|
+
spec; the new config (whatever shape) drives future syncs alone.
|
|
154
|
+
|
|
155
|
+
Constraints:
|
|
156
|
+
- Dexie schema must already declare the table (Dexie does not support
|
|
157
|
+
adding tables to an open database). The auto-register handles only the
|
|
158
|
+
runtime SyncedDb-level config.
|
|
159
|
+
- An active `syncOnlyTheseCollections` filter (non-null — set via
|
|
160
|
+
`setSyncOnlyTheseCollections([…])` with at least one entry) is extended
|
|
161
|
+
to include the new temp collection so it participates in future sync
|
|
162
|
+
ticks. When no filter is set (sync-all mode), this step is a no-op.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// No runtime config for "zivali" — boot only registers "racuni".
|
|
166
|
+
new SyncedDb({ collections: [{ name: "racuni" }], dexieDb /* has zivali */, ... });
|
|
167
|
+
|
|
168
|
+
// Previously: throws "Collection 'zivali' not configured".
|
|
169
|
+
// Now: auto-registers as temporary, fetches via referToServer, returns.
|
|
170
|
+
const zival = await syncedDb.findById("zivali", id);
|
|
171
|
+
|
|
172
|
+
// Upgrade to permanent when the app wires up real sync:
|
|
173
|
+
await syncedDb.replaceSyncCollection({
|
|
174
|
+
name: "zivali",
|
|
175
|
+
syncConfig: { query: () => ({ vrsta: "pes" }) },
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
5
179
|
### `preprocessDirtyItem` callback — per-item filter / transform before upload
|
|
6
180
|
|
|
7
181
|
New optional config callback fired for **every** dirty item just before it
|
package/dist/index.js
CHANGED
|
@@ -440,6 +440,10 @@ function computeDiffInto(existing, update, basePath, diff) {
|
|
|
440
440
|
diff[basePath] = update;
|
|
441
441
|
return;
|
|
442
442
|
}
|
|
443
|
+
if (Object.keys(update).length === 0 && Object.keys(existing).length > 0) {
|
|
444
|
+
diff[basePath] = update;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
443
447
|
for (const key of Object.keys(update)) {
|
|
444
448
|
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
445
449
|
computeDiffInto(existing[key], update[key], childPath, diff);
|
|
@@ -3079,6 +3083,9 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3079
3083
|
source: isInitial ? "initial" : "incremental"
|
|
3080
3084
|
});
|
|
3081
3085
|
}
|
|
3086
|
+
let requestBytes;
|
|
3087
|
+
let responseBytes;
|
|
3088
|
+
let ttfbMs;
|
|
3082
3089
|
try {
|
|
3083
3090
|
const completedCollections = /* @__PURE__ */ new Set();
|
|
3084
3091
|
const allSpecs = extras && extras.specs.length > 0 ? [...syncSpecs, ...extras.specs] : syncSpecs;
|
|
@@ -3114,6 +3121,17 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3114
3121
|
items: items.length
|
|
3115
3122
|
});
|
|
3116
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
|
+
}
|
|
3117
3135
|
}
|
|
3118
3136
|
),
|
|
3119
3137
|
"findNewerManyStream"
|
|
@@ -3126,7 +3144,15 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3126
3144
|
sentCount: 0
|
|
3127
3145
|
};
|
|
3128
3146
|
}
|
|
3129
|
-
this.callOnFindNewerManyResult(
|
|
3147
|
+
this.callOnFindNewerManyResult(
|
|
3148
|
+
syncSpecs,
|
|
3149
|
+
{},
|
|
3150
|
+
findNewerManyStartTime,
|
|
3151
|
+
true,
|
|
3152
|
+
calledFrom,
|
|
3153
|
+
void 0,
|
|
3154
|
+
{ requestBytes, responseBytes, ttfbMs }
|
|
3155
|
+
);
|
|
3130
3156
|
this.callbackSafe(this.callbacks.onServerSyncEnd, {
|
|
3131
3157
|
calledFrom,
|
|
3132
3158
|
collectionCount: syncSpecs.length,
|
|
@@ -3135,7 +3161,15 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3135
3161
|
success: true
|
|
3136
3162
|
});
|
|
3137
3163
|
} catch (err) {
|
|
3138
|
-
this.callOnFindNewerManyResult(
|
|
3164
|
+
this.callOnFindNewerManyResult(
|
|
3165
|
+
syncSpecs,
|
|
3166
|
+
{},
|
|
3167
|
+
findNewerManyStartTime,
|
|
3168
|
+
false,
|
|
3169
|
+
calledFrom,
|
|
3170
|
+
err,
|
|
3171
|
+
{ requestBytes, responseBytes, ttfbMs }
|
|
3172
|
+
);
|
|
3139
3173
|
this.callbackSafe(this.callbacks.onServerSyncEnd, {
|
|
3140
3174
|
calledFrom,
|
|
3141
3175
|
collectionCount: syncSpecs.length,
|
|
@@ -3795,7 +3829,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3795
3829
|
}
|
|
3796
3830
|
}
|
|
3797
3831
|
}
|
|
3798
|
-
callOnFindNewerManyResult(specs, results, startTime, success, calledFrom, error) {
|
|
3832
|
+
callOnFindNewerManyResult(specs, results, startTime, success, calledFrom, error, metrics) {
|
|
3799
3833
|
if (this.callbacks.onFindNewerManyResult) {
|
|
3800
3834
|
try {
|
|
3801
3835
|
this.callbacks.onFindNewerManyResult({
|
|
@@ -3804,7 +3838,10 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3804
3838
|
durationMs: Date.now() - startTime,
|
|
3805
3839
|
success,
|
|
3806
3840
|
error: error instanceof Error ? error : error ? new Error(String(error)) : void 0,
|
|
3807
|
-
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
|
|
3808
3845
|
});
|
|
3809
3846
|
} catch (err) {
|
|
3810
3847
|
console.error("[SyncEngine] onFindNewerManyResult callback failed:", err);
|
|
@@ -4567,6 +4604,36 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4567
4604
|
followerSince() {
|
|
4568
4605
|
return this.leaderElection.followerSince();
|
|
4569
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
|
+
}
|
|
4570
4637
|
/**
|
|
4571
4638
|
* Register a collection for sync at runtime. See `I_SyncedDb.addCollectionToSync`.
|
|
4572
4639
|
*/
|
|
@@ -4871,6 +4938,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4871
4938
|
// ==================== Read Operations ====================
|
|
4872
4939
|
async findById(collection, id, opts) {
|
|
4873
4940
|
var _a;
|
|
4941
|
+
this._autoRegisterTemporaryForFind(collection, [id]);
|
|
4874
4942
|
this.assertCollection(collection);
|
|
4875
4943
|
if (!id) {
|
|
4876
4944
|
const err = new Error(`[SyncedDb] findById ${collection} no id ${id}`);
|
|
@@ -4928,6 +4996,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4928
4996
|
}
|
|
4929
4997
|
async findByIds(collection, ids, opts) {
|
|
4930
4998
|
var _a;
|
|
4999
|
+
this._autoRegisterTemporaryForFind(collection, ids);
|
|
4931
5000
|
this.assertCollection(collection);
|
|
4932
5001
|
ids = ids.map((id) => this.normalizeId(id, "findByIds", collection));
|
|
4933
5002
|
opts = this.resolveOpts(opts);
|
|
@@ -6331,6 +6400,68 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6331
6400
|
throw new Error(`SyncedDb: Collection "${(name == null ? void 0 : name.toString()) || "?"}" not configured`);
|
|
6332
6401
|
}
|
|
6333
6402
|
}
|
|
6403
|
+
/**
|
|
6404
|
+
* Auto-register an unconfigured collection as TEMPORARY when `findById` /
|
|
6405
|
+
* `findByIds` is called for it. The collection becomes part of the sync
|
|
6406
|
+
* scope with a `{_id: {$in: <ids>}}` static query so future sync ticks
|
|
6407
|
+
* keep those rows fresh.
|
|
6408
|
+
*
|
|
6409
|
+
* Behavior matrix:
|
|
6410
|
+
*
|
|
6411
|
+
* | Existing config | Action |
|
|
6412
|
+
* |---|---|
|
|
6413
|
+
* | none | install temp with `query: {_id: {$in: [<ids>]}}` |
|
|
6414
|
+
* | temporary, query is `{_id: {$in: [...]}}` | append novel `<ids>` to the existing `$in` array |
|
|
6415
|
+
* | temporary, query is anything else (function, different shape, missing) | skip — leave config untouched |
|
|
6416
|
+
* | permanent | skip — never alter a permanent config |
|
|
6417
|
+
*
|
|
6418
|
+
* No extra state map — accumulation lives in the config's own `$in`
|
|
6419
|
+
* array. `replaceSyncCollection` (or any other path that installs a new
|
|
6420
|
+
* config) naturally resets accumulation by overwriting the config.
|
|
6421
|
+
*
|
|
6422
|
+
* Cheap fast paths: synchronous, no Dexie/server I/O. The regular
|
|
6423
|
+
* `findById` flow (in-mem cache → `referToServer` → `ensureItemsAreLoaded`)
|
|
6424
|
+
* handles the actual data load for THIS call.
|
|
6425
|
+
*
|
|
6426
|
+
* If an `syncOnlyTheseCollections` filter is active (non-null, i.e.
|
|
6427
|
+
* `setSyncOnlyTheseCollections([…])` was called with at least one
|
|
6428
|
+
* entry), a newly-installed temp collection is added to the filter so
|
|
6429
|
+
* it participates in future sync ticks. When no filter is set
|
|
6430
|
+
* (sync-all mode), this step is a no-op.
|
|
6431
|
+
*/
|
|
6432
|
+
_autoRegisterTemporaryForFind(name, ids) {
|
|
6433
|
+
var _a;
|
|
6434
|
+
const idStrings = ids.map((id) => String(id));
|
|
6435
|
+
const existing = this.collections.get(name);
|
|
6436
|
+
if (!existing) {
|
|
6437
|
+
this.collections.set(name, {
|
|
6438
|
+
name,
|
|
6439
|
+
temporaryConfig: true,
|
|
6440
|
+
syncConfig: {
|
|
6441
|
+
query: { _id: { $in: idStrings } }
|
|
6442
|
+
}
|
|
6443
|
+
});
|
|
6444
|
+
if (this.syncOnlyCollections) {
|
|
6445
|
+
this.syncOnlyCollections.add(name);
|
|
6446
|
+
}
|
|
6447
|
+
return;
|
|
6448
|
+
}
|
|
6449
|
+
if (!existing.temporaryConfig) return;
|
|
6450
|
+
const query = (_a = existing.syncConfig) == null ? void 0 : _a.query;
|
|
6451
|
+
if (typeof query !== "object" || query === null) return;
|
|
6452
|
+
const idField = query._id;
|
|
6453
|
+
if (typeof idField !== "object" || idField === null) return;
|
|
6454
|
+
const inArr = idField.$in;
|
|
6455
|
+
if (!Array.isArray(inArr)) return;
|
|
6456
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6457
|
+
for (const existingId of inArr) seen.add(String(existingId));
|
|
6458
|
+
for (const id of idStrings) {
|
|
6459
|
+
if (!seen.has(id)) {
|
|
6460
|
+
inArr.push(id);
|
|
6461
|
+
seen.add(id);
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6334
6465
|
/** Stringify an Id parameter (ObjectId → hex string). */
|
|
6335
6466
|
normalizeId(id, method, collection) {
|
|
6336
6467
|
if (!id && id !== void 0) {
|
|
@@ -9279,11 +9410,12 @@ var RestProxy = class {
|
|
|
9279
9410
|
* type=0x01 for data, type=0x00 for end-of-stream.
|
|
9280
9411
|
*/
|
|
9281
9412
|
async findNewerManyStream(spec, onChunk, options) {
|
|
9282
|
-
var _a, _b, _c;
|
|
9413
|
+
var _a, _b, _c, _d, _e;
|
|
9283
9414
|
const connectTimeout = (_a = options == null ? void 0 : options.timeoutMs) != null ? _a : this.defaultTimeoutMs;
|
|
9284
9415
|
const activityTimeout = (_b = options == null ? void 0 : options.activityTimeoutMs) != null ? _b : 3e4;
|
|
9285
9416
|
const externalSignal = (_c = options == null ? void 0 : options.signal) != null ? _c : this.globalSignal;
|
|
9286
9417
|
const startTime = this.timeRequests ? performance.now() : 0;
|
|
9418
|
+
const fetchStart = performance.now();
|
|
9287
9419
|
const data = {
|
|
9288
9420
|
payload: {
|
|
9289
9421
|
db: this.tenant,
|
|
@@ -9297,6 +9429,7 @@ var RestProxy = class {
|
|
|
9297
9429
|
}
|
|
9298
9430
|
};
|
|
9299
9431
|
const body = pack2(data);
|
|
9432
|
+
(_d = options == null ? void 0 : options.onRequestBytes) == null ? void 0 : _d.call(options, body.byteLength);
|
|
9300
9433
|
const requestUrl = this.apiKey ? `${this.endpoint}?apikey=${this.apiKey}&stream=1` : `${this.endpoint}?stream=1`;
|
|
9301
9434
|
const controller = new AbortController();
|
|
9302
9435
|
let timeoutId = setTimeout(
|
|
@@ -9311,6 +9444,7 @@ var RestProxy = class {
|
|
|
9311
9444
|
body,
|
|
9312
9445
|
signal: combinedSignal
|
|
9313
9446
|
});
|
|
9447
|
+
(_e = options == null ? void 0 : options.onTtfbMs) == null ? void 0 : _e.call(options, performance.now() - fetchStart);
|
|
9314
9448
|
clearTimeout(timeoutId);
|
|
9315
9449
|
timeoutId = void 0;
|
|
9316
9450
|
if (!response.ok) {
|
|
@@ -9322,7 +9456,12 @@ var RestProxy = class {
|
|
|
9322
9456
|
timeoutId = setTimeout(() => controller.abort(), activityTimeout);
|
|
9323
9457
|
};
|
|
9324
9458
|
resetActivity();
|
|
9325
|
-
await this.parseStreamingResponse(
|
|
9459
|
+
await this.parseStreamingResponse(
|
|
9460
|
+
response,
|
|
9461
|
+
onChunk,
|
|
9462
|
+
resetActivity,
|
|
9463
|
+
options == null ? void 0 : options.onResponseChunkBytes
|
|
9464
|
+
);
|
|
9326
9465
|
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
9327
9466
|
timeoutId = void 0;
|
|
9328
9467
|
if (this.timeRequests) {
|
|
@@ -9360,7 +9499,7 @@ var RestProxy = class {
|
|
|
9360
9499
|
*
|
|
9361
9500
|
* `onChunk` receives `specId` as the third arg for type-0x02 frames; `undefined` otherwise.
|
|
9362
9501
|
*/
|
|
9363
|
-
async parseStreamingResponse(response, onChunk, onActivity) {
|
|
9502
|
+
async parseStreamingResponse(response, onChunk, onActivity, onChunkBytes) {
|
|
9364
9503
|
const reader = response.body.getReader();
|
|
9365
9504
|
const buffer = new StreamBuffer();
|
|
9366
9505
|
const decoder2 = new TextDecoder();
|
|
@@ -9368,6 +9507,7 @@ var RestProxy = class {
|
|
|
9368
9507
|
const { done, value } = await reader.read();
|
|
9369
9508
|
if (done) return false;
|
|
9370
9509
|
onActivity();
|
|
9510
|
+
if (onChunkBytes) onChunkBytes(value.byteLength);
|
|
9371
9511
|
buffer.append(value);
|
|
9372
9512
|
return true;
|
|
9373
9513
|
};
|
|
@@ -9502,6 +9642,13 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
9502
9642
|
this.reconnectAttempt = 0;
|
|
9503
9643
|
this.forcedOffline = false;
|
|
9504
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();
|
|
9505
9652
|
var _a, _b, _c, _d, _e;
|
|
9506
9653
|
this.endpoint = config.wsUrl;
|
|
9507
9654
|
this.wsUrl = config.wsUrl;
|
|
@@ -9557,10 +9704,88 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
9557
9704
|
this.onWsConnectCallbacks.length = 0;
|
|
9558
9705
|
this.onWsDisconnectCallbacks.length = 0;
|
|
9559
9706
|
this.onWsReconnectCallbacks.length = 0;
|
|
9707
|
+
this._pendingRttPings.clear();
|
|
9560
9708
|
}
|
|
9561
9709
|
isConnected() {
|
|
9562
9710
|
return this.connected && !this.forcedOffline;
|
|
9563
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
|
+
}
|
|
9564
9789
|
/**
|
|
9565
9790
|
* Set connection lifecycle callbacks.
|
|
9566
9791
|
* These are merged with any callbacks provided in the constructor config.
|
|
@@ -9707,6 +9932,13 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
9707
9932
|
break;
|
|
9708
9933
|
case "pong":
|
|
9709
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
|
+
}
|
|
9710
9942
|
break;
|
|
9711
9943
|
case "error":
|
|
9712
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
|
*/
|
|
@@ -426,6 +438,36 @@ export declare class SyncedDb implements I_SyncedDb {
|
|
|
426
438
|
*/
|
|
427
439
|
private preloadAllSyncMetas;
|
|
428
440
|
private assertCollection;
|
|
441
|
+
/**
|
|
442
|
+
* Auto-register an unconfigured collection as TEMPORARY when `findById` /
|
|
443
|
+
* `findByIds` is called for it. The collection becomes part of the sync
|
|
444
|
+
* scope with a `{_id: {$in: <ids>}}` static query so future sync ticks
|
|
445
|
+
* keep those rows fresh.
|
|
446
|
+
*
|
|
447
|
+
* Behavior matrix:
|
|
448
|
+
*
|
|
449
|
+
* | Existing config | Action |
|
|
450
|
+
* |---|---|
|
|
451
|
+
* | none | install temp with `query: {_id: {$in: [<ids>]}}` |
|
|
452
|
+
* | temporary, query is `{_id: {$in: [...]}}` | append novel `<ids>` to the existing `$in` array |
|
|
453
|
+
* | temporary, query is anything else (function, different shape, missing) | skip — leave config untouched |
|
|
454
|
+
* | permanent | skip — never alter a permanent config |
|
|
455
|
+
*
|
|
456
|
+
* No extra state map — accumulation lives in the config's own `$in`
|
|
457
|
+
* array. `replaceSyncCollection` (or any other path that installs a new
|
|
458
|
+
* config) naturally resets accumulation by overwriting the config.
|
|
459
|
+
*
|
|
460
|
+
* Cheap fast paths: synchronous, no Dexie/server I/O. The regular
|
|
461
|
+
* `findById` flow (in-mem cache → `referToServer` → `ensureItemsAreLoaded`)
|
|
462
|
+
* handles the actual data load for THIS call.
|
|
463
|
+
*
|
|
464
|
+
* If an `syncOnlyTheseCollections` filter is active (non-null, i.e.
|
|
465
|
+
* `setSyncOnlyTheseCollections([…])` was called with at least one
|
|
466
|
+
* entry), a newly-installed temp collection is added to the filter so
|
|
467
|
+
* it participates in future sync ticks. When no filter is set
|
|
468
|
+
* (sync-all mode), this step is a no-op.
|
|
469
|
+
*/
|
|
470
|
+
private _autoRegisterTemporaryForFind;
|
|
429
471
|
private static readonly STRINGIFIED_FALSY;
|
|
430
472
|
/** Stringify an Id parameter (ObjectId → hex string). */
|
|
431
473
|
private normalizeId;
|
|
@@ -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)
|
|
@@ -864,6 +885,13 @@ export interface I_SyncedDb {
|
|
|
864
885
|
* jih v ozadju revalidira s serverja (Dexie + in-mem skozi konflikt
|
|
865
886
|
* resolucijo). Ortogonalno do referToServer; ne povzroči duplikata
|
|
866
887
|
* server klicev za missing ID-je.
|
|
888
|
+
*
|
|
889
|
+
* Auto-registracija: če `collection` ni registrirana v runtime sync
|
|
890
|
+
* configu, se avtomatsko doda kot **temporary** s queryjem
|
|
891
|
+
* `{_id: {$in: [<id>]}}`. Dexie schema mora že imeti tabelo
|
|
892
|
+
* (Dexie ne podpira dodajanja tabel ob runtime-u). Klic nato teče
|
|
893
|
+
* skozi normalen findById flow — `referToServer` (privzeto `true`)
|
|
894
|
+
* naloži zapis s serverja ob cache miss-u.
|
|
867
895
|
*/
|
|
868
896
|
findById<T extends DbEntity>(collection: string, id: Id, opts?: QueryOpts): Promise<T | null>;
|
|
869
897
|
/**
|
|
@@ -873,6 +901,11 @@ export interface I_SyncedDb {
|
|
|
873
901
|
* jih v ozadju revalidira s serverja (Dexie + in-mem skozi konflikt
|
|
874
902
|
* resolucijo). Ortogonalno do referToServer; ne povzroči duplikata
|
|
875
903
|
* server klicev za missing ID-je.
|
|
904
|
+
*
|
|
905
|
+
* Auto-registracija: enako kot `findById` — če `collection` ni v
|
|
906
|
+
* runtime sync configu, se doda kot temporary s queryjem
|
|
907
|
+
* `{_id: {$in: [<ids>]}}` (vsi ID-ji iz klica). Dexie schema mora že
|
|
908
|
+
* imeti tabelo.
|
|
876
909
|
*/
|
|
877
910
|
findByIds<T extends DbEntity>(collection: string, ids: Id[], opts?: QueryOpts): Promise<T[]>;
|
|
878
911
|
/**
|
|
@@ -1058,6 +1091,33 @@ export interface I_SyncedDb {
|
|
|
1058
1091
|
* @returns Date of follower transition, or undefined if currently the leader
|
|
1059
1092
|
*/
|
|
1060
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>;
|
|
1061
1121
|
/**
|
|
1062
1122
|
* Get metadata for a single object.
|
|
1063
1123
|
* @param collection Collection name
|