@g9tm/syncdata 1.5.0 → 1.5.2

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.
Files changed (59) hide show
  1. package/LICENSE.md +21 -21
  2. package/PARITY.md +96 -96
  3. package/README.md +71 -71
  4. package/THIRD-PARTY-NOTICES.md +31 -31
  5. package/dist/abstractions/digest.js.map +1 -1
  6. package/dist/abstractions/enums.js.map +1 -1
  7. package/dist/abstractions/errors.js.map +1 -1
  8. package/dist/abstractions/guid.js.map +1 -1
  9. package/dist/abstractions/key.js.map +1 -1
  10. package/dist/abstractions/long.js.map +1 -1
  11. package/dist/abstractions/parameters.js.map +1 -1
  12. package/dist/brotli-js/decode.js.map +1 -1
  13. package/dist/brotli-js/index.js.map +1 -1
  14. package/dist/client/engine.js.map +1 -1
  15. package/dist/client/model.js.map +1 -1
  16. package/dist/client/progress.js.map +1 -1
  17. package/dist/client/runs.js.map +1 -1
  18. package/dist/client/storeTypes.js.map +1 -1
  19. package/dist/client/transport.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/abort.js.map +1 -1
  24. package/dist/internal/base64.js.map +1 -1
  25. package/dist/internal/dotnetFormat.js.map +1 -1
  26. package/dist/internal/md5.js.map +1 -1
  27. package/dist/internal/sha256.js.map +1 -1
  28. package/dist/internal/utf8.js.map +1 -1
  29. package/dist/node/index.js.map +1 -1
  30. package/dist/node/snapshotFileSink.js.map +1 -1
  31. package/dist/node/sqliteDriver.js.map +1 -1
  32. package/dist/protocol/dtos.js.map +1 -1
  33. package/dist/protocol/protocol.js.map +1 -1
  34. package/dist/protocol/rowBatch/columnLayout.js.map +1 -1
  35. package/dist/protocol/rowBatch/format.js.map +1 -1
  36. package/dist/protocol/rowBatch/reader.js.map +1 -1
  37. package/dist/protocol/rowBatch/writer.js.map +1 -1
  38. package/dist/protocol/wire.js.map +1 -1
  39. package/dist/signalr/index.js.map +1 -1
  40. package/dist/signalr/protocols.js.map +1 -1
  41. package/dist/signalr/transport.d.ts +7 -3
  42. package/dist/signalr/transport.js +2 -1
  43. package/dist/signalr/transport.js.map +1 -1
  44. package/dist/sqlite/apply.js.map +1 -1
  45. package/dist/sqlite/commands.js.map +1 -1
  46. package/dist/sqlite/connection.js.map +1 -1
  47. package/dist/sqlite/driver.js.map +1 -1
  48. package/dist/sqlite/index.js.map +1 -1
  49. package/dist/sqlite/internals.js.map +1 -1
  50. package/dist/sqlite/options.js.map +1 -1
  51. package/dist/sqlite/push.js.map +1 -1
  52. package/dist/sqlite/query.js.map +1 -1
  53. package/dist/sqlite/revocation.js.map +1 -1
  54. package/dist/sqlite/sql.js +113 -113
  55. package/dist/sqlite/sql.js.map +1 -1
  56. package/dist/sqlite/store.js.map +1 -1
  57. package/dist/sqlite/values.js.map +1 -1
  58. package/dist/testing/index.js.map +1 -1
  59. package/package.json +1 -1
package/LICENSE.md CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024-present Iman Kari (G9TM)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Iman Kari (G9TM)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PARITY.md CHANGED
@@ -1,96 +1,96 @@
1
- # @g9tm/syncdata — parity with the .NET G9SyncData client
2
-
3
- This package is the TypeScript twin of the .NET client packages of G9SyncData. **One change lands on both sides**: a
4
- feature, fix, error code or wire change made to the .NET client is made here in the same change (and the other way
5
- round), with the same version (`G9SyncDataVersion` in `Directory.Build.props`; `js/scripts/check-versions.mjs` enforces
6
- it). This file maps every .NET piece to its twin and lists every deliberate difference.
7
-
8
- ## How parity is proven
9
-
10
- | Proof | Where |
11
- |---|---|
12
- | Golden vectors written by .NET, checked by TypeScript: varints, zig-zag, MD5, digest buckets and row hashes, canonical texts, key normalisation, subscription keys, G9RowBatch frames in both layouts (read in 4 encodings, re-written to the same bytes), the local schema SQL and model hashes, storage text forms, and the server's hub messages in JSON and MessagePack | `tests/G9SyncData.Tests/Protocol/JsGoldenVectorsTests.cs` → `test/golden/dotnet-vectors.json` → `test/protocol.test.ts`, `test/sql.test.ts`, `test/values.test.ts`, `test/hub.test.ts` |
13
- | Requests written by TypeScript, parsed by the .NET server's own hub protocols (JSON and Nerdbank MessagePack) and compared with the .NET samples | `test/hub.test.ts` → `test/golden/js-vectors.json` → `JsGoldenVectorsTests.Requests_the_TypeScript_client_writes_deserialize_on_the_server` |
14
- | The TypeScript engine + store + transport against the real .NET sample server (50,000 rows, push, pokes, reconcile, unsubscribe), over both hub protocols | `test/interop/server.test.ts` (`npm run test:interop`) |
15
- | The database the TypeScript store writes equals the one the .NET store writes for the same sync: app rows and storage classes, every `g9_*` row, the schema, the stored JSON | `test/interop/server.test.ts` → database parity |
16
- | A site's first download as a snapshot FILE (ADR-0072) copied by the TypeScript store leaves the database the .NET store leaves (both sites as files, the second appended to the first's table) | `test/interop/server.test.ts` → "a snapshot FILE copied by the TypeScript store" |
17
- | The same scenario in a Lynx-shaped runtime (no BigInt/TextEncoder/URL/WebSocket, native modules behind a copying bridge), with and without snapshot files through `lynxSnapshotFileSink`'s module methods | `@g9tm/syncdata-lynx` `npm run test:lynxsim` |
18
-
19
- `G9_UPDATE_GOLDEN=1` rewrites the golden files after an intended change; commit them with the change that needs them.
20
-
21
- ## Map
22
-
23
- | .NET | TypeScript | Notes |
24
- |---|---|---|
25
- | `G9ESyncColumnType`, `G9ESyncDirection`, `G9EConflictPolicy`, `G9EMoveOutPolicy` | same names (const objects) | wire numbers identical |
26
- | `G9SyncErrorCodes`, `G9CSyncException` | `G9SyncErrorCodes`, `G9SyncException` (`code`, `retryable`, `details`) | |
27
- | `long` (exact) | `G9Long` (hi/lo, no BigInt) | digests, row hashes, masks, ticks, int64 values; plain `number` for versions, ids, counts |
28
- | `G9CSyncKey` | `G9SyncKey` | `Guid.Parse` forms D, N, B, P (not X) |
29
- | `G9CSyncDigest`, `G9CMd5` | `G9SyncDigest`, `md5` | `canonicalText` takes the SQLite storage class (`typeof`) |
30
- | `G9CSyncParameters` | `G9SyncParameters` (`setGuid`, `setInt64`, `setGuidList`, …) | `Uri.EscapeDataString` reproduced byte for byte |
31
- | `G9Dt*` DTOs | same names, camelCase fields | `ServerTimeUtc` → `serverTimeUtcMs`; GUIDs as canonical strings |
32
- | `G9SyncProtocol`, `G9SyncHubErrors` | same | |
33
- | `G9CRowBatchWriter` / `G9CRowBatchReader` | `G9RowBatchWriter` / `openRowBatch` → `G9RowBatchReader` | async open (a Lynx decoder crosses the bridge) |
34
- | Brotli (`System.IO.Compression`) | `G9FrameDecompressor`: `useNodeBrotli()`, `useLynxBrotli()` (native), `jsBrotliDecompressor()` | push frames are uncompressed unless a `G9FrameCompressor` is given (Node: `nodeBrotliCompressor()`); every server reads both |
35
- | `G9CSyncClientModel(Builder)` | `G9SyncClientModel(Builder)` | no source generator: build the model in code, **tables in the order the .NET generator emits them** (entity type name, ordinal) for identical `tid`s |
36
- | `G9CSyncEngine`, `G9CSyncEngineOptions` | `G9SyncEngine`, `G9SyncEngineOptions` (durations in ms) | events → `onRowsApplied` / `onRunCompleted`; `CancellationToken` → `AbortSignal`; `IProgress` → callbacks; `ILogger` → `G9SyncLogger` |
37
- | `G9CSyncRun`, `G9CSyncRequest`, `G9CSyncRunResult`, `G9CSyncPostStep` | `G9SyncRun`, `G9SyncRequest`, `G9SyncRunResult`, `G9SyncPostStep` | `run.progress()` is the bounded progress channel |
38
- | `G9CSyncProgressTracker`, `G9DtSyncProgress` | `G9SyncProgressTracker`, `G9DtSyncProgress` (`etaMs`) | same planning, creep and monotonicity |
39
- | `IG9SyncClientStore` | `G9SyncClientStore` | |
40
- | `ReadChangeFeedAsync(table, after, limit, G9CChangeFeedKeyFilter, ct)` | `readChangeFeedFiltered(table, after, limit, G9ChangeFeedKeyFilter, signal?)` | a named method instead of an overload; the same SQL and the same validation (`validateChangeFeedKeyFilter`) |
41
- | `G9CSqliteSyncStore`, options | `G9SqliteSyncStore` (`@g9tm/syncdata/sqlite`) over a `G9SqliteDriver` | same tables, triggers, SQL, JSON blobs and storage forms |
42
- | Snapshot files (`G9CSqliteSyncStore.SnapshotFiles.cs`): `AcceptSnapshotFiles`, `SnapshotFileDirectory`, checkpoints, staging, verification, the copy/columns/append/merge apply, the sweep | `snapshotFiles: G9SnapshotFileSink` (`nodeSnapshotFileSink(dir)` in `@g9tm/syncdata/node`, `lynxSnapshotFileSink(db)` in `@g9tm/syncdata-lynx`), `src/sqlite/snapshotFiles.ts` | the same SQL statement for statement; the bytes go through the sink (decompress + SHA-256 + positional write) instead of `RandomAccess` in process; `G9CSnapshotFileWriter` (the server's) has no twin |
43
- | `EnqueueCommand(sqlite3 appConnection, …)` | `enqueueCommandOnSession({ driver, session, tx }, …)` | SAVEPOINT inside the app's transaction |
44
- | `IncarnationMarkerPath` | `incarnationMarker` (`fileIncarnationMarker(fileSystem, path)`) | any `G9FileSystem` (Lynx: the SignalR native module's) |
45
- | `ExportPendingChangesAsync(path)` | `exportPendingChanges(writeLine)` | the caller writes the lines where it wants |
46
- | `CheckIntegrityAsync` | `checkIntegrity()` | |
47
- | `G9CSqliteException` (`IG9SyncLocalStoreFailure`) | `G9SqliteStoreError` (`localStoreFailure`) | classified from the driver's code |
48
- | `G9CSignalRSyncTransport`, options | `G9SignalRSyncTransport` (`@g9tm/syncdata/signalr`) | over `G9Client` of `@g9tm/signalr-supernetcore-client`; Lynx: `createLynxSyncTransport`. Limits (ADR-0074): `CallTimeout` / `PushTimeout` / `StreamIdleTimeout` = `callTimeoutMs` / `pushTimeoutMs` / `streamIdleTimeoutMs`, same defaults, same retryable `G9S-NET-FAILURE`, same connection drop (`SignalRTimeoutTests` / `test/transportTimeouts.test.ts`) |
49
- | `G9CInMemorySyncTransport`, `G9CTransportFaults` | `G9FaultInjectingTransport`, `G9TransportFaults` (`@g9tm/syncdata/testing`) | wraps a real transport (no in-process server engine in JavaScript) |
50
-
51
- ## Deliberate differences
52
-
53
- - **Applying rows.** The .NET store runs one prepared statement per row and decides from each result. Through a driver
54
- every call is a bridge round trip on Lynx, so the TypeScript store queues the row statements and runs them in batches
55
- (`statementsPerBatch`, default 1,500); the per-row decisions move into SQL — the metadata upsert reads `changes()` of
56
- the row upsert, the clock moves with `UPDATE g9_clock … WHERE changes() > 0`. The resulting rows, metadata and clock
57
- are identical (database parity test).
58
- - **Skipping triggers during an apply.** There is no `SQLITE_DBCONFIG_ENABLE_TRIGGER` through a driver. For a staged
59
- pull of at least `skipTriggersMinRows` (default 5,000) rows the store drops its own capture and command triggers inside
60
- the apply transaction and re-creates them from their stored SQL before commit — never while a foreign trigger exists.
61
- Smaller applies run with triggers on and report it (`diagnostics.triggersSkipped`).
62
- - **Releasing many rows.** An unsubscribe, an access revocation or an expired ephemeral subscription disposes its
63
- released rows in bulk (`disposeReleasedRows`): metadata, pending commands and app rows read in three queries, the
64
- revocation policy deciding in the .NET store's order, writes batched with each row's explicit `local_seq`, the clock
65
- moved once. Same database (`test/revocation.test.ts` against the row-by-row path, interop parity against .NET); row
66
- by row while a foreign trigger exists. A run of move-outs (a MoveOut section of a pull, a reconciliation's move-out
67
- keys) goes the same way (`moveOutMany`): every membership update queued and flushed once, then the released rows
68
- disposed in bulk — same database as `moveOut` row by row (`test/moveout.test.ts`); row by row for an ephemeral
69
- subscription, a Keep policy, duplicate keys or a foreign trigger.
70
- - **Provisioning.** `initialize` reads everything its decisions need (g9_tables, the tables and their columns, the
71
- triggers, the AUTOINCREMENT counter) in ONE driver call and runs the resulting statements in ONE batch, in the order
72
- the .NET store's per-table `ProvisionTable` runs them; a new table gets its tid explicitly (the value AUTOINCREMENT
73
- would hand out, in model order) so its triggers are written in the same batch. Same objects, rows, tids and counter
74
- (`test/provision.test.ts` compares both paths on fresh, reopened, evolved and repaired databases). AgriPad's 47 tables:
75
- 14 driver calls on every start instead of 296, 17 instead of 534 on the first.
76
- - **Per-run bookkeeping.** `saveHello` writes the meta rows, the definitions' move-out policies and every negotiated
77
- table (with its capture triggers) in ONE batch, after one read for the only per-table question (does a table with
78
- newly negotiated columns hold rows?); the rows an apply protects (`readRowsChangedAfter`) are read with one
79
- `UNION ALL` statement instead of a query per table. Same database (`test/hello.test.ts`). AgriPad: a no-change sync
80
- of a site went from 216 driver calls to ~40.
81
- - **Staging a chunk.** One atomic driver call (`runAtomic`) instead of begin + insert + commit, and the connection's
82
- journal mode and `synchronous` read once per connection rather than per chunk: 3 calls per chunk instead of ~7
83
- (`test/staging.test.ts`). Same rows, same durability window.
84
- - **Snapshot-file bytes.** The .NET store decompresses and writes segments in process; the TypeScript store hands each
85
- compressed segment to its `G9SnapshotFileSink` and never sees the decoded bytes (on Lynx they never cross the bridge).
86
- The sink also answers the store's size/header checks. Without a sink (`snapshotFiles: null`, the default) the store
87
- offers no files and the server sends rows. Everything the store does in SQL - the checkpoints, the copy, `g9_rows`,
88
- the merge rules, the sweep - is the .NET store's.
89
- - **Two sessions.** A writer session under the store's lock and a reader session, instead of a pool.
90
- - **Reading values to push.** Decimal and String columns are selected as `CAST(… AS TEXT)`: exactly the text .NET's
91
- `GetString` / `GetDecimal` parse.
92
- - **DateTime text format.** Only the default `yyyy-MM-dd'T'HH:mm:ss.fffffff` is supported.
93
- - **Trigger text line endings.** A .NET build from a Windows checkout embeds CRLF in the trigger SQL it creates; the
94
- TypeScript store writes LF. SQLite ignores the difference.
95
- - **Not ported:** the Dotmim.Sync retirement (`RetireDotmimCaptureAsync` — a MAUI-only migration), OpenTelemetry
96
- (`G9CSyncClientTelemetry`; log through `G9SyncLogger`), DI extensions.
1
+ # @g9tm/syncdata — parity with the .NET G9SyncData client
2
+
3
+ This package is the TypeScript twin of the .NET client packages of G9SyncData. **One change lands on both sides**: a
4
+ feature, fix, error code or wire change made to the .NET client is made here in the same change (and the other way
5
+ round), with the same version (`G9SyncDataVersion` in `Directory.Build.props`; `js/scripts/check-versions.mjs` enforces
6
+ it). This file maps every .NET piece to its twin and lists every deliberate difference.
7
+
8
+ ## How parity is proven
9
+
10
+ | Proof | Where |
11
+ |---|---|
12
+ | Golden vectors written by .NET, checked by TypeScript: varints, zig-zag, MD5, digest buckets and row hashes, canonical texts, key normalisation, subscription keys, G9RowBatch frames in both layouts (read in 4 encodings, re-written to the same bytes), the local schema SQL and model hashes, storage text forms, and the server's hub messages in JSON and MessagePack | `tests/G9SyncData.Tests/Protocol/JsGoldenVectorsTests.cs` → `test/golden/dotnet-vectors.json` → `test/protocol.test.ts`, `test/sql.test.ts`, `test/values.test.ts`, `test/hub.test.ts` |
13
+ | Requests written by TypeScript, parsed by the .NET server's own hub protocols (JSON and Nerdbank MessagePack) and compared with the .NET samples | `test/hub.test.ts` → `test/golden/js-vectors.json` → `JsGoldenVectorsTests.Requests_the_TypeScript_client_writes_deserialize_on_the_server` |
14
+ | The TypeScript engine + store + transport against the real .NET sample server (50,000 rows, push, pokes, reconcile, unsubscribe), over both hub protocols | `test/interop/server.test.ts` (`npm run test:interop`) |
15
+ | The database the TypeScript store writes equals the one the .NET store writes for the same sync: app rows and storage classes, every `g9_*` row, the schema, the stored JSON | `test/interop/server.test.ts` → database parity |
16
+ | A site's first download as a snapshot FILE (ADR-0072) copied by the TypeScript store leaves the database the .NET store leaves (both sites as files, the second appended to the first's table) | `test/interop/server.test.ts` → "a snapshot FILE copied by the TypeScript store" |
17
+ | The same scenario in a Lynx-shaped runtime (no BigInt/TextEncoder/URL/WebSocket, native modules behind a copying bridge), with and without snapshot files through `lynxSnapshotFileSink`'s module methods | `@g9tm/syncdata-lynx` `npm run test:lynxsim` |
18
+
19
+ `G9_UPDATE_GOLDEN=1` rewrites the golden files after an intended change; commit them with the change that needs them.
20
+
21
+ ## Map
22
+
23
+ | .NET | TypeScript | Notes |
24
+ |---|---|---|
25
+ | `G9ESyncColumnType`, `G9ESyncDirection`, `G9EConflictPolicy`, `G9EMoveOutPolicy` | same names (const objects) | wire numbers identical |
26
+ | `G9SyncErrorCodes`, `G9CSyncException` | `G9SyncErrorCodes`, `G9SyncException` (`code`, `retryable`, `details`) | |
27
+ | `long` (exact) | `G9Long` (hi/lo, no BigInt) | digests, row hashes, masks, ticks, int64 values; plain `number` for versions, ids, counts |
28
+ | `G9CSyncKey` | `G9SyncKey` | `Guid.Parse` forms D, N, B, P (not X) |
29
+ | `G9CSyncDigest`, `G9CMd5` | `G9SyncDigest`, `md5` | `canonicalText` takes the SQLite storage class (`typeof`) |
30
+ | `G9CSyncParameters` | `G9SyncParameters` (`setGuid`, `setInt64`, `setGuidList`, …) | `Uri.EscapeDataString` reproduced byte for byte |
31
+ | `G9Dt*` DTOs | same names, camelCase fields | `ServerTimeUtc` → `serverTimeUtcMs`; GUIDs as canonical strings |
32
+ | `G9SyncProtocol`, `G9SyncHubErrors` | same | |
33
+ | `G9CRowBatchWriter` / `G9CRowBatchReader` | `G9RowBatchWriter` / `openRowBatch` → `G9RowBatchReader` | async open (a Lynx decoder crosses the bridge) |
34
+ | Brotli (`System.IO.Compression`) | `G9FrameDecompressor`: `useNodeBrotli()`, `useLynxBrotli()` (native), `jsBrotliDecompressor()` | push frames are uncompressed unless a `G9FrameCompressor` is given (Node: `nodeBrotliCompressor()`); every server reads both |
35
+ | `G9CSyncClientModel(Builder)` | `G9SyncClientModel(Builder)` | no source generator: build the model in code, **tables in the order the .NET generator emits them** (entity type name, ordinal) for identical `tid`s |
36
+ | `G9CSyncEngine`, `G9CSyncEngineOptions` | `G9SyncEngine`, `G9SyncEngineOptions` (durations in ms) | events → `onRowsApplied` / `onRunCompleted`; `CancellationToken` → `AbortSignal`; `IProgress` → callbacks; `ILogger` → `G9SyncLogger` |
37
+ | `G9CSyncRun`, `G9CSyncRequest`, `G9CSyncRunResult`, `G9CSyncPostStep` | `G9SyncRun`, `G9SyncRequest`, `G9SyncRunResult`, `G9SyncPostStep` | `run.progress()` is the bounded progress channel |
38
+ | `G9CSyncProgressTracker`, `G9DtSyncProgress` | `G9SyncProgressTracker`, `G9DtSyncProgress` (`etaMs`) | same planning, creep and monotonicity |
39
+ | `IG9SyncClientStore` | `G9SyncClientStore` | |
40
+ | `ReadChangeFeedAsync(table, after, limit, G9CChangeFeedKeyFilter, ct)` | `readChangeFeedFiltered(table, after, limit, G9ChangeFeedKeyFilter, signal?)` | a named method instead of an overload; the same SQL and the same validation (`validateChangeFeedKeyFilter`) |
41
+ | `G9CSqliteSyncStore`, options | `G9SqliteSyncStore` (`@g9tm/syncdata/sqlite`) over a `G9SqliteDriver` | same tables, triggers, SQL, JSON blobs and storage forms |
42
+ | Snapshot files (`G9CSqliteSyncStore.SnapshotFiles.cs`): `AcceptSnapshotFiles`, `SnapshotFileDirectory`, checkpoints, staging, verification, the copy/columns/append/merge apply, the sweep | `snapshotFiles: G9SnapshotFileSink` (`nodeSnapshotFileSink(dir)` in `@g9tm/syncdata/node`, `lynxSnapshotFileSink(db)` in `@g9tm/syncdata-lynx`), `src/sqlite/snapshotFiles.ts` | the same SQL statement for statement; the bytes go through the sink (decompress + SHA-256 + positional write) instead of `RandomAccess` in process; `G9CSnapshotFileWriter` (the server's) has no twin |
43
+ | `EnqueueCommand(sqlite3 appConnection, …)` | `enqueueCommandOnSession({ driver, session, tx }, …)` | SAVEPOINT inside the app's transaction |
44
+ | `IncarnationMarkerPath` | `incarnationMarker` (`fileIncarnationMarker(fileSystem, path)`) | any `G9FileSystem` (Lynx: the SignalR native module's) |
45
+ | `ExportPendingChangesAsync(path)` | `exportPendingChanges(writeLine)` | the caller writes the lines where it wants |
46
+ | `CheckIntegrityAsync` | `checkIntegrity()` | |
47
+ | `G9CSqliteException` (`IG9SyncLocalStoreFailure`) | `G9SqliteStoreError` (`localStoreFailure`) | classified from the driver's code |
48
+ | `G9CSignalRSyncTransport`, options | `G9SignalRSyncTransport` (`@g9tm/syncdata/signalr`) | over `G9Client` of `@g9tm/signalr-supernetcore-client`; Lynx: `createLynxSyncTransport`. Limits (ADR-0074): `CallTimeout` / `PlanTimeout` / `PushTimeout` / `StreamIdleTimeout` = `callTimeoutMs` / `planTimeoutMs` / `pushTimeoutMs` / `streamIdleTimeoutMs`, same defaults, same retryable `G9S-NET-FAILURE`, same connection drop (`SignalRTimeoutTests` / `test/transportTimeouts.test.ts`) |
49
+ | `G9CInMemorySyncTransport`, `G9CTransportFaults` | `G9FaultInjectingTransport`, `G9TransportFaults` (`@g9tm/syncdata/testing`) | wraps a real transport (no in-process server engine in JavaScript) |
50
+
51
+ ## Deliberate differences
52
+
53
+ - **Applying rows.** The .NET store runs one prepared statement per row and decides from each result. Through a driver
54
+ every call is a bridge round trip on Lynx, so the TypeScript store queues the row statements and runs them in batches
55
+ (`statementsPerBatch`, default 1,500); the per-row decisions move into SQL — the metadata upsert reads `changes()` of
56
+ the row upsert, the clock moves with `UPDATE g9_clock … WHERE changes() > 0`. The resulting rows, metadata and clock
57
+ are identical (database parity test).
58
+ - **Skipping triggers during an apply.** There is no `SQLITE_DBCONFIG_ENABLE_TRIGGER` through a driver. For a staged
59
+ pull of at least `skipTriggersMinRows` (default 5,000) rows the store drops its own capture and command triggers inside
60
+ the apply transaction and re-creates them from their stored SQL before commit — never while a foreign trigger exists.
61
+ Smaller applies run with triggers on and report it (`diagnostics.triggersSkipped`).
62
+ - **Releasing many rows.** An unsubscribe, an access revocation or an expired ephemeral subscription disposes its
63
+ released rows in bulk (`disposeReleasedRows`): metadata, pending commands and app rows read in three queries, the
64
+ revocation policy deciding in the .NET store's order, writes batched with each row's explicit `local_seq`, the clock
65
+ moved once. Same database (`test/revocation.test.ts` against the row-by-row path, interop parity against .NET); row
66
+ by row while a foreign trigger exists. A run of move-outs (a MoveOut section of a pull, a reconciliation's move-out
67
+ keys) goes the same way (`moveOutMany`): every membership update queued and flushed once, then the released rows
68
+ disposed in bulk — same database as `moveOut` row by row (`test/moveout.test.ts`); row by row for an ephemeral
69
+ subscription, a Keep policy, duplicate keys or a foreign trigger.
70
+ - **Provisioning.** `initialize` reads everything its decisions need (g9_tables, the tables and their columns, the
71
+ triggers, the AUTOINCREMENT counter) in ONE driver call and runs the resulting statements in ONE batch, in the order
72
+ the .NET store's per-table `ProvisionTable` runs them; a new table gets its tid explicitly (the value AUTOINCREMENT
73
+ would hand out, in model order) so its triggers are written in the same batch. Same objects, rows, tids and counter
74
+ (`test/provision.test.ts` compares both paths on fresh, reopened, evolved and repaired databases). AgriPad's 47 tables:
75
+ 14 driver calls on every start instead of 296, 17 instead of 534 on the first.
76
+ - **Per-run bookkeeping.** `saveHello` writes the meta rows, the definitions' move-out policies and every negotiated
77
+ table (with its capture triggers) in ONE batch, after one read for the only per-table question (does a table with
78
+ newly negotiated columns hold rows?); the rows an apply protects (`readRowsChangedAfter`) are read with one
79
+ `UNION ALL` statement instead of a query per table. Same database (`test/hello.test.ts`). AgriPad: a no-change sync
80
+ of a site went from 216 driver calls to ~40.
81
+ - **Staging a chunk.** One atomic driver call (`runAtomic`) instead of begin + insert + commit, and the connection's
82
+ journal mode and `synchronous` read once per connection rather than per chunk: 3 calls per chunk instead of ~7
83
+ (`test/staging.test.ts`). Same rows, same durability window.
84
+ - **Snapshot-file bytes.** The .NET store decompresses and writes segments in process; the TypeScript store hands each
85
+ compressed segment to its `G9SnapshotFileSink` and never sees the decoded bytes (on Lynx they never cross the bridge).
86
+ The sink also answers the store's size/header checks. Without a sink (`snapshotFiles: null`, the default) the store
87
+ offers no files and the server sends rows. Everything the store does in SQL - the checkpoints, the copy, `g9_rows`,
88
+ the merge rules, the sweep - is the .NET store's.
89
+ - **Two sessions.** A writer session under the store's lock and a reader session, instead of a pool.
90
+ - **Reading values to push.** Decimal and String columns are selected as `CAST(… AS TEXT)`: exactly the text .NET's
91
+ `GetString` / `GetDecimal` parse.
92
+ - **DateTime text format.** Only the default `yyyy-MM-dd'T'HH:mm:ss.fffffff` is supported.
93
+ - **Trigger text line endings.** A .NET build from a Windows checkout embeds CRLF in the trigger SQL it creates; the
94
+ TypeScript store writes LF. SQLite ignores the difference.
95
+ - **Not ported:** the Dotmim.Sync retirement (`RetireDotmimCaptureAsync` — a MAUI-only migration), OpenTelemetry
96
+ (`G9CSyncClientTelemetry`; log through `G9SyncLogger`), DI extensions.
package/README.md CHANGED
@@ -1,71 +1,71 @@
1
- # @g9tm/syncdata
2
-
3
- The TypeScript twin of the [G9SyncData](https://github.com/ImanKari/G9SyncData) .NET client: offline-first sync between
4
- a device's SQLite database and a PostgreSQL server, for **Lynx** apps (with
5
- [`@g9tm/syncdata-lynx`](../lynx/README.md)), Node and browsers. Same protocol, same engine behaviour, and **the same
6
- local database** as the .NET client — a database the MAUI app wrote opens in the Lynx app and keeps syncing.
7
-
8
- | Entry point | What |
9
- |---|---|
10
- | `@g9tm/syncdata` | the engine (`G9SyncEngine`), the client model, the protocol (DTOs, G9RowBatch frames), errors, digests |
11
- | `@g9tm/syncdata/sqlite` | the SQLite client store (`G9SqliteSyncStore`) over any `G9SqliteDriver` |
12
- | `@g9tm/syncdata/signalr` | the SignalR transport (`G9SignalRSyncTransport`), JSON and MessagePack with exact 64-bit values |
13
- | `@g9tm/syncdata/node` | node:sqlite driver (`nodeSqliteDriver`), Brotli through node:zlib (`useNodeBrotli`) |
14
- | `@g9tm/syncdata/testing` | fault injection over any transport (`G9FaultInjectingTransport`) |
15
- | `@g9tm/syncdata/brotli-js` | a pure-JavaScript Brotli decoder for runtimes without a native one |
16
-
17
- Version 1.2.2 — always equal to the .NET packages' version. What maps to what, and every deliberate difference:
18
- [PARITY.md](PARITY.md).
19
-
20
- ## Install
21
-
22
- ```bash
23
- npm install @g9tm/syncdata @g9tm/signalr-supernetcore-client @microsoft/signalr @microsoft/signalr-protocol-msgpack @msgpack/msgpack
24
- ```
25
-
26
- The SignalR packages are optional peers: only `@g9tm/syncdata/signalr` needs them.
27
-
28
- ## Use (Node)
29
-
30
- ```ts
31
- import { G9ESyncColumnType as T, G9SyncClientModelBuilder, G9SyncEngine, G9SyncParameters } from '@g9tm/syncdata';
32
- import { G9SqliteSyncStore } from '@g9tm/syncdata/sqlite';
33
- import { G9SignalRSyncTransport } from '@g9tm/syncdata/signalr';
34
- import { nodeSqliteDriver, useNodeBrotli } from '@g9tm/syncdata/node';
35
-
36
- useNodeBrotli(); // the server compresses frames with Brotli
37
-
38
- // Tables in the order the .NET source generator emits them (entity type name), so both clients number them alike.
39
- const model = new G9SyncClientModelBuilder()
40
- .table('farm.Pot', 'Pot', (t) =>
41
- t.key('Id').column('SiteId', T.Guid, false).column('Code', T.String, false).column('Weight', T.Decimal).index(null, false, 'SiteId'))
42
- .table('farm.Site', 'Site', (t) => t.key('Id').column('Name', T.String, false))
43
- .build();
44
-
45
- const store = new G9SqliteSyncStore({ driver: nodeSqliteDriver(), databasePath: './device.db' });
46
- const transport = new G9SignalRSyncTransport({ url: 'https://sync.example.com/g9sync', accessTokenFactory: () => token });
47
- const engine = new G9SyncEngine(store, transport, model, { appVersion: '1.0.0' });
48
-
49
- await engine.initialize();
50
- await engine.subscribe('sites');
51
- await engine.subscribe('site-pots', new G9SyncParameters().setGuid('SiteId', siteId));
52
- const run = engine.run({ subscriptions: ['site-pots'] });
53
- run.onProgress((p) => console.log(p.phase, Math.round(p.overall * 100) + '%'));
54
- const result = await run.completion;
55
- if (!result.isSuccess) console.error(result.error?.code, result.error?.message);
56
- ```
57
-
58
- Local edits are ordinary SQL on the app's own connection: the store's triggers capture them and the next run pushes
59
- them. Commands (`engine.executeCommand`), the change feed (`readChangeFeed`), rejections, quarantine and the run reports
60
- work as on .NET ([07-Client-Engine](../../../AiGuides/07-Client-Engine.md), [08-Client-Sqlite](../../../AiGuides/08-Client-Sqlite.md)).
61
-
62
- ## Lynx
63
-
64
- On Lynx's background thread use [`@g9tm/syncdata-lynx`](../lynx/README.md): `useLynxBrotli()` and
65
- `createLynxSyncTransport({ url })` (MessagePack over the G9 native WebSocket — release Lynx builds have none of their
66
- own), and G9LynxControls' native SQLite driver for the store. The code ships as ES2019 and uses no API the background
67
- thread lacks (no BigInt, `TextEncoder`, `URL`, `btoa`, `globalThis` timers) — the build fails otherwise.
68
-
69
- ## License
70
-
71
- MIT. The Brotli decoder in `brotli-js` is Google's (MIT): [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
1
+ # @g9tm/syncdata
2
+
3
+ The TypeScript twin of the [G9SyncData](https://github.com/ImanKari/G9SyncData) .NET client: offline-first sync between
4
+ a device's SQLite database and a PostgreSQL server, for **Lynx** apps (with
5
+ [`@g9tm/syncdata-lynx`](../lynx/README.md)), Node and browsers. Same protocol, same engine behaviour, and **the same
6
+ local database** as the .NET client — a database the MAUI app wrote opens in the Lynx app and keeps syncing.
7
+
8
+ | Entry point | What |
9
+ |---|---|
10
+ | `@g9tm/syncdata` | the engine (`G9SyncEngine`), the client model, the protocol (DTOs, G9RowBatch frames), errors, digests |
11
+ | `@g9tm/syncdata/sqlite` | the SQLite client store (`G9SqliteSyncStore`) over any `G9SqliteDriver` |
12
+ | `@g9tm/syncdata/signalr` | the SignalR transport (`G9SignalRSyncTransport`), JSON and MessagePack with exact 64-bit values |
13
+ | `@g9tm/syncdata/node` | node:sqlite driver (`nodeSqliteDriver`), Brotli through node:zlib (`useNodeBrotli`) |
14
+ | `@g9tm/syncdata/testing` | fault injection over any transport (`G9FaultInjectingTransport`) |
15
+ | `@g9tm/syncdata/brotli-js` | a pure-JavaScript Brotli decoder for runtimes without a native one |
16
+
17
+ Version 1.2.2 — always equal to the .NET packages' version. What maps to what, and every deliberate difference:
18
+ [PARITY.md](PARITY.md).
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @g9tm/syncdata @g9tm/signalr-supernetcore-client @microsoft/signalr @microsoft/signalr-protocol-msgpack @msgpack/msgpack
24
+ ```
25
+
26
+ The SignalR packages are optional peers: only `@g9tm/syncdata/signalr` needs them.
27
+
28
+ ## Use (Node)
29
+
30
+ ```ts
31
+ import { G9ESyncColumnType as T, G9SyncClientModelBuilder, G9SyncEngine, G9SyncParameters } from '@g9tm/syncdata';
32
+ import { G9SqliteSyncStore } from '@g9tm/syncdata/sqlite';
33
+ import { G9SignalRSyncTransport } from '@g9tm/syncdata/signalr';
34
+ import { nodeSqliteDriver, useNodeBrotli } from '@g9tm/syncdata/node';
35
+
36
+ useNodeBrotli(); // the server compresses frames with Brotli
37
+
38
+ // Tables in the order the .NET source generator emits them (entity type name), so both clients number them alike.
39
+ const model = new G9SyncClientModelBuilder()
40
+ .table('farm.Pot', 'Pot', (t) =>
41
+ t.key('Id').column('SiteId', T.Guid, false).column('Code', T.String, false).column('Weight', T.Decimal).index(null, false, 'SiteId'))
42
+ .table('farm.Site', 'Site', (t) => t.key('Id').column('Name', T.String, false))
43
+ .build();
44
+
45
+ const store = new G9SqliteSyncStore({ driver: nodeSqliteDriver(), databasePath: './device.db' });
46
+ const transport = new G9SignalRSyncTransport({ url: 'https://sync.example.com/g9sync', accessTokenFactory: () => token });
47
+ const engine = new G9SyncEngine(store, transport, model, { appVersion: '1.0.0' });
48
+
49
+ await engine.initialize();
50
+ await engine.subscribe('sites');
51
+ await engine.subscribe('site-pots', new G9SyncParameters().setGuid('SiteId', siteId));
52
+ const run = engine.run({ subscriptions: ['site-pots'] });
53
+ run.onProgress((p) => console.log(p.phase, Math.round(p.overall * 100) + '%'));
54
+ const result = await run.completion;
55
+ if (!result.isSuccess) console.error(result.error?.code, result.error?.message);
56
+ ```
57
+
58
+ Local edits are ordinary SQL on the app's own connection: the store's triggers capture them and the next run pushes
59
+ them. Commands (`engine.executeCommand`), the change feed (`readChangeFeed`), rejections, quarantine and the run reports
60
+ work as on .NET ([07-Client-Engine](../../../AiGuides/07-Client-Engine.md), [08-Client-Sqlite](../../../AiGuides/08-Client-Sqlite.md)).
61
+
62
+ ## Lynx
63
+
64
+ On Lynx's background thread use [`@g9tm/syncdata-lynx`](../lynx/README.md): `useLynxBrotli()` and
65
+ `createLynxSyncTransport({ url })` (MessagePack over the G9 native WebSocket — release Lynx builds have none of their
66
+ own), and G9LynxControls' native SQLite driver for the store. The code ships as ES2019 and uses no API the background
67
+ thread lacks (no BigInt, `TextEncoder`, `URL`, `btoa`, `globalThis` timers) — the build fails otherwise.
68
+
69
+ ## License
70
+
71
+ MIT. The Brotli decoder in `brotli-js` is Google's (MIT): [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
@@ -1,31 +1,31 @@
1
- # Third-party notices
2
-
3
- `@g9tm/syncdata` ships one piece of third-party code.
4
-
5
- ## Brotli decoder (`dist/brotli-js/decode.js`)
6
-
7
- Google's JavaScript Brotli decoder, `js/decode.ts` of https://github.com/google/brotli at commit
8
- `3499acbb7ac7818c1b929a8c9c5c5f8a634751da` (2025-10-22), vendored unmodified in `src/brotli-js/decode.ts`. It is only
9
- loaded through the `@g9tm/syncdata/brotli-js` entry point.
10
-
11
- ```text
12
- Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
13
-
14
- Permission is hereby granted, free of charge, to any person obtaining a copy
15
- of this software and associated documentation files (the "Software"), to deal
16
- in the Software without restriction, including without limitation the rights
17
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
- copies of the Software, and to permit persons to whom the Software is
19
- furnished to do so, subject to the following conditions:
20
-
21
- The above copyright notice and this permission notice shall be included in
22
- all copies or substantial portions of the Software.
23
-
24
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
- THE SOFTWARE.
31
- ```
1
+ # Third-party notices
2
+
3
+ `@g9tm/syncdata` ships one piece of third-party code.
4
+
5
+ ## Brotli decoder (`dist/brotli-js/decode.js`)
6
+
7
+ Google's JavaScript Brotli decoder, `js/decode.ts` of https://github.com/google/brotli at commit
8
+ `3499acbb7ac7818c1b929a8c9c5c5f8a634751da` (2025-10-22), vendored unmodified in `src/brotli-js/decode.ts`. It is only
9
+ loaded through the `@g9tm/syncdata/brotli-js` entry point.
10
+
11
+ ```text
12
+ Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in
22
+ all copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
+ THE SOFTWARE.
31
+ ```
@@ -1 +1 @@
1
- {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/abstractions/digest.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatDouble } from '../internal/dotnetFormat.js';\nimport { md5 } from '../internal/md5.js';\nimport { utf8Encode } from '../internal/utf8.js';\nimport { G9ESyncColumnType } from './enums.js';\nimport { tryNormalizeGuid } from './guid.js';\nimport { G9Long } from './long.js';\n\nconst HASH = new Uint8Array(16);\n\n/**\n * The reconciliation hash (twin of `G9CSyncDigest`), defined so PostgreSQL computes the same numbers in plain SQL:\n * bucket = first byte of md5(key); row hash = first 8 bytes (big-endian, signed) of md5(`key:version[:computed…]`);\n * bucket digest = XOR of the row hashes in the bucket.\n */\nexport const G9SyncDigest = {\n /** Number of first-level buckets. */\n BucketCount: 256,\n\n /** Bucket of a canonical key. */\n getBucket(canonicalKey: string): number {\n md5(utf8Encode(canonicalKey), HASH);\n return HASH[0] as number;\n },\n\n /** Bucket of a canonical key given as UTF-8. */\n getBucketUtf8(canonicalKeyUtf8: Uint8Array): number {\n md5(canonicalKeyUtf8, HASH);\n return HASH[0] as number;\n },\n\n /**\n * Row hash of a canonical key, its server version and optional computed-column canonical texts (a null value\n * contributes an empty field, like .NET's `StringBuilder.Append(null)`).\n */\n getRowHash(canonicalKey: string, version: number | G9Long, computedValues?: readonly (string | null)[]): G9Long {\n let text = canonicalKey + ':' + (version instanceof G9Long ? version.toString() : G9Long.fromNumber(version).toString());\n if (computedValues) for (let i = 0; i < computedValues.length; i++) text += ':' + (computedValues[i] ?? '');\n md5(utf8Encode(text), HASH);\n return G9Long.readBE(HASH, 0);\n },\n\n /**\n * Canonical text of a computed value, identical to what the server SQL produces (`G9CSyncDigest.CanonicalText`).\n * `value` is what the SQLite driver read: null, a number, an `{ int64 }` tag or {@link G9Long}, text, or a BLOB.\n * `storage` says whether a number came from an INTEGER or a REAL cell (JavaScript cannot tell 5 from 5.0, .NET\n * formats them alike, but 1e15 as a REAL is `1E+15` and as an INTEGER `1000000000000000`); without it, integral\n * numbers count as INTEGER.\n */\n canonicalText(type: number, value: unknown, storage?: 'integer' | 'real'): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'boolean') return value ? '1' : '0';\n if (value instanceof G9Long || (typeof value === 'object' && value !== null && typeof (value as { int64?: unknown }).int64 === 'string')) {\n const long = G9Long.from(value as G9Long);\n if (type === G9ESyncColumnType.Boolean) return long.isZero() ? '0' : '1';\n return long.toString();\n }\n if (typeof value === 'number') {\n const integer = storage === 'integer' || (storage === undefined && Math.floor(value) === value && Math.abs(value) <= 9007199254740991);\n if (integer) {\n if (type === G9ESyncColumnType.Boolean) return value !== 0 ? '1' : '0';\n return G9Long.fromNumber(value).toString();\n }\n return formatDouble(value);\n }\n if (typeof value === 'string') {\n if (type === G9ESyncColumnType.Guid) {\n const guid = tryNormalizeGuid(value);\n if (guid !== null) return guid;\n }\n return value;\n }\n // A BLOB: .NET's object.ToString() of a byte[].\n return 'System.Byte[]';\n },\n};\n"]}
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/abstractions/digest.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatDouble } from '../internal/dotnetFormat.js';\r\nimport { md5 } from '../internal/md5.js';\r\nimport { utf8Encode } from '../internal/utf8.js';\r\nimport { G9ESyncColumnType } from './enums.js';\r\nimport { tryNormalizeGuid } from './guid.js';\r\nimport { G9Long } from './long.js';\r\n\r\nconst HASH = new Uint8Array(16);\r\n\r\n/**\r\n * The reconciliation hash (twin of `G9CSyncDigest`), defined so PostgreSQL computes the same numbers in plain SQL:\r\n * bucket = first byte of md5(key); row hash = first 8 bytes (big-endian, signed) of md5(`key:version[:computed…]`);\r\n * bucket digest = XOR of the row hashes in the bucket.\r\n */\r\nexport const G9SyncDigest = {\r\n /** Number of first-level buckets. */\r\n BucketCount: 256,\r\n\r\n /** Bucket of a canonical key. */\r\n getBucket(canonicalKey: string): number {\r\n md5(utf8Encode(canonicalKey), HASH);\r\n return HASH[0] as number;\r\n },\r\n\r\n /** Bucket of a canonical key given as UTF-8. */\r\n getBucketUtf8(canonicalKeyUtf8: Uint8Array): number {\r\n md5(canonicalKeyUtf8, HASH);\r\n return HASH[0] as number;\r\n },\r\n\r\n /**\r\n * Row hash of a canonical key, its server version and optional computed-column canonical texts (a null value\r\n * contributes an empty field, like .NET's `StringBuilder.Append(null)`).\r\n */\r\n getRowHash(canonicalKey: string, version: number | G9Long, computedValues?: readonly (string | null)[]): G9Long {\r\n let text = canonicalKey + ':' + (version instanceof G9Long ? version.toString() : G9Long.fromNumber(version).toString());\r\n if (computedValues) for (let i = 0; i < computedValues.length; i++) text += ':' + (computedValues[i] ?? '');\r\n md5(utf8Encode(text), HASH);\r\n return G9Long.readBE(HASH, 0);\r\n },\r\n\r\n /**\r\n * Canonical text of a computed value, identical to what the server SQL produces (`G9CSyncDigest.CanonicalText`).\r\n * `value` is what the SQLite driver read: null, a number, an `{ int64 }` tag or {@link G9Long}, text, or a BLOB.\r\n * `storage` says whether a number came from an INTEGER or a REAL cell (JavaScript cannot tell 5 from 5.0, .NET\r\n * formats them alike, but 1e15 as a REAL is `1E+15` and as an INTEGER `1000000000000000`); without it, integral\r\n * numbers count as INTEGER.\r\n */\r\n canonicalText(type: number, value: unknown, storage?: 'integer' | 'real'): string | null {\r\n if (value === null || value === undefined) return null;\r\n if (typeof value === 'boolean') return value ? '1' : '0';\r\n if (value instanceof G9Long || (typeof value === 'object' && value !== null && typeof (value as { int64?: unknown }).int64 === 'string')) {\r\n const long = G9Long.from(value as G9Long);\r\n if (type === G9ESyncColumnType.Boolean) return long.isZero() ? '0' : '1';\r\n return long.toString();\r\n }\r\n if (typeof value === 'number') {\r\n const integer = storage === 'integer' || (storage === undefined && Math.floor(value) === value && Math.abs(value) <= 9007199254740991);\r\n if (integer) {\r\n if (type === G9ESyncColumnType.Boolean) return value !== 0 ? '1' : '0';\r\n return G9Long.fromNumber(value).toString();\r\n }\r\n return formatDouble(value);\r\n }\r\n if (typeof value === 'string') {\r\n if (type === G9ESyncColumnType.Guid) {\r\n const guid = tryNormalizeGuid(value);\r\n if (guid !== null) return guid;\r\n }\r\n return value;\r\n }\r\n // A BLOB: .NET's object.ToString() of a byte[].\r\n return 'System.Byte[]';\r\n },\r\n};\r\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/abstractions/enums.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,uGAAuG;AAEvG,uDAAuD;AACvD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;IACX,cAAc,EAAE,CAAC;IACjB,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,EAAE;CACD,CAAC;AAGX,MAAM,iBAAiB,GAAG;IACxB,aAAa;IACb,SAAS;IACT,OAAO;IACP,OAAO;IACP,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;IACN,UAAU;IACV,gBAAgB;IAChB,UAAU;IACV,OAAO;CACC,CAAC;AAEX,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,IAAY;;IACzC,OAAO,MAAA,iBAAiB,CAAC,IAAI,CAAC,mCAAI,MAAM,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;AAC1F,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,aAAa,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC;IACf,UAAU,EAAE,CAAC;CACL,CAAC;AAGX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,CAAC;IACd,cAAc,EAAE,CAAC;IACjB,MAAM,EAAE,CAAC;CACD,CAAC;AAGX,qFAAqF;AACrF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;CACC,CAAC","sourcesContent":["// Twins of G9SyncData.Abstractions/G9SyncEnums.cs and the protocol enums. Values are the wire numbers; the objects\n// give them names (const objects, not TypeScript enums, so the shipped JavaScript stays plain ES2019).\n\n/** The portable column types (`G9ESyncColumnType`). */\nexport const G9ESyncColumnType = {\n Unspecified: 0,\n Boolean: 1,\n Int32: 2,\n Int64: 3,\n Double: 4,\n Decimal: 5,\n String: 6,\n Guid: 7,\n DateTime: 8,\n DateTimeOffset: 9,\n TimeSpan: 10,\n Bytes: 11,\n} as const;\nexport type G9ESyncColumnType = (typeof G9ESyncColumnType)[keyof typeof G9ESyncColumnType];\n\nconst COLUMN_TYPE_NAMES = [\n 'Unspecified',\n 'Boolean',\n 'Int32',\n 'Int64',\n 'Double',\n 'Decimal',\n 'String',\n 'Guid',\n 'DateTime',\n 'DateTimeOffset',\n 'TimeSpan',\n 'Bytes',\n] as const;\n\n/** The .NET enum member name of a column type (`G9ESyncColumnType.ToString()`), e.g. `Guid`. */\nexport function columnTypeName(type: number): string {\n return COLUMN_TYPE_NAMES[type] ?? String(type);\n}\n\n/** True for a defined, specified column type. */\nexport function isColumnType(type: unknown): type is G9ESyncColumnType {\n return typeof type === 'number' && type >= 1 && type <= 11 && Math.floor(type) === type;\n}\n\n/** Which way rows of a table may travel (`G9ESyncDirection`). */\nexport const G9ESyncDirection = {\n Bidirectional: 0,\n DownloadOnly: 1,\n UploadOnly: 2,\n} as const;\nexport type G9ESyncDirection = (typeof G9ESyncDirection)[keyof typeof G9ESyncDirection];\n\n/** How the server resolves a conflicting upload (`G9EConflictPolicy`). */\nexport const G9EConflictPolicy = {\n ServerWins: 0,\n ClientWins: 1,\n ColumnMerge: 2,\n LastWriterWins: 3,\n Custom: 4,\n} as const;\nexport type G9EConflictPolicy = (typeof G9EConflictPolicy)[keyof typeof G9EConflictPolicy];\n\n/** What the client does with a row that left a subscription (`G9EMoveOutPolicy`). */\nexport const G9EMoveOutPolicy = {\n Delete: 0,\n Keep: 1,\n} as const;\nexport type G9EMoveOutPolicy = (typeof G9EMoveOutPolicy)[keyof typeof G9EMoveOutPolicy];\n"]}
1
+ {"version":3,"file":"enums.js","sourceRoot":"","sources":["../../src/abstractions/enums.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,uGAAuG;AAEvG,uDAAuD;AACvD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,WAAW,EAAE,CAAC;IACd,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;IACX,cAAc,EAAE,CAAC;IACjB,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,EAAE;CACD,CAAC;AAGX,MAAM,iBAAiB,GAAG;IACxB,aAAa;IACb,SAAS;IACT,OAAO;IACP,OAAO;IACP,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;IACN,UAAU;IACV,gBAAgB;IAChB,UAAU;IACV,OAAO;CACC,CAAC;AAEX,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,IAAY;;IACzC,OAAO,MAAA,iBAAiB,CAAC,IAAI,CAAC,mCAAI,MAAM,CAAC,IAAI,CAAC,CAAC;AACjD,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;AAC1F,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,aAAa,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC;IACf,UAAU,EAAE,CAAC;CACL,CAAC;AAGX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,CAAC;IACd,cAAc,EAAE,CAAC;IACjB,MAAM,EAAE,CAAC;CACD,CAAC;AAGX,qFAAqF;AACrF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;CACC,CAAC","sourcesContent":["// Twins of G9SyncData.Abstractions/G9SyncEnums.cs and the protocol enums. Values are the wire numbers; the objects\r\n// give them names (const objects, not TypeScript enums, so the shipped JavaScript stays plain ES2019).\r\n\r\n/** The portable column types (`G9ESyncColumnType`). */\r\nexport const G9ESyncColumnType = {\r\n Unspecified: 0,\r\n Boolean: 1,\r\n Int32: 2,\r\n Int64: 3,\r\n Double: 4,\r\n Decimal: 5,\r\n String: 6,\r\n Guid: 7,\r\n DateTime: 8,\r\n DateTimeOffset: 9,\r\n TimeSpan: 10,\r\n Bytes: 11,\r\n} as const;\r\nexport type G9ESyncColumnType = (typeof G9ESyncColumnType)[keyof typeof G9ESyncColumnType];\r\n\r\nconst COLUMN_TYPE_NAMES = [\r\n 'Unspecified',\r\n 'Boolean',\r\n 'Int32',\r\n 'Int64',\r\n 'Double',\r\n 'Decimal',\r\n 'String',\r\n 'Guid',\r\n 'DateTime',\r\n 'DateTimeOffset',\r\n 'TimeSpan',\r\n 'Bytes',\r\n] as const;\r\n\r\n/** The .NET enum member name of a column type (`G9ESyncColumnType.ToString()`), e.g. `Guid`. */\r\nexport function columnTypeName(type: number): string {\r\n return COLUMN_TYPE_NAMES[type] ?? String(type);\r\n}\r\n\r\n/** True for a defined, specified column type. */\r\nexport function isColumnType(type: unknown): type is G9ESyncColumnType {\r\n return typeof type === 'number' && type >= 1 && type <= 11 && Math.floor(type) === type;\r\n}\r\n\r\n/** Which way rows of a table may travel (`G9ESyncDirection`). */\r\nexport const G9ESyncDirection = {\r\n Bidirectional: 0,\r\n DownloadOnly: 1,\r\n UploadOnly: 2,\r\n} as const;\r\nexport type G9ESyncDirection = (typeof G9ESyncDirection)[keyof typeof G9ESyncDirection];\r\n\r\n/** How the server resolves a conflicting upload (`G9EConflictPolicy`). */\r\nexport const G9EConflictPolicy = {\r\n ServerWins: 0,\r\n ClientWins: 1,\r\n ColumnMerge: 2,\r\n LastWriterWins: 3,\r\n Custom: 4,\r\n} as const;\r\nexport type G9EConflictPolicy = (typeof G9EConflictPolicy)[keyof typeof G9EConflictPolicy];\r\n\r\n/** What the client does with a row that left a subscription (`G9EMoveOutPolicy`). */\r\nexport const G9EMoveOutPolicy = {\r\n Delete: 0,\r\n Keep: 1,\r\n} as const;\r\nexport type G9EMoveOutPolicy = (typeof G9EMoveOutPolicy)[keyof typeof G9EMoveOutPolicy];\r\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/abstractions/errors.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAC9G,yEAAyE;AAEzE,gHAAgH;AAChH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,cAAc,EAAE,iBAAiB;IACjC,WAAW,EAAE,eAAe;IAC5B,QAAQ,EAAE,cAAc;IAExB,YAAY,EAAE,cAAc;IAC5B,eAAe,EAAE,iBAAiB;IAClC,gBAAgB,EAAE,kBAAkB;IAEpC,YAAY,EAAE,mBAAmB;IACjC,qBAAqB,EAAE,iBAAiB;IACxC,YAAY,EAAE,uBAAuB;IACrC,iHAAiH;IACjH,iBAAiB,EAAE,yBAAyB;IAC5C,sFAAsF;IACtF,UAAU,EAAE,cAAc;IAE1B,mBAAmB,EAAE,iBAAiB;IACtC,iBAAiB,EAAE,eAAe;IAClC,qBAAqB,EAAE,mBAAmB;IAE1C,WAAW,EAAE,eAAe;IAC5B,aAAa,EAAE,iBAAiB;IAChC,iBAAiB,EAAE,cAAc;IACjC,YAAY,EAAE,mBAAmB;IACjC,YAAY,EAAE,mBAAmB;IAEjC,gBAAgB,EAAE,oBAAoB;IACtC,gBAAgB,EAAE,oBAAoB;IACtC,gBAAgB,EAAE,oBAAoB;IACtC,eAAe,EAAE,mBAAmB;IACpC,aAAa,EAAE,kBAAkB;IACjC,kHAAkH;IAClH,cAAc,EAAE,mBAAmB;IACnC,eAAe,EAAE,mBAAmB;IAEpC,cAAc,EAAE,iBAAiB;IACjC,eAAe,EAAE,kBAAkB;IAEnC,kBAAkB,EAAE,qBAAqB;IACzC,eAAe,EAAE,iBAAiB;IAClC,eAAe,EAAE,iBAAiB;IAClC,wBAAwB,EAAE,kBAAkB;IAE5C,mHAAmH;IACnH,uBAAuB,EAAE,sBAAsB;IAC/C,0HAA0H;IAC1H,mBAAmB,EAAE,kBAAkB;IACvC,eAAe,EAAE,iBAAiB;IAClC,wHAAwH;IACxH,iBAAiB,EAAE,mBAAmB;IACtC,iEAAiE;IACjE,kBAAkB,EAAE,qBAAqB;IACzC,sFAAsF;IACtF,cAAc,EAAE,gBAAgB;IAEhC,OAAO,EAAE,aAAa;CACd,CAAC;AAEX,MAAM,SAAS,GAA6B;IAC1C,iBAAiB,EAAE,IAAI;IACvB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,IAAI;IACpB,mBAAmB,EAAE,IAAI;IACzB,eAAe,EAAE,IAAI;IACrB,iBAAiB,EAAE,IAAI;IACvB,cAAc,EAAE,IAAI;IACpB,gBAAgB,EAAE,IAAI;CACvB,CAAC;AAEF,4HAA4H;AAC5H,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;AAClC,CAAC;AAYD;;;GAGG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAQxC,YAAY,IAAY,EAAE,OAAe,EAAE,OAAgC;;QACzE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,mCAAI,eAAe,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO,GAAG,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,OAAO,mCAAI,IAAI,CAAC;QACxC,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;YAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QACxH,CAAC;QACD,+EAA+E;QAC/E,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,oGAAoG;AACpG,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,CACL,KAAK,YAAY,eAAe;QAChC,CAAC,OAAO,KAAK,KAAK,QAAQ;YACxB,KAAK,KAAK,IAAI;YACb,KAA4B,CAAC,IAAI,KAAK,iBAAiB;YACxD,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAAC,CAC1D,CAAC;AACJ,CAAC","sourcesContent":["// Twins of G9SyncData.Abstractions/G9SyncErrorCodes.cs and G9CSyncException.cs. A code is a public API: never\n// repurpose one, only add — and add it on both sides in the same change.\n\n/** Stable, support-facing error codes. The client classifies failures by these codes, never by message text. */\nexport const G9SyncErrorCodes = {\n NetworkFailure: 'G9S-NET-FAILURE',\n ServerError: 'G9S-SRV-ERROR',\n Canceled: 'G9S-CANCELED',\n\n Unauthorized: 'G9S-AUTH-401',\n UpgradeRequired: 'G9S-VER-UPGRADE',\n ProtocolMismatch: 'G9S-VER-PROTOCOL',\n\n ModelInvalid: 'G9S-MODEL-INVALID',\n SchemaUpgradeRequired: 'G9S-SCH-UPGRADE',\n UnknownTable: 'G9S-SCH-UNKNOWN-TABLE',\n /** A column declared with a type the server's column does not have (ADR-0061): it syncs in neither direction. */\n ModelTypeMismatch: 'G9S-MODEL-TYPE-MISMATCH',\n /** The server is at its limit of concurrent pulls or pushes (ADR-0062). Retryable. */\n ServerBusy: 'G9S-SRV-BUSY',\n\n UnknownSubscription: 'G9S-SUB-UNKNOWN',\n InvalidParameters: 'G9S-SUB-PARAM',\n SubscriptionForbidden: 'G9S-SUB-FORBIDDEN',\n\n ResyncEpoch: 'G9S-RSY-EPOCH',\n ResyncHorizon: 'G9S-RSY-HORIZON',\n ResyncAuthChanged: 'G9S-RSY-AUTH',\n TokenInvalid: 'G9S-TOKEN-INVALID',\n TokenExpired: 'G9S-TOKEN-EXPIRED',\n\n RejectValidation: 'G9S-REJ-VALIDATION',\n RejectConstraint: 'G9S-REJ-CONSTRAINT',\n RejectPermission: 'G9S-REJ-PERMISSION',\n RejectDirection: 'G9S-REJ-DIRECTION',\n RejectTooMany: 'G9S-REJ-TOO-MANY',\n /** One row is larger than the server accepts in a push: rejected on the device, every other row still uploads. */\n RejectTooLarge: 'G9S-REJ-TOO-LARGE',\n RejectMalformed: 'G9S-REJ-MALFORMED',\n\n CommandUnknown: 'G9S-CMD-UNKNOWN',\n CommandRejected: 'G9S-CMD-REJECTED',\n\n ConflictServerWins: 'G9S-CON-SERVER-WINS',\n ConflictColumns: 'G9S-CON-COLUMNS',\n ConflictDeleted: 'G9S-CON-DELETED',\n ConflictResolverRejected: 'G9S-CON-RESOLVER',\n\n /** The server could not build or serve a planned snapshot file (ADR-0072); the client replans the pull as rows. */\n SnapshotFileUnavailable: 'G9S-FILE-UNAVAILABLE',\n /** A downloaded snapshot file failed the device's checks; nothing of it was applied and the pull is replanned as rows. */\n SnapshotFileCorrupt: 'G9S-FILE-CORRUPT',\n LocalStoreError: 'G9S-LOCAL-STORE',\n /** The device database is damaged or not a database (ADR-0068). Export what is readable and rebuild from the server. */\n LocalStoreCorrupt: 'G9S-LOCAL-CORRUPT',\n /** The device ran out of storage while the store was writing. */\n LocalStoreDiskFull: 'G9S-LOCAL-DISK-FULL',\n /** Another connection held the write lock longer than the busy timeout. Retryable. */\n LocalStoreBusy: 'G9S-LOCAL-BUSY',\n\n Unknown: 'G9S-UNKNOWN',\n} as const;\n\nconst RETRYABLE: { [code: string]: true } = {\n 'G9S-NET-FAILURE': true,\n 'G9S-SRV-ERROR': true,\n 'G9S-SRV-BUSY': true,\n 'G9S-TOKEN-EXPIRED': true,\n 'G9S-RSY-EPOCH': true,\n 'G9S-RSY-HORIZON': true,\n 'G9S-RSY-AUTH': true,\n 'G9S-LOCAL-BUSY': true,\n};\n\n/** True for failures a later attempt may succeed at without any change to data or code (`G9SyncErrorCodes.IsRetryable`). */\nexport function isRetryableCode(code: string): boolean {\n return RETRYABLE[code] === true;\n}\n\n/** Options of {@link G9SyncException}. */\nexport interface G9SyncExceptionOptions {\n /** Overrides the default retryability of the code. */\n retryable?: boolean;\n /** Structured detail (JSON or text) for logs. Never shown to end users. */\n details?: string | null;\n /** The underlying failure. */\n cause?: unknown;\n}\n\n/**\n * The single error type G9SyncData throws for expected failures (twin of `G9CSyncException`). Always carries a stable\n * {@link code}; callers switch on the code, never on the message.\n */\nexport class G9SyncException extends Error {\n /** Stable error code (see {@link G9SyncErrorCodes}). */\n readonly code: string;\n /** True when retrying without changes may succeed. */\n readonly retryable: boolean;\n /** Optional structured detail (JSON or text) for logs. */\n readonly details: string | null;\n\n constructor(code: string, message: string, options?: G9SyncExceptionOptions) {\n super(message);\n this.name = 'G9SyncException';\n this.code = code;\n this.retryable = options?.retryable ?? isRetryableCode(code);\n this.details = options?.details ?? null;\n if (options && 'cause' in options) {\n Object.defineProperty(this, 'cause', { value: options.cause, enumerable: false, writable: true, configurable: true });\n }\n // ES2019 output: keep instanceof working for subclasses compiled to functions.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** True for a {@link G9SyncException} (also across bundles that each carry a copy of the class). */\nexport function isSyncException(error: unknown): error is G9SyncException {\n return (\n error instanceof G9SyncException ||\n (typeof error === 'object' &&\n error !== null &&\n (error as { name?: unknown }).name === 'G9SyncException' &&\n typeof (error as { code?: unknown }).code === 'string')\n );\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/abstractions/errors.ts"],"names":[],"mappings":"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","sourcesContent":["// Twins of G9SyncData.Abstractions/G9SyncErrorCodes.cs and G9CSyncException.cs. A code is a public API: never\r\n// repurpose one, only add — and add it on both sides in the same change.\r\n\r\n/** Stable, support-facing error codes. The client classifies failures by these codes, never by message text. */\r\nexport const G9SyncErrorCodes = {\r\n NetworkFailure: 'G9S-NET-FAILURE',\r\n ServerError: 'G9S-SRV-ERROR',\r\n Canceled: 'G9S-CANCELED',\r\n\r\n Unauthorized: 'G9S-AUTH-401',\r\n UpgradeRequired: 'G9S-VER-UPGRADE',\r\n ProtocolMismatch: 'G9S-VER-PROTOCOL',\r\n\r\n ModelInvalid: 'G9S-MODEL-INVALID',\r\n SchemaUpgradeRequired: 'G9S-SCH-UPGRADE',\r\n UnknownTable: 'G9S-SCH-UNKNOWN-TABLE',\r\n /** A column declared with a type the server's column does not have (ADR-0061): it syncs in neither direction. */\r\n ModelTypeMismatch: 'G9S-MODEL-TYPE-MISMATCH',\r\n /** The server is at its limit of concurrent pulls or pushes (ADR-0062). Retryable. */\r\n ServerBusy: 'G9S-SRV-BUSY',\r\n\r\n UnknownSubscription: 'G9S-SUB-UNKNOWN',\r\n InvalidParameters: 'G9S-SUB-PARAM',\r\n SubscriptionForbidden: 'G9S-SUB-FORBIDDEN',\r\n\r\n ResyncEpoch: 'G9S-RSY-EPOCH',\r\n ResyncHorizon: 'G9S-RSY-HORIZON',\r\n ResyncAuthChanged: 'G9S-RSY-AUTH',\r\n TokenInvalid: 'G9S-TOKEN-INVALID',\r\n TokenExpired: 'G9S-TOKEN-EXPIRED',\r\n\r\n RejectValidation: 'G9S-REJ-VALIDATION',\r\n RejectConstraint: 'G9S-REJ-CONSTRAINT',\r\n RejectPermission: 'G9S-REJ-PERMISSION',\r\n RejectDirection: 'G9S-REJ-DIRECTION',\r\n RejectTooMany: 'G9S-REJ-TOO-MANY',\r\n /** One row is larger than the server accepts in a push: rejected on the device, every other row still uploads. */\r\n RejectTooLarge: 'G9S-REJ-TOO-LARGE',\r\n RejectMalformed: 'G9S-REJ-MALFORMED',\r\n\r\n CommandUnknown: 'G9S-CMD-UNKNOWN',\r\n CommandRejected: 'G9S-CMD-REJECTED',\r\n\r\n ConflictServerWins: 'G9S-CON-SERVER-WINS',\r\n ConflictColumns: 'G9S-CON-COLUMNS',\r\n ConflictDeleted: 'G9S-CON-DELETED',\r\n ConflictResolverRejected: 'G9S-CON-RESOLVER',\r\n\r\n /** The server could not build or serve a planned snapshot file (ADR-0072); the client replans the pull as rows. */\r\n SnapshotFileUnavailable: 'G9S-FILE-UNAVAILABLE',\r\n /** A downloaded snapshot file failed the device's checks; nothing of it was applied and the pull is replanned as rows. */\r\n SnapshotFileCorrupt: 'G9S-FILE-CORRUPT',\r\n LocalStoreError: 'G9S-LOCAL-STORE',\r\n /** The device database is damaged or not a database (ADR-0068). Export what is readable and rebuild from the server. */\r\n LocalStoreCorrupt: 'G9S-LOCAL-CORRUPT',\r\n /** The device ran out of storage while the store was writing. */\r\n LocalStoreDiskFull: 'G9S-LOCAL-DISK-FULL',\r\n /** Another connection held the write lock longer than the busy timeout. Retryable. */\r\n LocalStoreBusy: 'G9S-LOCAL-BUSY',\r\n\r\n Unknown: 'G9S-UNKNOWN',\r\n} as const;\r\n\r\nconst RETRYABLE: { [code: string]: true } = {\r\n 'G9S-NET-FAILURE': true,\r\n 'G9S-SRV-ERROR': true,\r\n 'G9S-SRV-BUSY': true,\r\n 'G9S-TOKEN-EXPIRED': true,\r\n 'G9S-RSY-EPOCH': true,\r\n 'G9S-RSY-HORIZON': true,\r\n 'G9S-RSY-AUTH': true,\r\n 'G9S-LOCAL-BUSY': true,\r\n};\r\n\r\n/** True for failures a later attempt may succeed at without any change to data or code (`G9SyncErrorCodes.IsRetryable`). */\r\nexport function isRetryableCode(code: string): boolean {\r\n return RETRYABLE[code] === true;\r\n}\r\n\r\n/** Options of {@link G9SyncException}. */\r\nexport interface G9SyncExceptionOptions {\r\n /** Overrides the default retryability of the code. */\r\n retryable?: boolean;\r\n /** Structured detail (JSON or text) for logs. Never shown to end users. */\r\n details?: string | null;\r\n /** The underlying failure. */\r\n cause?: unknown;\r\n}\r\n\r\n/**\r\n * The single error type G9SyncData throws for expected failures (twin of `G9CSyncException`). Always carries a stable\r\n * {@link code}; callers switch on the code, never on the message.\r\n */\r\nexport class G9SyncException extends Error {\r\n /** Stable error code (see {@link G9SyncErrorCodes}). */\r\n readonly code: string;\r\n /** True when retrying without changes may succeed. */\r\n readonly retryable: boolean;\r\n /** Optional structured detail (JSON or text) for logs. */\r\n readonly details: string | null;\r\n\r\n constructor(code: string, message: string, options?: G9SyncExceptionOptions) {\r\n super(message);\r\n this.name = 'G9SyncException';\r\n this.code = code;\r\n this.retryable = options?.retryable ?? isRetryableCode(code);\r\n this.details = options?.details ?? null;\r\n if (options && 'cause' in options) {\r\n Object.defineProperty(this, 'cause', { value: options.cause, enumerable: false, writable: true, configurable: true });\r\n }\r\n // ES2019 output: keep instanceof working for subclasses compiled to functions.\r\n Object.setPrototypeOf(this, new.target.prototype);\r\n }\r\n}\r\n\r\n/** True for a {@link G9SyncException} (also across bundles that each carry a copy of the class). */\r\nexport function isSyncException(error: unknown): error is G9SyncException {\r\n return (\r\n error instanceof G9SyncException ||\r\n (typeof error === 'object' &&\r\n error !== null &&\r\n (error as { name?: unknown }).name === 'G9SyncException' &&\r\n typeof (error as { code?: unknown }).code === 'string')\r\n );\r\n}\r\n"]}