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
- console.error(`[Connection] Failed to go online after forceOffline release: ${err}`, err);
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
- console.error(`[Connection] Auto-sync failed: ${err}`, err);
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
- console.error(`[Connection] Reconnect tryGoOnline failed: ${err}`, err);
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
- console.error(`[PendingChanges] REST upload failed: ${err}`, err);
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
- console.error(
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
- console.error(`[SyncEngine] Sync failed: ${err}`, err);
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
- console.error(`[WakeSync] Wake sync (${trigger}) failed:`, err);
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
- console.error(`[SyncedDb] tryGoOnline on becameLeader failed: ${err}`, err);
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
- console.error(`[SyncedDb] referToServer failed for ${collection}:`, err);
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
- console.error(`[SyncedDb] refreshInBackground failed for ${collection}:`, err);
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
- console.error(`[SyncedDb] Failed to hard delete ${String(item.id)}:`, err);
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
- console.error(
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
- console.error(
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). Sends an HTTP request to ebus-proxy's `echo`
9849
- * service with `Date.now()` as the msgpack payload; the worker
9850
- * returns the payload unchanged, and we verify byte-equality before
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
- * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
9854
- * `wss://` `https://`). Throws on HTTP error, payload mismatch,
9855
- * or timeout.
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 HTTP response (default: 5000)
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 signal = typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : void 0;
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
- console.error(`[Ebus2ProxyNotifier] WebSocket error: ${event.type}`, event);
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
- console.error(`[Ebus2ProxyNotifier] Reconnection failed: ${err}`, err);
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). Sends an HTTP request to ebus-proxy's `echo`
108
- * service with `Date.now()` as the msgpack payload; the worker
109
- * returns the payload unchanged, and we verify byte-equality before
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
- * The HTTP base URL is derived from `wsUrl` (`ws://` → `http://`,
113
- * `wss://` `https://`). Throws on HTTP error, payload mismatch,
114
- * or timeout.
111
+ * Rides the existing WebSocket same transport real notifications
112
+ * arrive on, so this is the more representative diagnostic of actual
113
+ * app-traffic latency. Throws on payload mismatch, timeout, or any
114
+ * `callWorker` failure path (WS not OPEN, proxy error, etc).
115
115
  *
116
- * @param timeoutMs Max wait for HTTP response (default: 5000)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.180",
3
+ "version": "0.1.182",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",