@g9tm/syncdata 1.5.19 → 1.5.21
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/PARITY.md +9 -0
- package/dist/client/engine.d.ts +23 -1
- package/dist/client/engine.js +82 -1
- package/dist/client/engine.js.map +1 -1
- package/dist/client/storeTypes.d.ts +9 -0
- package/dist/client/storeTypes.js +5 -0
- package/dist/client/storeTypes.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol/dtos.d.ts +16 -0
- package/dist/protocol/dtos.js.map +1 -1
- package/dist/protocol/wire.js +6 -0
- package/dist/protocol/wire.js.map +1 -1
- package/dist/signalr/transport.d.ts +9 -0
- package/dist/signalr/transport.js +34 -3
- package/dist/signalr/transport.js.map +1 -1
- package/dist/sqlite/revocation.d.ts +14 -0
- package/dist/sqlite/revocation.js +76 -0
- package/dist/sqlite/revocation.js.map +1 -1
- package/dist/sqlite/snapshotFiles.js.map +1 -1
- package/dist/sqlite/store.d.ts +2 -0
- package/dist/sqlite/store.js +12 -3
- package/dist/sqlite/store.js.map +1 -1
- package/package.json +1 -1
package/PARITY.md
CHANGED
|
@@ -48,6 +48,8 @@ it). This file maps every .NET piece to its twin and lists every deliberate diff
|
|
|
48
48
|
| A value its type cannot carry refuses only its row, on the device (LES-0088) | `findUnencodable` | `FindUnencodable`: the writer's conversions run before a row enters the batch; a failure is `G9S-REJ-MALFORMED` with the column, counted in `rejectedLocally`; `MalformedLocalValueTests` / `test/malformedLocalValue.test.ts` |
|
|
49
49
|
| Lost = pushed columns only; timestamps as instants (LES-0086) | `pushedMask`, `sameValue(…, type)` / `instant` | `PushedMask` / `SameValue(type, …)` / `Instant`: the dirty mask restricted to the batch's columns; DateTime/DateTimeOffset text in either form equal within one microsecond (TS: no BigInt - milliseconds plus 100 ns units); `HeldValueTests` / `test/heldValues.test.ts` |
|
|
50
50
|
| `SaveHelloAsync(hello, localMidpointUtc, ct)` (LES-0075) | `saveHello(hello, signal, localMidpointMs)` | the engine measures the Hello round trip in both clients and stores `serverTime - midpoint` |
|
|
51
|
+
| `RefetchRowsAsync(table, keys?)` (1.5.19, LES-0090) | `refetchRows(table, keys?)` | the same rows (clean, held: `dirty = 0 AND rejected = 0 AND deleted = 0 AND subs <> 0`) get `server_ver = -1`, their holders NeedsReconcile; the holder bitmask of .NET is one `EXISTS … (subs >> slot) & 1` statement; keys canonicalised, pages of 5000; `test/refetchAndRelease.test.ts` |
|
|
52
|
+
| `ReleaseUnclaimedRowsAsync(table, keys?, includePending)` (1.5.19) | `releaseUnclaimedRows(table, keys?, includePending)` | the same selection (`subs = 0 AND server_ver = 0`, not revoked / quarantined / ephemeral / in a pending command; pending and refused only with `includePending`), read whole before the disposal (`temp.g9_unclaimed`, key order), capture off, reason `G9ERevocationReason.Unclaimed` (4), `changedKeys` capped by `maxChangedKeysPerTable`, RowsApplied (PostStep) when anything left; `test/refetchAndRelease.test.ts`, `test/engine.test.ts`. Until 1.5.20 both were .NET only (1.5.19 shipped without the twins) |
|
|
51
53
|
| `G9CSqliteException` (`IG9SyncLocalStoreFailure`) | `G9SqliteStoreError` (`localStoreFailure`) | classified from the driver's code |
|
|
52
54
|
| `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`). After an automatic reconnect both resend the last accepted Hello so the new connection rejoins its poke groups (LES-0087: `OnReconnectedAsync` / `_rejoin`; `SignalRTransportTests.After_the_connection_reconnects_by_itself_…` / `test/transportRejoin.test.ts`) |
|
|
53
55
|
| `G9CInMemorySyncTransport`, `G9CTransportFaults` | `G9FaultInjectingTransport`, `G9TransportFaults` (`@g9tm/syncdata/testing`) | wraps a real transport (no in-process server engine in JavaScript) |
|
|
@@ -77,6 +79,13 @@ it). This file maps every .NET piece to its twin and lists every deliberate diff
|
|
|
77
79
|
would hand out, in model order) so its triggers are written in the same batch. Same objects, rows, tids and counter
|
|
78
80
|
(`test/provision.test.ts` compares both paths on fresh, reopened, evolved and repaired databases). AgriPad's 47 tables:
|
|
79
81
|
14 driver calls on every start instead of 296, 17 instead of 534 on the first.
|
|
82
|
+
- **Hello sends the model once per connection (1.5.21).** `G9CSignalRSyncTransport.HelloAsync` / `G9SignalRSyncTransport.hello`
|
|
83
|
+
strip the tables on a later Hello of the same connection (`_modelOnConnection`, keyed by the connection id and the engine's one
|
|
84
|
+
model list/array) and resend them inside the call on `ModelRequired`; the engines keep the last answer in memory
|
|
85
|
+
(`G9CSyncEngine.HelloCache` / `_lastAnswer`), send `KnownTablesHash` / `KnownDefinitionsHash` and put back what an "unchanged"
|
|
86
|
+
answer left out (`RestoreUnchanged` / `restoreUnchanged`); both stores skip `definitions_json`, `tables_json` and the per-table
|
|
87
|
+
updates of an unchanged answer. Tests: `HelloModelOnceTests` / `test/transportModelOnce.test.ts`; the golden Hello samples carry
|
|
88
|
+
the six new fields.
|
|
80
89
|
- **Per-run bookkeeping.** `saveHello` writes the meta rows, the definitions' move-out policies and every negotiated
|
|
81
90
|
table (with its capture triggers) in ONE batch, after one read for the only per-table question (does a table with
|
|
82
91
|
newly negotiated columns hold rows?); the rows an apply protects (`readRowsChangedAfter`) are read with one
|
package/dist/client/engine.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { G9SyncParameters } from '../abstractions/parameters.js';
|
|
|
2
2
|
import { type G9DtHelloResult, type G9DtNegotiatedTable, type G9DtSubscriptionState } from '../protocol/dtos.js';
|
|
3
3
|
import type { G9SyncClientModel } from './model.js';
|
|
4
4
|
import { G9SyncRun, type G9RowsAppliedEvent, type G9SubscriptionOptions, type G9SyncEngineOptions, type G9SyncHealth, type G9SyncRequest, type G9SyncRunResult } from './runs.js';
|
|
5
|
-
import { type G9ChangeFeedEntry, type G9ChangeFeedKeyFilter, type G9LocalCommand, type G9LocalCommandWriter, type G9ELocalCommandState, type G9PendingSummary, type G9QuarantinedRow, type G9RetainedRow, type G9SyncClientStore, type G9SyncRejection, type G9SyncSubscriptionRecord } from './storeTypes.js';
|
|
5
|
+
import { G9ApplyReport, type G9ChangeFeedEntry, type G9ChangeFeedKeyFilter, type G9LocalCommand, type G9LocalCommandWriter, type G9ELocalCommandState, type G9PendingSummary, type G9QuarantinedRow, type G9RetainedRow, type G9SyncClientStore, type G9SyncRejection, type G9SyncSubscriptionRecord } from './storeTypes.js';
|
|
6
6
|
import type { G9SyncTransport } from './transport.js';
|
|
7
7
|
/**
|
|
8
8
|
* The client sync engine (`G9CSyncEngine`): one per local database. Owns a prioritised run queue, so status queries
|
|
@@ -32,6 +32,13 @@ export declare class G9SyncEngine {
|
|
|
32
32
|
private _disposed;
|
|
33
33
|
/** The model problems last logged, so the warning is said once per distinct set (ADR-0061). */
|
|
34
34
|
private _reportedModelProblems;
|
|
35
|
+
/**
|
|
36
|
+
* What Hello does not send again (1.5.21, TR-0069; twin of .NET `G9CSyncEngine.HelloCache`): the last negotiated tables and definitions,
|
|
37
|
+
* with the server's ids for them. In memory only: the first Hello of a process gets them in full.
|
|
38
|
+
*/
|
|
39
|
+
private _lastAnswer;
|
|
40
|
+
/** The model as Hello sends it, built once: the transport recognises an unchanged model by this very array. */
|
|
41
|
+
private _wireTables;
|
|
35
42
|
constructor(store: G9SyncClientStore, transport: G9SyncTransport, model: G9SyncClientModel, options?: G9SyncEngineOptions);
|
|
36
43
|
get clientId(): string;
|
|
37
44
|
get isRunning(): boolean;
|
|
@@ -91,6 +98,21 @@ export declare class G9SyncEngine {
|
|
|
91
98
|
getChangeFeedFloor(signal?: AbortSignal): Promise<number>;
|
|
92
99
|
adoptExistingRows(signal?: AbortSignal): Promise<number>;
|
|
93
100
|
markDirty(localTable: string, keys: readonly string[], signal?: AbortSignal): Promise<number>;
|
|
101
|
+
/**
|
|
102
|
+
* `RefetchRowsAsync` (1.5.19): has the next sync fetch these rows again and overwrite their local values with the server's
|
|
103
|
+
* (all of a table, or `keys`) - for a server whose READ of a column changed while the rows did not, e.g. a column it now
|
|
104
|
+
* serves empty (a reconcile compares key + version, never values). Only clean rows a subscription holds; a row with a pending
|
|
105
|
+
* change, a rejection or a delete is left alone. Nothing is uploaded. Their subscriptions become NeedsReconcile, so the next
|
|
106
|
+
* sync of any kind does it, offline in between or not. Returns the rows marked.
|
|
107
|
+
*/
|
|
108
|
+
refetchRows(localTable: string, keys?: readonly string[] | null, signal?: AbortSignal): Promise<number>;
|
|
109
|
+
/**
|
|
110
|
+
* `ReleaseUnclaimedRowsAsync` (1.5.19): removes rows of one table (all, or `keys`) that no subscription holds and the server
|
|
111
|
+
* never claimed (`server_ver = 0`: adopted from the device's own table, never confirmed since). Locally only: capture is off,
|
|
112
|
+
* nothing is uploaded. A row with a pending (or refused) change stays unless `includePending` - then the revocation policy
|
|
113
|
+
* decides (by default quarantined, the refusal with it). Raises RowsApplied (PostStep) when anything left.
|
|
114
|
+
*/
|
|
115
|
+
releaseUnclaimedRows(localTable: string, keys?: readonly string[] | null, includePending?: boolean, signal?: AbortSignal): Promise<G9ApplyReport>;
|
|
94
116
|
getHealth(signal?: AbortSignal): Promise<G9SyncHealth>;
|
|
95
117
|
dispose(): Promise<void>;
|
|
96
118
|
private _signal;
|
package/dist/client/engine.js
CHANGED
|
@@ -42,6 +42,13 @@ export class G9SyncEngine {
|
|
|
42
42
|
this._disposed = false;
|
|
43
43
|
/** The model problems last logged, so the warning is said once per distinct set (ADR-0061). */
|
|
44
44
|
this._reportedModelProblems = null;
|
|
45
|
+
/**
|
|
46
|
+
* What Hello does not send again (1.5.21, TR-0069; twin of .NET `G9CSyncEngine.HelloCache`): the last negotiated tables and definitions,
|
|
47
|
+
* with the server's ids for them. In memory only: the first Hello of a process gets them in full.
|
|
48
|
+
*/
|
|
49
|
+
this._lastAnswer = null;
|
|
50
|
+
/** The model as Hello sends it, built once: the transport recognises an unchanged model by this very array. */
|
|
51
|
+
this._wireTables = null;
|
|
45
52
|
if (!store)
|
|
46
53
|
throw new TypeError('A store is required.');
|
|
47
54
|
if (!transport)
|
|
@@ -276,6 +283,34 @@ export class G9SyncEngine {
|
|
|
276
283
|
await this.initialize(signal);
|
|
277
284
|
return this._store.markDirty(localTable, keys, signal);
|
|
278
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* `RefetchRowsAsync` (1.5.19): has the next sync fetch these rows again and overwrite their local values with the server's
|
|
288
|
+
* (all of a table, or `keys`) - for a server whose READ of a column changed while the rows did not, e.g. a column it now
|
|
289
|
+
* serves empty (a reconcile compares key + version, never values). Only clean rows a subscription holds; a row with a pending
|
|
290
|
+
* change, a rejection or a delete is left alone. Nothing is uploaded. Their subscriptions become NeedsReconcile, so the next
|
|
291
|
+
* sync of any kind does it, offline in between or not. Returns the rows marked.
|
|
292
|
+
*/
|
|
293
|
+
async refetchRows(localTable, keys = null, signal) {
|
|
294
|
+
if (!localTable || !localTable.trim())
|
|
295
|
+
throw new Error('localTable is required');
|
|
296
|
+
await this.initialize(signal);
|
|
297
|
+
return this._store.refetchRows(localTable, keys, signal);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* `ReleaseUnclaimedRowsAsync` (1.5.19): removes rows of one table (all, or `keys`) that no subscription holds and the server
|
|
301
|
+
* never claimed (`server_ver = 0`: adopted from the device's own table, never confirmed since). Locally only: capture is off,
|
|
302
|
+
* nothing is uploaded. A row with a pending (or refused) change stays unless `includePending` - then the revocation policy
|
|
303
|
+
* decides (by default quarantined, the refusal with it). Raises RowsApplied (PostStep) when anything left.
|
|
304
|
+
*/
|
|
305
|
+
async releaseUnclaimedRows(localTable, keys = null, includePending = false, signal) {
|
|
306
|
+
if (!localTable || !localTable.trim())
|
|
307
|
+
throw new Error('localTable is required');
|
|
308
|
+
await this.initialize(signal);
|
|
309
|
+
const report = await this._store.releaseUnclaimedRows(localTable, keys, includePending, signal);
|
|
310
|
+
if (report.revokedDeleted + report.revokedQuarantined + report.revokedRetained > 0)
|
|
311
|
+
this._raiseRowsApplied(report, G9ESyncPhase.PostStep);
|
|
312
|
+
return report;
|
|
313
|
+
}
|
|
279
314
|
async getHealth(signal) {
|
|
280
315
|
await this.initialize(signal);
|
|
281
316
|
const state = await this._store.getServerState(signal);
|
|
@@ -431,6 +466,7 @@ export class G9SyncEngine {
|
|
|
431
466
|
}
|
|
432
467
|
// ================================================================ execution
|
|
433
468
|
async _executeRun(run) {
|
|
469
|
+
var _a, _b, _c;
|
|
434
470
|
const request = run.request;
|
|
435
471
|
const startedUtcMs = Date.now();
|
|
436
472
|
const changes = new G9ApplyReport();
|
|
@@ -465,10 +501,19 @@ export class G9SyncEngine {
|
|
|
465
501
|
hostScope = await this._options.hooks.beginRun(run, ct);
|
|
466
502
|
// ---------------------------------------------------------------- 1. connect + hello
|
|
467
503
|
tracker.enter(G9ESyncPhase.Connecting);
|
|
504
|
+
// Where the Connecting phase goes (AgriPad TR-0069, 2026-11-13: «Connecting takes long»): each step is timed and the
|
|
505
|
+
// split logged once per run (event 20030) — the transport, the ephemeral sweep, reading the subscriptions, the
|
|
506
|
+
// Hello round trip and the save of its answer are separate costs with separate cures.
|
|
507
|
+
const connectT0 = Date.now();
|
|
468
508
|
await this._ensureConnected(ct);
|
|
509
|
+
const connectT1 = Date.now();
|
|
469
510
|
await this._store.collectExpiredEphemeral(Date.now(), ct);
|
|
511
|
+
const connectT2 = Date.now();
|
|
470
512
|
const subscriptions = await this._store.getSubscriptions(ct);
|
|
513
|
+
const connectT3 = Date.now();
|
|
471
514
|
let helloMidpointMs = Date.now();
|
|
515
|
+
const known = this._lastAnswer;
|
|
516
|
+
const wireTables = (_a = this._wireTables) !== null && _a !== void 0 ? _a : (this._wireTables = this.model.tables.map((t) => t.toWire()));
|
|
472
517
|
const hello = await this._withRetry(async (token) => {
|
|
473
518
|
// The round trip of the attempt that answered: its midpoint is the local time of the server's clock reading (LES-0075).
|
|
474
519
|
const sent = Date.now();
|
|
@@ -476,15 +521,34 @@ export class G9SyncEngine {
|
|
|
476
521
|
clientId: this._clientId,
|
|
477
522
|
appVersion: this._options.appVersion,
|
|
478
523
|
protocolVersion: 1,
|
|
479
|
-
tables:
|
|
524
|
+
tables: wireTables,
|
|
480
525
|
subscriptions: subscriptions.map(toSubscriptionRef),
|
|
526
|
+
knownTablesHash: known ? known.tablesHash : null,
|
|
527
|
+
knownDefinitionsHash: known ? known.definitionsHash : null,
|
|
481
528
|
}, token);
|
|
482
529
|
helloMidpointMs = sent + (Date.now() - sent) / 2;
|
|
483
530
|
return answer;
|
|
484
531
|
}, ct);
|
|
485
532
|
if (hello.error)
|
|
486
533
|
throw syncErrorToException(hello.error);
|
|
534
|
+
const unchanged = (hello.tablesUnchanged ? 'tables' : '') + (hello.definitionsUnchanged ? (hello.tablesUnchanged ? '+' : '') + 'definitions' : '');
|
|
535
|
+
restoreUnchanged(hello, known);
|
|
536
|
+
const connectT4 = Date.now();
|
|
487
537
|
await this._store.saveHello(hello, ct, helloMidpointMs);
|
|
538
|
+
this._lastAnswer =
|
|
539
|
+
hello.tablesHash || hello.definitionsHash
|
|
540
|
+
? {
|
|
541
|
+
tablesHash: (_b = hello.tablesHash) !== null && _b !== void 0 ? _b : null,
|
|
542
|
+
tables: hello.tables,
|
|
543
|
+
unknownTables: hello.unknownTables,
|
|
544
|
+
definitionsHash: (_c = hello.definitionsHash) !== null && _c !== void 0 ? _c : null,
|
|
545
|
+
definitions: hello.definitions,
|
|
546
|
+
}
|
|
547
|
+
: null;
|
|
548
|
+
const connectT5 = Date.now();
|
|
549
|
+
this._log('information', 20030, `Connecting ${connectT5 - connectT0} ms: transport ${connectT1 - connectT0}, ephemeral sweep ${connectT2 - connectT1}, ` +
|
|
550
|
+
`subscriptions ${connectT3 - connectT2} (${subscriptions.length}), hello ${connectT4 - connectT3} ` +
|
|
551
|
+
`(${this.model.tables.length} tables; unchanged: ${unchanged || 'none'}), save ${connectT5 - connectT4}`);
|
|
488
552
|
if (hello.unknownTables.length > 0) {
|
|
489
553
|
this._log('warning', 20016, 'The server does not know these client tables (they will not sync): ' + hello.unknownTables.join(', '));
|
|
490
554
|
}
|
|
@@ -1230,4 +1294,21 @@ async function* toChunks(batch) {
|
|
|
1230
1294
|
for (const frame of batch.frames)
|
|
1231
1295
|
yield { sequence: sequence++, payload: frame, rowCount: 0, isFinal: false };
|
|
1232
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Puts back what an "unchanged" Hello answer left out (1.5.21; twin of .NET `RestoreUnchanged`), so the rest of a run reads a complete
|
|
1299
|
+
* answer either way. An answer that says unchanged for something this client never held is a server fault.
|
|
1300
|
+
*/
|
|
1301
|
+
function restoreUnchanged(hello, known) {
|
|
1302
|
+
if (hello.tablesUnchanged) {
|
|
1303
|
+
if (!known)
|
|
1304
|
+
throw new G9SyncException(G9SyncErrorCodes.ProtocolMismatch, "The server answered 'tables unchanged' to a client that holds none.");
|
|
1305
|
+
hello.tables = known.tables;
|
|
1306
|
+
hello.unknownTables = known.unknownTables;
|
|
1307
|
+
}
|
|
1308
|
+
if (hello.definitionsUnchanged) {
|
|
1309
|
+
if (!known)
|
|
1310
|
+
throw new G9SyncException(G9SyncErrorCodes.ProtocolMismatch, "The server answered 'definitions unchanged' to a client that holds none.");
|
|
1311
|
+
hello.definitions = known.definitions;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1233
1314
|
//# sourceMappingURL=engine.js.map
|