cry-synced-db-client 0.1.180 → 0.1.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 0.1.182 (2026-05-14)
|
|
4
|
+
|
|
5
|
+
### `networkError` utility — quiet log noise during sustained offline
|
|
6
|
+
|
|
7
|
+
New `src/utils/networkError.ts`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export function networkError(message: string, ...rest: unknown[]): void {
|
|
11
|
+
const isOnline =
|
|
12
|
+
typeof navigator === "undefined" || navigator.onLine !== false;
|
|
13
|
+
if (isOnline) {
|
|
14
|
+
console.error(message, ...rest);
|
|
15
|
+
} else {
|
|
16
|
+
console.info(message, ...rest);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Applied to 15 sites where the failure mode reduces to "REST/WS
|
|
22
|
+
unreachable because the network is down":
|
|
23
|
+
|
|
24
|
+
| Module | Site |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `ConnectionManager` | Failed to go online after forceOffline release |
|
|
27
|
+
| `ConnectionManager` | Auto-sync failed |
|
|
28
|
+
| `ConnectionManager` | Reconnect tryGoOnline failed |
|
|
29
|
+
| `PendingChangesManager` | REST upload failed |
|
|
30
|
+
| `WakeSyncManager` | Wake sync (`<trigger>`) failed |
|
|
31
|
+
| `SyncEngine` | uploadDirtyItems failed (download succeeded) |
|
|
32
|
+
| `SyncEngine` | Sync failed |
|
|
33
|
+
| `SyncedDb` | tryGoOnline on becameLeader failed |
|
|
34
|
+
| `SyncedDb` | referToServer failed for `<collection>` |
|
|
35
|
+
| `SyncedDb` | refreshInBackground failed for `<collection>` |
|
|
36
|
+
| `SyncedDb` | Failed to hard delete `<id>` |
|
|
37
|
+
| `SyncedDb` | `[evict]` server-assisted pass failed |
|
|
38
|
+
| `SyncedDb` | `[evict]` server-assisted batch failed |
|
|
39
|
+
| `Ebus2ProxyNotifier` | WebSocket error: `<event.type>` |
|
|
40
|
+
| `Ebus2ProxyNotifier` | Reconnection failed |
|
|
41
|
+
|
|
42
|
+
Effect: a 5-minute offline period that previously produced ~25–30
|
|
43
|
+
`console.error` lines (auto-sync ticks every 60s, reconnect probes
|
|
44
|
+
every 60s, WS reconnect backoff, dirty-flush retries) now produces 0.
|
|
45
|
+
The events are still logged at `console.info` so an operator inspecting
|
|
46
|
+
them sees the full timeline — they just don't surface as "errors" in
|
|
47
|
+
sentry/log dashboards.
|
|
48
|
+
|
|
49
|
+
What's NOT routed through `networkError` (intentional — these are real
|
|
50
|
+
bugs regardless of network state):
|
|
51
|
+
- Caller bugs (falsy `_id`, missing `_id`, id mismatch, no id provided)
|
|
52
|
+
- Dexie failures (`bulkPut failed`, `Failed to write to Dexie`, etc.)
|
|
53
|
+
- Consumer-supplied callback throws (`onSyncEnd callback failed`, etc.)
|
|
54
|
+
- WS protocol errors (malformed msgpack, server-side error frames)
|
|
55
|
+
- Server-side per-item rejection (`Sync upload error [coll] _id=...`)
|
|
56
|
+
- `DB-WARNING` per-item warnings
|
|
57
|
+
- BroadcastChannel failures (cross-tab, local, not network)
|
|
58
|
+
|
|
59
|
+
Caveat: `navigator.onLine` only reports the OS network interface, not
|
|
60
|
+
server reachability. WiFi-on-but-server-unreachable still logs at error
|
|
61
|
+
severity, which is the correct behavior — that failure is unexpected
|
|
62
|
+
from the browser's point of view.
|
|
63
|
+
|
|
64
|
+
Bundle: 356.0 KB → 356.2 KB. Tests: 736 bun + 18 vitest still pass.
|
|
65
|
+
|
|
66
|
+
## 0.1.181 (2026-05-14)
|
|
67
|
+
|
|
68
|
+
### `measureEndToEndRtt` now rides `callWorker` over the WebSocket
|
|
69
|
+
|
|
70
|
+
The old HTTP echo path was a workaround for the missing WS
|
|
71
|
+
request/response correlation in ebus-proxy 1.x. Now that
|
|
72
|
+
`callWorker` (0.1.180) provides it, `measureEndToEndRtt` becomes a
|
|
73
|
+
5-line wrapper:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
async measureEndToEndRtt(timeoutMs = 5000): Promise<number> {
|
|
77
|
+
const sentAt = Date.now();
|
|
78
|
+
const t0 = performance.now();
|
|
79
|
+
const echoed = await this.callWorker<number>("echo", sentAt, { timeoutMs });
|
|
80
|
+
if (echoed !== sentAt) throw payload-mismatch error;
|
|
81
|
+
return performance.now() - t0;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Dropped HTTP-specific code: base URL derivation (`ws://`→`http://`),
|
|
86
|
+
base64 encoding (Buffer + btoa fallback), URL query-string assembly,
|
|
87
|
+
`AbortSignal.timeout` shim, `fetch` + `arrayBuffer` + `unpack` pipeline,
|
|
88
|
+
HTTP status error branch. Bundle: 356.9 KB → 356.0 KB.
|
|
89
|
+
|
|
90
|
+
**Semantic shift**: now measures the WebSocket round-trip rather than
|
|
91
|
+
the HTTP round-trip. For this codebase that's more representative —
|
|
92
|
+
real notifications arrive on the WS, not via HTTP. The old HTTP path
|
|
93
|
+
was characterizing a transport the app barely uses (sync uploads go to
|
|
94
|
+
cry-db directly, not through ebus-proxy).
|
|
95
|
+
|
|
96
|
+
Public signature is unchanged; existing callers keep working. The
|
|
97
|
+
diagnostic interpretation table in CLAUDE.md still applies — the
|
|
98
|
+
labels just refer to the WS chain instead of the HTTP chain.
|
|
99
|
+
|
|
100
|
+
Tests: 736 bun + 18 vitest, all green.
|
|
101
|
+
|
|
3
102
|
## 0.1.180 (2026-05-14)
|
|
4
103
|
|
|
5
104
|
### `SyncedDb.callWorker` — invoke ebus2 worker services over the WS
|
package/dist/index.js
CHANGED
|
@@ -732,6 +732,16 @@ function childPathForArrayElement(prefix, element, index) {
|
|
|
732
732
|
return prefix ? `${prefix}.${index}` : String(index);
|
|
733
733
|
}
|
|
734
734
|
|
|
735
|
+
// src/utils/networkError.ts
|
|
736
|
+
function networkError(message, ...rest) {
|
|
737
|
+
const isOnline = typeof navigator === "undefined" || navigator.onLine !== false;
|
|
738
|
+
if (isOnline) {
|
|
739
|
+
console.error(message, ...rest);
|
|
740
|
+
} else {
|
|
741
|
+
console.info(message, ...rest);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
735
745
|
// src/db/managers/InMemManager.ts
|
|
736
746
|
var InMemManager = class {
|
|
737
747
|
constructor(config) {
|
|
@@ -1422,7 +1432,7 @@ var ConnectionManager = class {
|
|
|
1422
1432
|
} else {
|
|
1423
1433
|
this.deps.tryBecomeLeader();
|
|
1424
1434
|
this.tryGoOnline().catch((err) => {
|
|
1425
|
-
|
|
1435
|
+
networkError(`[Connection] Failed to go online after forceOffline release: ${err}`, err);
|
|
1426
1436
|
});
|
|
1427
1437
|
}
|
|
1428
1438
|
}
|
|
@@ -1480,7 +1490,7 @@ var ConnectionManager = class {
|
|
|
1480
1490
|
this.autoSyncTimer = setInterval(() => {
|
|
1481
1491
|
if (this.forcedOffline || !this.online) return;
|
|
1482
1492
|
this.deps.sync(`interval ${intervalMs}ms`).catch((err) => {
|
|
1483
|
-
|
|
1493
|
+
networkError(`[Connection] Auto-sync failed: ${err}`, err);
|
|
1484
1494
|
});
|
|
1485
1495
|
}, intervalMs);
|
|
1486
1496
|
}
|
|
@@ -1489,7 +1499,7 @@ var ConnectionManager = class {
|
|
|
1489
1499
|
this.reconnectTimer = setInterval(() => {
|
|
1490
1500
|
if (this.forcedOffline || this.online || this.tryGoOnlineInFlight) return;
|
|
1491
1501
|
this.tryGoOnline().catch((err) => {
|
|
1492
|
-
|
|
1502
|
+
networkError(`[Connection] Reconnect tryGoOnline failed: ${err}`, err);
|
|
1493
1503
|
});
|
|
1494
1504
|
}, retryMs);
|
|
1495
1505
|
}
|
|
@@ -2827,7 +2837,7 @@ var PendingChangesManager = class {
|
|
|
2827
2837
|
try {
|
|
2828
2838
|
await this.deps.uploadDirtyItems();
|
|
2829
2839
|
} catch (err) {
|
|
2830
|
-
|
|
2840
|
+
networkError(`[PendingChanges] REST upload failed: ${err}`, err);
|
|
2831
2841
|
} finally {
|
|
2832
2842
|
this.isUploadingToRest = false;
|
|
2833
2843
|
resolveUpload();
|
|
@@ -3209,7 +3219,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3209
3219
|
}
|
|
3210
3220
|
}
|
|
3211
3221
|
} catch (err) {
|
|
3212
|
-
|
|
3222
|
+
networkError(
|
|
3213
3223
|
"[SyncEngine] uploadDirtyItems failed (download succeeded, staying online):",
|
|
3214
3224
|
err
|
|
3215
3225
|
);
|
|
@@ -3236,7 +3246,7 @@ var _SyncEngine = class _SyncEngine {
|
|
|
3236
3246
|
});
|
|
3237
3247
|
} catch (err) {
|
|
3238
3248
|
const reason = err instanceof Error ? err.message : String(err);
|
|
3239
|
-
|
|
3249
|
+
networkError(`[SyncEngine] Sync failed: ${err}`, err);
|
|
3240
3250
|
this.deps.onSyncFailed(`Sync failed: ${reason}`);
|
|
3241
3251
|
this.callOnSyncEnd({
|
|
3242
3252
|
durationMs: Date.now() - startTime,
|
|
@@ -4291,7 +4301,7 @@ var WakeSyncManager = class {
|
|
|
4291
4301
|
}
|
|
4292
4302
|
}
|
|
4293
4303
|
this.deps.sync(`wake-sync:${trigger}`).catch((err) => {
|
|
4294
|
-
|
|
4304
|
+
networkError(`[WakeSync] Wake sync (${trigger}) failed:`, err);
|
|
4295
4305
|
});
|
|
4296
4306
|
}, this.debounceMs);
|
|
4297
4307
|
}
|
|
@@ -4447,7 +4457,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
4447
4457
|
onBecameLeader: () => {
|
|
4448
4458
|
if (this.initialized && !this.connectionManager.isOnline() && !this.connectionManager.isForcedOffline()) {
|
|
4449
4459
|
this.connectionManager.tryGoOnline().catch((err) => {
|
|
4450
|
-
|
|
4460
|
+
networkError(`[SyncedDb] tryGoOnline on becameLeader failed: ${err}`, err);
|
|
4451
4461
|
});
|
|
4452
4462
|
}
|
|
4453
4463
|
if (config.onBecameLeader) {
|
|
@@ -5246,7 +5256,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5246
5256
|
await this.syncEngine.processCollectionServerData(collection, serverData, { source: "refresh" });
|
|
5247
5257
|
}
|
|
5248
5258
|
}).catch((err) => {
|
|
5249
|
-
|
|
5259
|
+
networkError(`[SyncedDb] referToServer failed for ${collection}:`, err);
|
|
5250
5260
|
});
|
|
5251
5261
|
}
|
|
5252
5262
|
/**
|
|
@@ -5267,7 +5277,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5267
5277
|
if (!serverItems || serverItems.length === 0) return;
|
|
5268
5278
|
await this.syncEngine.processCollectionServerData(collection, serverItems, { source: "incremental" });
|
|
5269
5279
|
}).catch((err) => {
|
|
5270
|
-
|
|
5280
|
+
networkError(`[SyncedDb] refreshInBackground failed for ${collection}:`, err);
|
|
5271
5281
|
});
|
|
5272
5282
|
}
|
|
5273
5283
|
async ensureItemsAreLoaded(collection, ids, withDeleted) {
|
|
@@ -5532,7 +5542,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5532
5542
|
this.inMemManager.writeBatch(collection, [{ _id: item.id }], "delete", { source: "incremental" });
|
|
5533
5543
|
results.push(true);
|
|
5534
5544
|
} catch (err) {
|
|
5535
|
-
|
|
5545
|
+
networkError(`[SyncedDb] Failed to hard delete ${String(item.id)}:`, err);
|
|
5536
5546
|
results.push(false);
|
|
5537
5547
|
}
|
|
5538
5548
|
}
|
|
@@ -5913,7 +5923,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
5913
5923
|
for (const id of serverExits) evictIds.push(id);
|
|
5914
5924
|
serverEvictedCount = serverExits.length;
|
|
5915
5925
|
} catch (err) {
|
|
5916
|
-
|
|
5926
|
+
networkError(
|
|
5917
5927
|
`[SyncedDb] [evict] server-assisted pass failed for ${collection} (proceeding with local-only):`,
|
|
5918
5928
|
err
|
|
5919
5929
|
);
|
|
@@ -6030,7 +6040,7 @@ var _SyncedDb = class _SyncedDb {
|
|
|
6030
6040
|
}
|
|
6031
6041
|
} catch (err) {
|
|
6032
6042
|
serverFailed = true;
|
|
6033
|
-
|
|
6043
|
+
networkError(
|
|
6034
6044
|
"[SyncedDb] [evict] server-assisted batch failed (proceeding with local-only):",
|
|
6035
6045
|
err
|
|
6036
6046
|
);
|
|
@@ -9845,41 +9855,22 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
9845
9855
|
}
|
|
9846
9856
|
/**
|
|
9847
9857
|
* End-to-end RTT (client → proxy → broker → echo worker → broker →
|
|
9848
|
-
* proxy → client).
|
|
9849
|
-
*
|
|
9850
|
-
*
|
|
9851
|
-
* reporting RTT.
|
|
9858
|
+
* proxy → client). Invokes the `echo` worker via `callWorker` with
|
|
9859
|
+
* `Date.now()` as the payload; the worker returns it unchanged and
|
|
9860
|
+
* we verify byte-equality before reporting RTT.
|
|
9852
9861
|
*
|
|
9853
|
-
*
|
|
9854
|
-
*
|
|
9855
|
-
* or
|
|
9862
|
+
* Rides the existing WebSocket — same transport real notifications
|
|
9863
|
+
* arrive on, so this is the more representative diagnostic of actual
|
|
9864
|
+
* app-traffic latency. Throws on payload mismatch, timeout, or any
|
|
9865
|
+
* `callWorker` failure path (WS not OPEN, proxy error, etc).
|
|
9856
9866
|
*
|
|
9857
|
-
* @param timeoutMs Max wait for
|
|
9867
|
+
* @param timeoutMs Max wait for echo reply (default: 5000)
|
|
9858
9868
|
* @returns RTT in milliseconds (full round-trip)
|
|
9859
9869
|
*/
|
|
9860
9870
|
async measureEndToEndRtt(timeoutMs = 5e3) {
|
|
9861
|
-
const httpBase = this.wsUrl.replace(/^ws:\/\//, "http://").replace(/^wss:\/\//, "https://").replace(/\/+$/, "");
|
|
9862
9871
|
const sentAt = Date.now();
|
|
9863
|
-
const packed = packr2.pack(sentAt);
|
|
9864
|
-
let payloadB64;
|
|
9865
|
-
const bufCtor = globalThis.Buffer;
|
|
9866
|
-
if (bufCtor) {
|
|
9867
|
-
payloadB64 = bufCtor.from(packed).toString("base64");
|
|
9868
|
-
} else {
|
|
9869
|
-
let bin = "";
|
|
9870
|
-
for (let i = 0; i < packed.length; i++) bin += String.fromCharCode(packed[i]);
|
|
9871
|
-
payloadB64 = btoa(bin);
|
|
9872
|
-
}
|
|
9873
|
-
const keyParam = this.ebusProxyApiKey ? `&apikey=${encodeURIComponent(this.ebusProxyApiKey)}` : "";
|
|
9874
|
-
const url = `${httpBase}/?service=echo&payload=${encodeURIComponent(payloadB64)}${keyParam}&timeout=${timeoutMs}`;
|
|
9875
9872
|
const t0 = performance.now();
|
|
9876
|
-
const
|
|
9877
|
-
const res = await fetch(url, signal ? { signal } : void 0);
|
|
9878
|
-
if (!res.ok) {
|
|
9879
|
-
throw new Error(`[Ebus2ProxyNotifier] measureEndToEndRtt: HTTP ${res.status}`);
|
|
9880
|
-
}
|
|
9881
|
-
const buf = new Uint8Array(await res.arrayBuffer());
|
|
9882
|
-
const echoed = unpackr2.unpack(buf);
|
|
9873
|
+
const echoed = await this.callWorker("echo", sentAt, { timeoutMs });
|
|
9883
9874
|
if (echoed !== sentAt) {
|
|
9884
9875
|
throw new Error(
|
|
9885
9876
|
`[Ebus2ProxyNotifier] measureEndToEndRtt: payload mismatch (sent ${sentAt}, got ${JSON.stringify(echoed)})`
|
|
@@ -10132,7 +10123,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
10132
10123
|
}
|
|
10133
10124
|
}
|
|
10134
10125
|
handleError(event) {
|
|
10135
|
-
|
|
10126
|
+
networkError(`[Ebus2ProxyNotifier] WebSocket error: ${event.type}`, event);
|
|
10136
10127
|
}
|
|
10137
10128
|
handleMessage(event) {
|
|
10138
10129
|
try {
|
|
@@ -10253,7 +10244,7 @@ var Ebus2ProxyServerUpdateNotifier = class {
|
|
|
10253
10244
|
this.reconnectTimer = void 0;
|
|
10254
10245
|
if (this.shouldReconnect && !this.forcedOffline) {
|
|
10255
10246
|
this.createWebSocket().catch((err) => {
|
|
10256
|
-
|
|
10247
|
+
networkError(`[Ebus2ProxyNotifier] Reconnection failed: ${err}`, err);
|
|
10257
10248
|
this.currentReconnectDelay = Math.min(
|
|
10258
10249
|
this.currentReconnectDelay * 2,
|
|
10259
10250
|
this.maxReconnectDelayMs
|
|
@@ -104,16 +104,16 @@ export declare class Ebus2ProxyServerUpdateNotifier implements I_ServerUpdateNot
|
|
|
104
104
|
measureWsRtt(timeoutMs?: number): Promise<number>;
|
|
105
105
|
/**
|
|
106
106
|
* End-to-end RTT (client → proxy → broker → echo worker → broker →
|
|
107
|
-
* proxy → client).
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* reporting RTT.
|
|
107
|
+
* proxy → client). Invokes the `echo` worker via `callWorker` with
|
|
108
|
+
* `Date.now()` as the payload; the worker returns it unchanged and
|
|
109
|
+
* we verify byte-equality before reporting RTT.
|
|
111
110
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* or
|
|
111
|
+
* Rides the existing WebSocket — same transport real notifications
|
|
112
|
+
* arrive on, so this is the more representative diagnostic of actual
|
|
113
|
+
* app-traffic latency. Throws on payload mismatch, timeout, or any
|
|
114
|
+
* `callWorker` failure path (WS not OPEN, proxy error, etc).
|
|
115
115
|
*
|
|
116
|
-
* @param timeoutMs Max wait for
|
|
116
|
+
* @param timeoutMs Max wait for echo reply (default: 5000)
|
|
117
117
|
* @returns RTT in milliseconds (full round-trip)
|
|
118
118
|
*/
|
|
119
119
|
measureEndToEndRtt(timeoutMs?: number): Promise<number>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log a failure at appropriate severity depending on perceived network
|
|
3
|
+
* connectivity.
|
|
4
|
+
*
|
|
5
|
+
* - **Online** (`navigator.onLine === true`, or `navigator` unavailable
|
|
6
|
+
* like in Node/SSR): routes to `console.error`. Failures while the
|
|
7
|
+
* browser thinks it's online are unexpected — operators should see
|
|
8
|
+
* them.
|
|
9
|
+
* - **Offline** (`navigator.onLine === false`): routes to
|
|
10
|
+
* `console.info`. Sync/upload/reconnect failures are the expected
|
|
11
|
+
* steady-state and shouldn't pollute the error stream.
|
|
12
|
+
*
|
|
13
|
+
* Use this ONLY for sites whose failure reduces to "the network is
|
|
14
|
+
* down" — caller bugs, Dexie failures, parse errors, and consumer
|
|
15
|
+
* callback throws must keep using `console.error` directly so they're
|
|
16
|
+
* visible regardless of connectivity.
|
|
17
|
+
*
|
|
18
|
+
* Signature mirrors `console.error` — first arg is the tag-line string
|
|
19
|
+
* (per the console-reporting skill), remaining args are objects /
|
|
20
|
+
* values for devtools inspection.
|
|
21
|
+
*
|
|
22
|
+
* Caveat: `navigator.onLine` only tells you whether the browser has a
|
|
23
|
+
* network interface; it can't detect "WiFi connected but server
|
|
24
|
+
* unreachable". In that case this still routes to `console.error`,
|
|
25
|
+
* which is correct — the failure is unexpected from the browser's
|
|
26
|
+
* point of view.
|
|
27
|
+
*/
|
|
28
|
+
export declare function networkError(message: string, ...rest: unknown[]): void;
|