@interop/wallet-core 0.1.0
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/LICENSE.md +20 -0
- package/README.md +83 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/space/activity.d.ts +175 -0
- package/dist/space/activity.d.ts.map +1 -0
- package/dist/space/activity.js +209 -0
- package/dist/space/activity.js.map +1 -0
- package/dist/space/collections.d.ts +65 -0
- package/dist/space/collections.d.ts.map +1 -0
- package/dist/space/collections.js +65 -0
- package/dist/space/collections.js.map +1 -0
- package/dist/space/errors.d.ts +14 -0
- package/dist/space/errors.d.ts.map +1 -0
- package/dist/space/errors.js +18 -0
- package/dist/space/errors.js.map +1 -0
- package/dist/space/index.d.ts +26 -0
- package/dist/space/index.d.ts.map +1 -0
- package/dist/space/index.js +23 -0
- package/dist/space/index.js.map +1 -0
- package/dist/space/publicLink.d.ts +17 -0
- package/dist/space/publicLink.d.ts.map +1 -0
- package/dist/space/publicLink.js +27 -0
- package/dist/space/publicLink.js.map +1 -0
- package/dist/space/wasLink.d.ts +37 -0
- package/dist/space/wasLink.d.ts.map +1 -0
- package/dist/space/wasLink.js +123 -0
- package/dist/space/wasLink.js.map +1 -0
- package/dist/sync/collections.d.ts +61 -0
- package/dist/sync/collections.d.ts.map +1 -0
- package/dist/sync/collections.js +2 -0
- package/dist/sync/collections.js.map +1 -0
- package/dist/sync/engine.d.ts +112 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +188 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/index.d.ts +33 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +29 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/pull.d.ts +68 -0
- package/dist/sync/pull.d.ts.map +1 -0
- package/dist/sync/pull.js +92 -0
- package/dist/sync/pull.js.map +1 -0
- package/dist/sync/push.d.ts +55 -0
- package/dist/sync/push.d.ts.map +1 -0
- package/dist/sync/push.js +191 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/types.d.ts +123 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +25 -0
- package/dist/sync/types.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { runPull } from './pull.js';
|
|
2
|
+
import { runPush } from './push.js';
|
|
3
|
+
const DEFAULT_BATCH_SIZE = 100;
|
|
4
|
+
const DEFAULT_BASE_DELAY_MS = 1_000;
|
|
5
|
+
const DEFAULT_MAX_DELAY_MS = 60_000;
|
|
6
|
+
function defaultSchedule(fn, delayMs) {
|
|
7
|
+
const timer = setTimeout(fn, delayMs);
|
|
8
|
+
return () => clearTimeout(timer);
|
|
9
|
+
}
|
|
10
|
+
export class SyncEngine {
|
|
11
|
+
deps;
|
|
12
|
+
status = 'idle';
|
|
13
|
+
batchSize;
|
|
14
|
+
baseDelayMs;
|
|
15
|
+
maxDelayMs;
|
|
16
|
+
schedule;
|
|
17
|
+
random;
|
|
18
|
+
running = false;
|
|
19
|
+
rerunRequested = false;
|
|
20
|
+
stopped = false;
|
|
21
|
+
failureCount = 0;
|
|
22
|
+
currentRun = null;
|
|
23
|
+
abortController = null;
|
|
24
|
+
cancelRetry = null;
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
this.batchSize = deps.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
28
|
+
this.baseDelayMs = deps.backoff?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
29
|
+
this.maxDelayMs = deps.backoff?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
30
|
+
this.schedule = deps.schedule ?? defaultSchedule;
|
|
31
|
+
this.random = deps.random ?? Math.random;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Requests a sync. Single-flight: if a cycle is in flight this only flags a
|
|
35
|
+
* rerun (so writes that land mid-cycle are not lost) and resolves with the
|
|
36
|
+
* in-flight run; otherwise it starts a fresh run. Never rejects -- failures
|
|
37
|
+
* settle into `status = 'error'` plus a scheduled backoff retry, per the
|
|
38
|
+
* local-first invariant (sync must never surface as a rejected write).
|
|
39
|
+
*
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
sync() {
|
|
43
|
+
if (this.stopped) {
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
if (this.running && this.currentRun !== null) {
|
|
47
|
+
this.rerunRequested = true;
|
|
48
|
+
return this.currentRun;
|
|
49
|
+
}
|
|
50
|
+
this.currentRun = this.run();
|
|
51
|
+
return this.currentRun;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Stops the engine: aborts any in-flight cycle (the injected signal unwinds
|
|
55
|
+
* pull/push between pages/rows), cancels a pending retry, and resets to idle.
|
|
56
|
+
* The caller drops the cached agents/ciphers so key material does not outlive
|
|
57
|
+
* the unlocked session.
|
|
58
|
+
*/
|
|
59
|
+
stop() {
|
|
60
|
+
this.stopped = true;
|
|
61
|
+
this.abortController?.abort();
|
|
62
|
+
this.clearRetry();
|
|
63
|
+
this.setStatus('idle');
|
|
64
|
+
}
|
|
65
|
+
async run() {
|
|
66
|
+
this.running = true;
|
|
67
|
+
this.clearRetry();
|
|
68
|
+
this.setStatus('syncing');
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
this.abortController = controller;
|
|
71
|
+
const { signal } = controller;
|
|
72
|
+
try {
|
|
73
|
+
do {
|
|
74
|
+
this.rerunRequested = false;
|
|
75
|
+
await this.runCycle(signal);
|
|
76
|
+
} while (this.rerunRequested && !signal.aborted && !this.stopped);
|
|
77
|
+
if (!signal.aborted && !this.stopped) {
|
|
78
|
+
this.failureCount = 0;
|
|
79
|
+
this.setStatus('synced');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Local-first: an engine failure is never fatal. A stop-driven abort is
|
|
84
|
+
// not an error; any other failure flips to `error` and schedules a retry.
|
|
85
|
+
if (!signal.aborted && !this.stopped) {
|
|
86
|
+
this.setStatus('error');
|
|
87
|
+
this.scheduleRetry();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.running = false;
|
|
92
|
+
this.currentRun = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One full replication cycle. On the very first run (never migrated) it pulls
|
|
97
|
+
* before the sweep so existing local rows hash-link to any bodies already on
|
|
98
|
+
* the server -- the sweep then only encrypts genuinely-new records
|
|
99
|
+
* (re-encrypting an existing one would mint a different content id and leave a
|
|
100
|
+
* permanent server duplicate). The unlinked-record sweep (`runLazyMigration`)
|
|
101
|
+
* runs on EVERY cycle, not just the first, so records that enter the replica
|
|
102
|
+
* outside the synced write path -- an import, or a write whose minting failed
|
|
103
|
+
* and fell back to a plain insert -- are still picked up and pushed (it is a
|
|
104
|
+
* cheap no-op when there are none). Steady state is sweep-then-push-then-pull:
|
|
105
|
+
* our own writes echo back in the same cycle's pull, idempotently.
|
|
106
|
+
*/
|
|
107
|
+
async runCycle(signal) {
|
|
108
|
+
await this.deps.ensureProvisioned();
|
|
109
|
+
if (signal.aborted) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const firstCycle = !(await this.deps.isMigrated());
|
|
113
|
+
if (firstCycle) {
|
|
114
|
+
await this.pull(signal);
|
|
115
|
+
if (signal.aborted) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
await this.deps.runLazyMigration(signal);
|
|
120
|
+
if (signal.aborted) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (firstCycle) {
|
|
124
|
+
await this.deps.stampMigrated();
|
|
125
|
+
if (signal.aborted) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const { conflictsResolved } = await runPush({
|
|
130
|
+
port: this.deps.port,
|
|
131
|
+
store: this.deps.store,
|
|
132
|
+
resolveConflict: this.deps.resolveConflict,
|
|
133
|
+
signal
|
|
134
|
+
});
|
|
135
|
+
if (signal.aborted) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// A resolved last-write-wins conflict may leave a row dirty (local-wins
|
|
139
|
+
// re-encrypt); rerun this run so the re-push settles instead of waiting for
|
|
140
|
+
// the next external trigger. Idempotent once the conflict clears.
|
|
141
|
+
if (conflictsResolved > 0) {
|
|
142
|
+
this.rerunRequested = true;
|
|
143
|
+
}
|
|
144
|
+
const applied = await this.pull(signal);
|
|
145
|
+
if (signal.aborted) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await this.deps.stampLastSynced();
|
|
149
|
+
if (applied > 0) {
|
|
150
|
+
this.deps.onPullApplied?.();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async pull(signal) {
|
|
154
|
+
const { applied } = await runPull({
|
|
155
|
+
port: this.deps.port,
|
|
156
|
+
store: this.deps.store,
|
|
157
|
+
batchSize: this.batchSize,
|
|
158
|
+
decryptDoc: this.deps.decryptDoc,
|
|
159
|
+
signal
|
|
160
|
+
});
|
|
161
|
+
return applied;
|
|
162
|
+
}
|
|
163
|
+
scheduleRetry() {
|
|
164
|
+
const capped = Math.min(this.baseDelayMs * 2 ** this.failureCount, this.maxDelayMs);
|
|
165
|
+
// Full jitter over the first half of the interval keeps N engines from
|
|
166
|
+
// retrying in lockstep.
|
|
167
|
+
const delay = capped + capped * 0.5 * this.random();
|
|
168
|
+
this.failureCount += 1;
|
|
169
|
+
this.cancelRetry = this.schedule(() => {
|
|
170
|
+
this.cancelRetry = null;
|
|
171
|
+
void this.sync();
|
|
172
|
+
}, delay);
|
|
173
|
+
}
|
|
174
|
+
clearRetry() {
|
|
175
|
+
if (this.cancelRetry !== null) {
|
|
176
|
+
this.cancelRetry();
|
|
177
|
+
this.cancelRetry = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
setStatus(status) {
|
|
181
|
+
if (this.status === status) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.status = status;
|
|
185
|
+
this.deps.onStatusChange?.(status);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/sync/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `@interop/wallet-core/sync` subpath: the WAS replication engine core --
|
|
6
|
+
* the correctness-critical, cross-replica byte-compatibility surface two
|
|
7
|
+
* WAS-enabled wallet apps share.
|
|
8
|
+
*
|
|
9
|
+
* - The wire contract and port seam (`WasSyncPort`, `SyncCheckpoint`, `WireDoc`,
|
|
10
|
+
* `Json`, `DocCipher`, `MasterState`, and the `WasSyncConflictError` /
|
|
11
|
+
* `WasSyncNotFoundError` signals) come from `@interop/was-client/sync` and are
|
|
12
|
+
* re-exported here so an engine consumer imports one package.
|
|
13
|
+
* - `SyncStore` / `SyncedRow` / `ProjectionAction` / `ResolveConflict` are the
|
|
14
|
+
* replica-side persistence seam.
|
|
15
|
+
* - `runPull` / `projectionForDoc`, `runPush`, and `SyncEngine` are the pull,
|
|
16
|
+
* push, and orchestration algorithms.
|
|
17
|
+
* - `SyncedCollectionSpec` is the generic per-collection spec shape a concrete
|
|
18
|
+
* registry implements.
|
|
19
|
+
*
|
|
20
|
+
* The RxDB adapter (the web wallet's driver) is intentionally not part of this
|
|
21
|
+
* subpath in v0: that app keeps its own `replicateRxCollection` driver, and its
|
|
22
|
+
* metadata (`putMeta` / `metaVersion`) push half stays driver-side. See
|
|
23
|
+
* `push.ts` for why the metadata half is left out of the shared core.
|
|
24
|
+
*/
|
|
25
|
+
export { WasSyncConflictError, WasSyncNotFoundError } from '@interop/was-client/sync';
|
|
26
|
+
export type { Json, SyncCheckpoint, WireDoc, MasterState, WasSyncPort, DocCipher } from '@interop/was-client/sync';
|
|
27
|
+
export type { SyncStore, SyncedRow, ProjectionAction, ResolveConflict } from './types.js';
|
|
28
|
+
export { runPull, projectionForDoc } from './pull.js';
|
|
29
|
+
export { runPush, formatEtag } from './push.js';
|
|
30
|
+
export { SyncEngine } from './engine.js';
|
|
31
|
+
export type { SyncEngineDeps, SyncStatus } from './engine.js';
|
|
32
|
+
export type { SyncedCollectionSpec } from './collections.js';
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EACV,IAAI,EACJ,cAAc,EACd,OAAO,EACP,WAAW,EACX,WAAW,EACX,SAAS,EACV,MAAM,0BAA0B,CAAA;AAEjC,YAAY,EACV,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,eAAe,EAChB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7D,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `@interop/wallet-core/sync` subpath: the WAS replication engine core --
|
|
6
|
+
* the correctness-critical, cross-replica byte-compatibility surface two
|
|
7
|
+
* WAS-enabled wallet apps share.
|
|
8
|
+
*
|
|
9
|
+
* - The wire contract and port seam (`WasSyncPort`, `SyncCheckpoint`, `WireDoc`,
|
|
10
|
+
* `Json`, `DocCipher`, `MasterState`, and the `WasSyncConflictError` /
|
|
11
|
+
* `WasSyncNotFoundError` signals) come from `@interop/was-client/sync` and are
|
|
12
|
+
* re-exported here so an engine consumer imports one package.
|
|
13
|
+
* - `SyncStore` / `SyncedRow` / `ProjectionAction` / `ResolveConflict` are the
|
|
14
|
+
* replica-side persistence seam.
|
|
15
|
+
* - `runPull` / `projectionForDoc`, `runPush`, and `SyncEngine` are the pull,
|
|
16
|
+
* push, and orchestration algorithms.
|
|
17
|
+
* - `SyncedCollectionSpec` is the generic per-collection spec shape a concrete
|
|
18
|
+
* registry implements.
|
|
19
|
+
*
|
|
20
|
+
* The RxDB adapter (the web wallet's driver) is intentionally not part of this
|
|
21
|
+
* subpath in v0: that app keeps its own `replicateRxCollection` driver, and its
|
|
22
|
+
* metadata (`putMeta` / `metaVersion`) push half stays driver-side. See
|
|
23
|
+
* `push.ts` for why the metadata half is left out of the shared core.
|
|
24
|
+
*/
|
|
25
|
+
export { WasSyncConflictError, WasSyncNotFoundError } from '@interop/was-client/sync';
|
|
26
|
+
export { runPull, projectionForDoc } from './pull.js';
|
|
27
|
+
export { runPush, formatEtag } from './push.js';
|
|
28
|
+
export { SyncEngine } from './engine.js';
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,0BAA0B,CAAA;AAiBjC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The pull side of the WAS replication engine core: iterate the `changes` feed
|
|
6
|
+
* from the stored checkpoint, decrypt each document to a projection action
|
|
7
|
+
* OUTSIDE the store transaction, and hand each page to the store to apply
|
|
8
|
+
* atomically.
|
|
9
|
+
*
|
|
10
|
+
* All I/O is through the injected {@link WasSyncPort} and {@link SyncStore};
|
|
11
|
+
* decryption is the injected `decryptDoc`. No environment-specific imports, so
|
|
12
|
+
* the loop runs identically in the browser, Node, and React Native.
|
|
13
|
+
*
|
|
14
|
+
* Cross-replica invariants this loop and the RxDB `createPullHandler` in the web
|
|
15
|
+
* wallet's driver both hold, so the two replicas resume the same feed the same
|
|
16
|
+
* way:
|
|
17
|
+
* - Omit `checkpoint` entirely on the first pull (never send `null`).
|
|
18
|
+
* - Empty page (`checkpoint: null`, "no change") keeps the PRIOR checkpoint --
|
|
19
|
+
* never overwrite it with `null`, or the feed restarts from the beginning.
|
|
20
|
+
* - A tombstone deletes the projected row; a live document decrypts its `data`
|
|
21
|
+
* body to the payload to upsert.
|
|
22
|
+
*/
|
|
23
|
+
import type { Json, ProjectionAction, SyncStore, WasSyncPort, WireDoc } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Maps one pulled wire document to the projection action for the decrypted
|
|
26
|
+
* read-model. A tombstone deletes the projected row; a live document decrypts
|
|
27
|
+
* its `data` body to the payload to upsert. A live document with no body (should
|
|
28
|
+
* not occur on an encrypted collection) is a no-op.
|
|
29
|
+
*
|
|
30
|
+
* Decryption runs here, outside the store transaction, so a slow/failing decrypt
|
|
31
|
+
* never holds the store's write lock. A document whose body cannot be decrypted
|
|
32
|
+
* (legacy plaintext row, corrupt/foreign envelope, key mismatch) is skipped with
|
|
33
|
+
* a `none` projection rather than throwing: the body is still stored and the
|
|
34
|
+
* checkpoint advances past it, so one poison document can never permanently
|
|
35
|
+
* wedge the feed for the whole replica.
|
|
36
|
+
*
|
|
37
|
+
* @param doc {WireDoc}
|
|
38
|
+
* @param decryptDoc {(envelope: Json) => Promise<Json>}
|
|
39
|
+
* @returns {Promise<ProjectionAction>}
|
|
40
|
+
*/
|
|
41
|
+
export declare function projectionForDoc(doc: WireDoc, decryptDoc: (envelope: Json) => Promise<Json>): Promise<ProjectionAction>;
|
|
42
|
+
/**
|
|
43
|
+
* Runs the pull loop to exhaustion for one feed. Fetches a page from the current
|
|
44
|
+
* checkpoint, decrypts it to projections, and applies it (upserts + projection +
|
|
45
|
+
* checkpoint advance) in one exclusive store transaction. Terminates on a short
|
|
46
|
+
* page (caught up) or an empty page (no change -- the prior checkpoint is kept,
|
|
47
|
+
* never overwritten with `null`). Honors `signal` between pages so a lock drops
|
|
48
|
+
* the loop promptly; a mid-loop abort leaves each already-applied page intact
|
|
49
|
+
* and the feed resumable.
|
|
50
|
+
*
|
|
51
|
+
* @param options {object}
|
|
52
|
+
* @param options.port {WasSyncPort}
|
|
53
|
+
* @param options.store {SyncStore}
|
|
54
|
+
* @param options.batchSize {number} pull `limit` (server clamps at 1000)
|
|
55
|
+
* @param options.decryptDoc {(envelope: Json) => Promise<Json>}
|
|
56
|
+
* @param [options.signal] {AbortSignal}
|
|
57
|
+
* @returns {Promise<{ applied: number }>} documents applied across all pages
|
|
58
|
+
*/
|
|
59
|
+
export declare function runPull({ port, store, batchSize, decryptDoc, signal }: {
|
|
60
|
+
port: WasSyncPort;
|
|
61
|
+
store: SyncStore;
|
|
62
|
+
batchSize: number;
|
|
63
|
+
decryptDoc: (envelope: Json) => Promise<Json>;
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
applied: number;
|
|
67
|
+
}>;
|
|
68
|
+
//# sourceMappingURL=pull.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/sync/pull.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,gBAAgB,EAEhB,SAAS,EACT,WAAW,EACX,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,GAC5C,OAAO,CAAC,gBAAgB,CAAC,CAiB3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,OAAO,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,SAAS,EACT,UAAU,EACV,MAAM,EACP,EAAE;IACD,IAAI,EAAE,WAAW,CAAA;IACjB,KAAK,EAAE,SAAS,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA8C/B"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps one pulled wire document to the projection action for the decrypted
|
|
3
|
+
* read-model. A tombstone deletes the projected row; a live document decrypts
|
|
4
|
+
* its `data` body to the payload to upsert. A live document with no body (should
|
|
5
|
+
* not occur on an encrypted collection) is a no-op.
|
|
6
|
+
*
|
|
7
|
+
* Decryption runs here, outside the store transaction, so a slow/failing decrypt
|
|
8
|
+
* never holds the store's write lock. A document whose body cannot be decrypted
|
|
9
|
+
* (legacy plaintext row, corrupt/foreign envelope, key mismatch) is skipped with
|
|
10
|
+
* a `none` projection rather than throwing: the body is still stored and the
|
|
11
|
+
* checkpoint advances past it, so one poison document can never permanently
|
|
12
|
+
* wedge the feed for the whole replica.
|
|
13
|
+
*
|
|
14
|
+
* @param doc {WireDoc}
|
|
15
|
+
* @param decryptDoc {(envelope: Json) => Promise<Json>}
|
|
16
|
+
* @returns {Promise<ProjectionAction>}
|
|
17
|
+
*/
|
|
18
|
+
export async function projectionForDoc(doc, decryptDoc) {
|
|
19
|
+
if (doc._deleted) {
|
|
20
|
+
return { kind: 'delete' };
|
|
21
|
+
}
|
|
22
|
+
if (doc.data === undefined || doc.data === null) {
|
|
23
|
+
return { kind: 'none' };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const payload = await decryptDoc(doc.data);
|
|
27
|
+
return { kind: 'upsert', payload };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.warn(`Skipping undecryptable synced document "${doc.id}" (no projection):`, err);
|
|
31
|
+
return { kind: 'none' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs the pull loop to exhaustion for one feed. Fetches a page from the current
|
|
36
|
+
* checkpoint, decrypts it to projections, and applies it (upserts + projection +
|
|
37
|
+
* checkpoint advance) in one exclusive store transaction. Terminates on a short
|
|
38
|
+
* page (caught up) or an empty page (no change -- the prior checkpoint is kept,
|
|
39
|
+
* never overwritten with `null`). Honors `signal` between pages so a lock drops
|
|
40
|
+
* the loop promptly; a mid-loop abort leaves each already-applied page intact
|
|
41
|
+
* and the feed resumable.
|
|
42
|
+
*
|
|
43
|
+
* @param options {object}
|
|
44
|
+
* @param options.port {WasSyncPort}
|
|
45
|
+
* @param options.store {SyncStore}
|
|
46
|
+
* @param options.batchSize {number} pull `limit` (server clamps at 1000)
|
|
47
|
+
* @param options.decryptDoc {(envelope: Json) => Promise<Json>}
|
|
48
|
+
* @param [options.signal] {AbortSignal}
|
|
49
|
+
* @returns {Promise<{ applied: number }>} documents applied across all pages
|
|
50
|
+
*/
|
|
51
|
+
export async function runPull({ port, store, batchSize, decryptDoc, signal }) {
|
|
52
|
+
let applied = 0;
|
|
53
|
+
for (;;) {
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const checkpoint = await store.getCheckpoint();
|
|
58
|
+
const { documents, checkpoint: next } = await port.query({
|
|
59
|
+
// Omit `checkpoint` entirely on the first pull.
|
|
60
|
+
...(checkpoint !== undefined && { checkpoint }),
|
|
61
|
+
limit: batchSize
|
|
62
|
+
});
|
|
63
|
+
// Empty page ("no change") -- or a defensively-guarded null checkpoint on a
|
|
64
|
+
// non-empty page: keep the prior checkpoint so the feed does not restart.
|
|
65
|
+
if (documents.length === 0 || next === null) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const projections = new Map();
|
|
69
|
+
for (const doc of documents) {
|
|
70
|
+
projections.set(doc.id, await projectionForDoc(doc, decryptDoc));
|
|
71
|
+
}
|
|
72
|
+
// A lock/stop between decrypt and apply drops this page (checkpoint not
|
|
73
|
+
// advanced), leaving it to a clean re-pull -- cheaper than holding the write
|
|
74
|
+
// lock across the abort check.
|
|
75
|
+
if (signal?.aborted) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
await store.applyPulledPage({
|
|
79
|
+
documents,
|
|
80
|
+
checkpoint: next,
|
|
81
|
+
projections
|
|
82
|
+
});
|
|
83
|
+
applied += documents.length;
|
|
84
|
+
// Short page: the server had fewer than a full batch left, so we are caught
|
|
85
|
+
// up. A full page means there may be more -- loop with the new checkpoint.
|
|
86
|
+
if (documents.length < batchSize) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { applied };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=pull.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull.js","sourceRoot":"","sources":["../../src/sync/pull.ts"],"names":[],"mappings":"AA+BA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAY,EACZ,UAA6C;IAE7C,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;IAC3B,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACzB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,IAAY,CAAC,CAAA;QAClD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CACV,2CAA2C,GAAG,CAAC,EAAE,oBAAoB,EACrE,GAAG,CACJ,CAAA;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IACzB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,SAAS,EACT,UAAU,EACV,MAAM,EAOP;IACC,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,SAAS,CAAC;QACR,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAK;QACP,CAAC;QAED,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,aAAa,EAAE,CAAA;QAC9C,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;YACvD,gDAAgD;YAChD,GAAG,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,CAAC;YAC/C,KAAK,EAAE,SAAS;SACjB,CAAC,CAAA;QAEF,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC5C,MAAK;QACP,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,GAAG,EAA4B,CAAA;QACvD,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,gBAAgB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAA;QAClE,CAAC;QAED,wEAAwE;QACxE,6EAA6E;QAC7E,+BAA+B;QAC/B,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAK;QACP,CAAC;QAED,MAAM,KAAK,CAAC,eAAe,CAAC;YAC1B,SAAS;YACT,UAAU,EAAE,IAAsB;YAClC,WAAW;SACZ,CAAC,CAAA;QACF,OAAO,IAAI,SAAS,CAAC,MAAM,CAAA;QAE3B,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,SAAS,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;YACjC,MAAK;QACP,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAA;AACpB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The push side of the WAS replication engine core: fan each dirty local row out
|
|
6
|
+
* to a conditional WAS write, then reconcile per the content-addressed conflict
|
|
7
|
+
* table.
|
|
8
|
+
*
|
|
9
|
+
* On a content-addressed collection an id's `data` never mutates, so a live row
|
|
10
|
+
* only ever pushes as a create (`If-None-Match: *`) and a tombstone as a delete;
|
|
11
|
+
* there is no update path. A mutable (last-write-wins) collection pushes a live
|
|
12
|
+
* row as a create while never-acked (`version 0`) and as an in-place update
|
|
13
|
+
* (`If-Match`) once acked, and settles a `412` through its injected
|
|
14
|
+
* {@link ResolveConflict} policy.
|
|
15
|
+
*
|
|
16
|
+
* This loop covers the CONTENT sub-resource only (`data` / `version`, at
|
|
17
|
+
* `PUT/DELETE /:id`). It does not drive the independently-versioned METADATA
|
|
18
|
+
* sub-resource (`custom` / `metaVersion`, at `PUT /:id/meta`): a replica that
|
|
19
|
+
* syncs user-writable metadata (the web wallet's RxDB driver, via
|
|
20
|
+
* `WasSyncPort.putMeta`) keeps that half in its own push handler. It is left out
|
|
21
|
+
* of this core deliberately -- none of the wallet Space collections
|
|
22
|
+
* (`private-credentials`, `public-credentials`, `wallet-activity`, `contacts`,
|
|
23
|
+
* `contacts-history`) versions its metadata independently of its content, so
|
|
24
|
+
* folding a `putMeta` diff into this loop would add an untested code path with
|
|
25
|
+
* no collection to exercise it. The `WasSyncPort.putMeta` capability stays
|
|
26
|
+
* optional on the port for the driver that needs it.
|
|
27
|
+
*/
|
|
28
|
+
import { formatEtag } from '@interop/was-client/sync';
|
|
29
|
+
import type { ResolveConflict, SyncStore, WasSyncPort } from './types.js';
|
|
30
|
+
export { formatEtag };
|
|
31
|
+
/**
|
|
32
|
+
* Pushes every dirty row for one feed, sequentially (bounds sockets/CPU, and
|
|
33
|
+
* keeps conflict reconciliation deterministic). Honors `signal` between rows.
|
|
34
|
+
* A non-conflict error from any row propagates so the engine aborts the cycle
|
|
35
|
+
* and backs off; already-pushed rows in the batch stay settled.
|
|
36
|
+
*
|
|
37
|
+
* @param options {object}
|
|
38
|
+
* @param options.port {WasSyncPort}
|
|
39
|
+
* @param options.store {SyncStore}
|
|
40
|
+
* @param [options.resolveConflict] {ResolveConflict} mutable-collection policy
|
|
41
|
+
* @param [options.signal] {AbortSignal}
|
|
42
|
+
* @returns {Promise<{ pushed: number; conflictsResolved: number }>} dirty rows
|
|
43
|
+
* processed this cycle, and how many invoked the LWW resolver (a positive
|
|
44
|
+
* count means the caller should rerun so a local-wins re-push settles)
|
|
45
|
+
*/
|
|
46
|
+
export declare function runPush({ port, store, resolveConflict, signal }: {
|
|
47
|
+
port: WasSyncPort;
|
|
48
|
+
store: SyncStore;
|
|
49
|
+
resolveConflict?: ResolveConflict;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
pushed: number;
|
|
53
|
+
conflictsResolved: number;
|
|
54
|
+
}>;
|
|
55
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/sync/push.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAQ,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAM/E,OAAO,EAAE,UAAU,EAAE,CAAA;AAoJrB;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,eAAe,EACf,MAAM,EACP,EAAE;IACD,IAAI,EAAE,WAAW,CAAA;IACjB,KAAK,EAAE,SAAS,CAAA;IAChB,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBzD"}
|