@opendatalabs/personal-server-ts-lite 1.34.2 → 1.35.1
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/dist/additions-crypto.d.ts +1 -1
- package/dist/additions-crypto.d.ts.map +1 -1
- package/dist/additions-crypto.js +16 -6
- package/dist/additions-crypto.js.map +1 -1
- package/dist/additions-protocol.d.ts +9 -1
- package/dist/additions-protocol.d.ts.map +1 -1
- package/dist/additions-protocol.js.map +1 -1
- package/dist/additions-session.d.ts +2 -0
- package/dist/additions-session.d.ts.map +1 -1
- package/dist/additions-session.js +29 -4
- package/dist/additions-session.js.map +1 -1
- package/dist/additions-transport.d.ts +10 -2
- package/dist/additions-transport.d.ts.map +1 -1
- package/dist/additions-transport.js +143 -48
- package/dist/additions-transport.js.map +1 -1
- package/dist/additions-worker.bundle.js +538 -101
- package/dist/additions-worker.js +45 -24
- package/dist/additions-worker.js.map +1 -1
- package/dist/browser-runtime.d.ts +23 -0
- package/dist/browser-runtime.d.ts.map +1 -1
- package/dist/browser-runtime.js +38 -1
- package/dist/browser-runtime.js.map +1 -1
- package/dist/canonical-persistence.d.ts +53 -0
- package/dist/canonical-persistence.d.ts.map +1 -0
- package/dist/canonical-persistence.js +13 -0
- package/dist/canonical-persistence.js.map +1 -0
- package/dist/canonical-store.d.ts +395 -0
- package/dist/canonical-store.d.ts.map +1 -0
- package/dist/canonical-store.js +1807 -0
- package/dist/canonical-store.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +154 -1
- package/dist/client.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +9 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/history-probe-worker.bundle.js +31216 -0
- package/dist/history-probe.worker.d.ts +35 -0
- package/dist/history-probe.worker.d.ts.map +1 -0
- package/dist/history-probe.worker.js +160 -0
- package/dist/history-probe.worker.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-store.d.ts.map +1 -1
- package/dist/mcp-store.js +109 -19
- package/dist/mcp-store.js.map +1 -1
- package/dist/oura-import.d.ts +2 -0
- package/dist/oura-import.d.ts.map +1 -0
- package/dist/oura-import.js +3 -0
- package/dist/oura-import.js.map +1 -0
- package/dist/replication-coordinator-state.d.ts +19 -0
- package/dist/replication-coordinator-state.d.ts.map +1 -0
- package/dist/replication-coordinator-state.js +20 -0
- package/dist/replication-coordinator-state.js.map +1 -0
- package/dist/replication-door.d.ts +17 -0
- package/dist/replication-door.d.ts.map +1 -0
- package/dist/replication-door.js +23 -0
- package/dist/replication-door.js.map +1 -0
- package/dist/replication-engine.d.ts +49 -0
- package/dist/replication-engine.d.ts.map +1 -0
- package/dist/replication-engine.js +589 -0
- package/dist/replication-engine.js.map +1 -0
- package/dist/replication-host.d.ts +92 -0
- package/dist/replication-host.d.ts.map +1 -0
- package/dist/replication-host.js +135 -0
- package/dist/replication-host.js.map +1 -0
- package/dist/replication-ports.d.ts +194 -0
- package/dist/replication-ports.d.ts.map +1 -0
- package/dist/replication-ports.js +58 -0
- package/dist/replication-ports.js.map +1 -0
- package/dist/replication-runtime.d.ts +64 -0
- package/dist/replication-runtime.d.ts.map +1 -0
- package/dist/replication-runtime.js +79 -0
- package/dist/replication-runtime.js.map +1 -0
- package/dist/replication-storage.d.ts +42 -0
- package/dist/replication-storage.d.ts.map +1 -0
- package/dist/replication-storage.js +173 -0
- package/dist/replication-storage.js.map +1 -0
- package/dist/runtime.d.ts +20 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +80 -1
- package/dist/runtime.js.map +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/storage.d.ts +15 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +352 -146
- package/dist/storage.js.map +1 -1
- package/dist/sync.d.ts +8 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +25 -7
- package/dist/sync.js.map +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replication phase engine on PS-Lite: core's shared engine
|
|
3
|
+
* (`core/replication/engine.ts`, also Node's) bound to the Lite ports. Every
|
|
4
|
+
* storage effect runs inside one native transaction, asynchronously.
|
|
5
|
+
*
|
|
6
|
+
* - Relational replication state uses `tx.sql` (host contract §4.3).
|
|
7
|
+
* - Local operations come from the canonical store's outbox, under the
|
|
8
|
+
* writer identity the canonical clock is bound to.
|
|
9
|
+
* - Received batches use core's copy custody (`replication/copies.ts`); a
|
|
10
|
+
* promoted copy imports through the canonical store.
|
|
11
|
+
* - Received blob bytes are replica bytes attached in the outcome
|
|
12
|
+
* transaction, then published as canonical blobs with `tx.publishBlob`.
|
|
13
|
+
* - A new writer (first enable with local writes, rotation) re-exports the
|
|
14
|
+
* canonical state into the outbox (canonical `rebaseline`, core
|
|
15
|
+
* `rebaseline.ts` rules), so the baseline is empty and the tail carries it.
|
|
16
|
+
* - The outbox drops what the gateway confirmed (`acknowledgePublished`).
|
|
17
|
+
*/
|
|
18
|
+
import type { MergeContext } from "@opendatalabs/personal-server-ts-core/replication/merge-plan";
|
|
19
|
+
import type { ReplicaVisibilityPolicy } from "@opendatalabs/personal-server-ts-core/replication/visibility-policy";
|
|
20
|
+
import { type ReplicaEngine, type ReplicaEngineConfig } from "@opendatalabs/personal-server-ts-core/replication/engine";
|
|
21
|
+
import { type ReplicaWriterIdentity } from "@opendatalabs/personal-server-ts-core/replication/writer-identity-store";
|
|
22
|
+
import type { LiteReplicaPersistence } from "./replication-ports.js";
|
|
23
|
+
export interface LiteReplicaEngineInput {
|
|
24
|
+
persistence: LiteReplicaPersistence;
|
|
25
|
+
/** The canonical store's trusted context, as `createLiteCanonicalStore` takes it. */
|
|
26
|
+
canonical: {
|
|
27
|
+
context: () => MergeContext;
|
|
28
|
+
authenticateReplica: (credential: unknown) => string | undefined;
|
|
29
|
+
visibilityPolicy?: ReplicaVisibilityPolicy;
|
|
30
|
+
newId?: () => string;
|
|
31
|
+
};
|
|
32
|
+
/** Host-independent engine configuration (transport, authority, limits). */
|
|
33
|
+
engine: ReplicaEngineConfig;
|
|
34
|
+
/** The storage adapter's bytes in use (`createLiteReplicaStorage`). */
|
|
35
|
+
transientBytesInUse?: ReadonlySet<string>;
|
|
36
|
+
}
|
|
37
|
+
/** The core engine on Lite, plus the writer rotation a host calls itself. */
|
|
38
|
+
export interface LiteReplicaEngine extends ReplicaEngine {
|
|
39
|
+
/**
|
|
40
|
+
* A new nonce under the same registration, after a restore, clone or
|
|
41
|
+
* reinstall (host contract §3.2). Call it before the engine steps again;
|
|
42
|
+
* the next step re-exports the canonical state under the new writer.
|
|
43
|
+
*/
|
|
44
|
+
rotateWriter(input?: {
|
|
45
|
+
nonce?: string;
|
|
46
|
+
}): Promise<ReplicaWriterIdentity>;
|
|
47
|
+
}
|
|
48
|
+
export declare function createLiteReplicaEngine(input: LiteReplicaEngineInput): LiteReplicaEngine;
|
|
49
|
+
//# sourceMappingURL=replication-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replication-engine.d.ts","sourceRoot":"","sources":["../src/replication-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8DAA8D,CAAC;AACjG,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qEAAqE,CAAC;AACnH,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,mBAAmB,EAIzB,MAAM,0DAA0D,CAAC;AAoBlE,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yEAAyE,CAAC;AAuBjF,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,wBAAwB,CAAC;AAmFhC,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,sBAAsB,CAAC;IACpC,qFAAqF;IACrF,SAAS,EAAE;QACT,OAAO,EAAE,MAAM,YAAY,CAAC;QAC5B,mBAAmB,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;QACjE,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;QAC3C,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;KACtB,CAAC;IACF,4EAA4E;IAC5E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC3C;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,YAAY,CAAC,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC1E;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAmlBnB"}
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { createReplicaEngine, } from "@opendatalabs/personal-server-ts-core/replication/engine";
|
|
2
|
+
import { perform, runEffectAsync, sql, } from "@opendatalabs/personal-server-ts-core/replication/effects";
|
|
3
|
+
import { ReplicaCoordinatorConflict, } from "@opendatalabs/personal-server-ts-core/replication/coordinator";
|
|
4
|
+
import { REPLICA_LOCK_SCHEMA } from "@opendatalabs/personal-server-ts-core/replication/coordinator-state";
|
|
5
|
+
import { REPLICA_PAYLOAD_MIGRATION } from "@opendatalabs/personal-server-ts-core/replication/payload";
|
|
6
|
+
import { REPLICA_COPY_MIGRATION, ReplicaCopyRefused, replicaCopies, } from "@opendatalabs/personal-server-ts-core/replication/copies";
|
|
7
|
+
import { REPLICA_REFETCH_BUDGET } from "@opendatalabs/personal-server-ts-core/replication/copy-promotion";
|
|
8
|
+
import { REPLICA_WRITER_IDENTITY_MIGRATION, currentReplicaWriterIdentity, enableReplicaWriterIdentity, establishReplicaWriterIdentity, rotateReplicaWriterIdentity, } from "@opendatalabs/personal-server-ts-core/replication/writer-identity-store";
|
|
9
|
+
import { replicaEnvelopeSchema, } from "@opendatalabs/personal-server-ts-core/replication/envelope";
|
|
10
|
+
import { LITE_CANONICAL_MAX_OPERATION_BYTES, } from "./canonical-persistence.js";
|
|
11
|
+
import { createLiteCanonicalStore, LiteCanonicalRefusal, litePendingFingerprint, } from "./canonical-store.js";
|
|
12
|
+
import { LITE_REPLICA_TRANSIENT_MIGRATION, reclaimLiteTransientBytes, } from "./replication-storage.js";
|
|
13
|
+
import { createLiteReplicaCoordinatorState } from "./replication-coordinator-state.js";
|
|
14
|
+
const FRAME_BYTES = 128 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Received blob bytes a retained copy holds, keyed (id, blobId) like Node's
|
|
17
|
+
* pending namespace. A blob id is its content hash, so copies of one batch
|
|
18
|
+
* that share a blob share these bytes.
|
|
19
|
+
*/
|
|
20
|
+
const LITE_REPLICA_CUSTODY_MIGRATION = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS pdpp_lite_replica_custody (
|
|
22
|
+
id TEXT NOT NULL, blob_id TEXT NOT NULL, bytes_id TEXT NOT NULL,
|
|
23
|
+
PRIMARY KEY (id, blob_id));`;
|
|
24
|
+
/** One in-tx operation of the native transaction. */
|
|
25
|
+
const inTx = (call) => perform(call);
|
|
26
|
+
/** Every blob descriptor an envelope names, by blob id. */
|
|
27
|
+
function descriptors(envelope) {
|
|
28
|
+
const found = new Map();
|
|
29
|
+
for (const op of envelope.operations) {
|
|
30
|
+
if (op.kind === "blob-claim")
|
|
31
|
+
found.set(op.blob.blobId, op.blob);
|
|
32
|
+
if (op.kind === "upsert")
|
|
33
|
+
for (const blob of op.blobs)
|
|
34
|
+
found.set(blob.blobId, blob);
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A staged byte stream: `write` hands frames to the native stage with
|
|
40
|
+
* backpressure, `complete` closes it and returns its id. The digest is
|
|
41
|
+
* checked natively when the outcome transaction attaches the bytes.
|
|
42
|
+
*/
|
|
43
|
+
function stageStream(persistence) {
|
|
44
|
+
const queue = [];
|
|
45
|
+
let wake;
|
|
46
|
+
let taken;
|
|
47
|
+
let failure;
|
|
48
|
+
const notify = () => {
|
|
49
|
+
wake?.();
|
|
50
|
+
wake = undefined;
|
|
51
|
+
};
|
|
52
|
+
const id = persistence.stageBytes((async function* () {
|
|
53
|
+
for (;;) {
|
|
54
|
+
while (!queue.length) {
|
|
55
|
+
if (failure)
|
|
56
|
+
throw failure;
|
|
57
|
+
await new Promise((resolve) => (wake = resolve));
|
|
58
|
+
}
|
|
59
|
+
const frame = queue.shift();
|
|
60
|
+
taken?.();
|
|
61
|
+
taken = undefined;
|
|
62
|
+
if (frame === null)
|
|
63
|
+
return;
|
|
64
|
+
yield frame;
|
|
65
|
+
}
|
|
66
|
+
})());
|
|
67
|
+
// A failed stage is reported by write/complete; never leave it unobserved.
|
|
68
|
+
id.catch(() => undefined);
|
|
69
|
+
return {
|
|
70
|
+
async write(bytes) {
|
|
71
|
+
for (let i = 0; i < bytes.length; i += FRAME_BYTES) {
|
|
72
|
+
queue.push(bytes.slice(i, i + FRAME_BYTES));
|
|
73
|
+
const consumed = new Promise((resolve) => (taken = resolve));
|
|
74
|
+
notify();
|
|
75
|
+
await Promise.race([consumed, id.then(() => consumed)]);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
complete() {
|
|
79
|
+
queue.push(null);
|
|
80
|
+
notify();
|
|
81
|
+
return id;
|
|
82
|
+
},
|
|
83
|
+
fail(error) {
|
|
84
|
+
failure = error;
|
|
85
|
+
notify();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function createLiteReplicaEngine(input) {
|
|
90
|
+
const { persistence, canonical, engine: config } = input;
|
|
91
|
+
const now = config.now ?? (() => Date.now());
|
|
92
|
+
let migrated;
|
|
93
|
+
const migrate = () => (migrated ??= persistence
|
|
94
|
+
.transaction(async (tx) => {
|
|
95
|
+
for (const migration of [
|
|
96
|
+
REPLICA_LOCK_SCHEMA,
|
|
97
|
+
REPLICA_PAYLOAD_MIGRATION,
|
|
98
|
+
REPLICA_COPY_MIGRATION,
|
|
99
|
+
REPLICA_WRITER_IDENTITY_MIGRATION,
|
|
100
|
+
LITE_REPLICA_CUSTODY_MIGRATION,
|
|
101
|
+
LITE_REPLICA_TRANSIENT_MIGRATION,
|
|
102
|
+
])
|
|
103
|
+
await tx.sql.exec(migration);
|
|
104
|
+
})
|
|
105
|
+
.catch((error) => {
|
|
106
|
+
// A failed migration is tried again by the next step.
|
|
107
|
+
migrated = undefined;
|
|
108
|
+
throw error;
|
|
109
|
+
}));
|
|
110
|
+
const run = async (effect) => {
|
|
111
|
+
await migrate();
|
|
112
|
+
return persistence.transaction((tx) => runEffectAsync(effect, tx));
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The canonical store inside one native transaction. A received blob's
|
|
116
|
+
* bytes are attached replica bytes, so its canonical attach publishes
|
|
117
|
+
* them (`publishBlob`) instead of a staged blob.
|
|
118
|
+
*/
|
|
119
|
+
const receiving = new WeakMap();
|
|
120
|
+
const canonicalIn = (tx) => {
|
|
121
|
+
let view = receiving.get(tx);
|
|
122
|
+
if (!view) {
|
|
123
|
+
view = {
|
|
124
|
+
get: (key) => tx.get(key),
|
|
125
|
+
put: (key, value) => tx.put(key, value),
|
|
126
|
+
delete: (key) => tx.delete(key),
|
|
127
|
+
scan: (prefix, after) => tx.scan(prefix, after),
|
|
128
|
+
blob: (blobId) => tx.blob(blobId),
|
|
129
|
+
attachBlob: (bytesId, expected) => tx.publishBlob(bytesId, expected),
|
|
130
|
+
};
|
|
131
|
+
receiving.set(tx, view);
|
|
132
|
+
}
|
|
133
|
+
const bound = view;
|
|
134
|
+
const scoped = {
|
|
135
|
+
transaction: (work) => work(bound),
|
|
136
|
+
stageBlob: (descriptor, chunks) => persistence.stageBlob(descriptor, chunks),
|
|
137
|
+
readBlob: (blobId) => persistence.readBlob(blobId),
|
|
138
|
+
};
|
|
139
|
+
return createLiteCanonicalStore({
|
|
140
|
+
persistence: scoped,
|
|
141
|
+
enabled: true,
|
|
142
|
+
context: canonical.context,
|
|
143
|
+
authenticateReplica: canonical.authenticateReplica,
|
|
144
|
+
visibilityPolicy: canonical.visibilityPolicy,
|
|
145
|
+
newId: canonical.newId,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const store = (call) => inTx((tx) => call(canonicalIn(tx)));
|
|
149
|
+
function authorize(credential) {
|
|
150
|
+
const owner = canonical.context().owner;
|
|
151
|
+
if (!owner || canonical.authenticateReplica(credential) !== owner)
|
|
152
|
+
throw new Error("replica authority required for owner");
|
|
153
|
+
}
|
|
154
|
+
function* custody(id, blobs) {
|
|
155
|
+
const held = [];
|
|
156
|
+
for (const blob of blobs) {
|
|
157
|
+
const row = yield* sql.get("SELECT bytes_id FROM pdpp_lite_replica_custody WHERE id=? AND blob_id=?", id, blob.blobId);
|
|
158
|
+
if (!row)
|
|
159
|
+
throw new Error("missing replica blob custody");
|
|
160
|
+
held.push({
|
|
161
|
+
stagingId: row.bytes_id,
|
|
162
|
+
blobId: blob.blobId,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return held;
|
|
166
|
+
}
|
|
167
|
+
function* blobsFor(id, envelope, blobs, claimed) {
|
|
168
|
+
const named = descriptors(envelope);
|
|
169
|
+
const claims = new Set(envelope.operations.flatMap((op) => op.kind === "blob-claim" ? [op.blob.blobId] : []));
|
|
170
|
+
return (yield* custody(id, blobs)).flatMap((blob) => {
|
|
171
|
+
const descriptor = named.get(blob.blobId);
|
|
172
|
+
// A pending preview stages only claimed blobs (canonical rule).
|
|
173
|
+
return descriptor && (!claimed || claims.has(blob.blobId))
|
|
174
|
+
? [{ stagingId: blob.stagingId, descriptor }]
|
|
175
|
+
: [];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** A promoted copy into canonical state, from its pending preview if any. */
|
|
179
|
+
function* importCanonical(envelope, blobs, id) {
|
|
180
|
+
const fingerprint = litePendingFingerprint(envelope);
|
|
181
|
+
const pending = yield* store((s) => s.pendingBatch(id));
|
|
182
|
+
if (pending?.state === "pending" && pending.fingerprint === fingerprint)
|
|
183
|
+
yield* store((s) => s.promotePending({ id, credential: config.credential, fingerprint }));
|
|
184
|
+
else {
|
|
185
|
+
const attached = yield* blobsFor(id, envelope, blobs, false);
|
|
186
|
+
yield* store((s) => s.import({
|
|
187
|
+
id,
|
|
188
|
+
envelope,
|
|
189
|
+
credential: config.credential,
|
|
190
|
+
blobs: attached,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const copies = replicaCopies({
|
|
195
|
+
get owner() {
|
|
196
|
+
return canonical.context().owner;
|
|
197
|
+
},
|
|
198
|
+
*importEnvelope(envelope, blobs, id) {
|
|
199
|
+
try {
|
|
200
|
+
yield* importCanonical(envelope, blobs, id);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
// The same batch fails the same way again: stop retrying it.
|
|
204
|
+
if (error instanceof LiteCanonicalRefusal)
|
|
205
|
+
throw new ReplicaCopyRefused(`canonical_${error.reason}`);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
quarantineConnection: (connectionId) => store((s) => s.quarantineConnection({
|
|
210
|
+
credential: config.credential,
|
|
211
|
+
connectionId,
|
|
212
|
+
reason: "finalized_conflict",
|
|
213
|
+
})),
|
|
214
|
+
preview: {
|
|
215
|
+
*show(id, envelope, blobsJson) {
|
|
216
|
+
const pending = yield* store((s) => s.pendingBatch(id));
|
|
217
|
+
// The canonical pending row of an id never changes content: another
|
|
218
|
+
// copy's preview hides the old one instead of replacing it.
|
|
219
|
+
if (pending &&
|
|
220
|
+
pending.fingerprint !== litePendingFingerprint(envelope)) {
|
|
221
|
+
if (pending.state === "pending")
|
|
222
|
+
yield* store((s) => s.quarantinePending({
|
|
223
|
+
id,
|
|
224
|
+
credential: config.credential,
|
|
225
|
+
reason: "copy conflict",
|
|
226
|
+
}));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const blobs = yield* blobsFor(id, envelope, JSON.parse(blobsJson), true);
|
|
230
|
+
try {
|
|
231
|
+
yield* store((s) => s.stagePending({
|
|
232
|
+
id,
|
|
233
|
+
envelope,
|
|
234
|
+
credential: config.credential,
|
|
235
|
+
blobs,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// No preview for a batch the store refuses; promotion fails
|
|
240
|
+
// closed on it (`importEnvelope`).
|
|
241
|
+
if (!(error instanceof LiteCanonicalRefusal))
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
*hide(id, state) {
|
|
246
|
+
const pending = yield* store((s) => s.pendingBatch(id));
|
|
247
|
+
if (pending?.state === "pending")
|
|
248
|
+
yield* store((s) => s.quarantinePending({
|
|
249
|
+
id,
|
|
250
|
+
credential: config.credential,
|
|
251
|
+
reason: state === "quarantined" ? "copy conflict" : state,
|
|
252
|
+
}));
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
*releaseBytes(id, blobIds) {
|
|
256
|
+
for (const blobId of blobIds) {
|
|
257
|
+
const row = yield* sql.get("SELECT bytes_id FROM pdpp_lite_replica_custody WHERE id=? AND blob_id=?", id, blobId);
|
|
258
|
+
if (!row)
|
|
259
|
+
continue;
|
|
260
|
+
yield* inTx((tx) => tx.releaseBytes(row.bytes_id));
|
|
261
|
+
yield* sql.run("DELETE FROM pdpp_lite_replica_custody WHERE id=? AND blob_id=?", id, blobId);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
*savepoint(effect) {
|
|
265
|
+
yield* sql.exec("SAVEPOINT lite_replica_import");
|
|
266
|
+
try {
|
|
267
|
+
const result = yield* effect;
|
|
268
|
+
yield* sql.exec("RELEASE lite_replica_import");
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
yield* sql.exec("ROLLBACK TO lite_replica_import");
|
|
273
|
+
yield* sql.exec("RELEASE lite_replica_import");
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
now,
|
|
278
|
+
});
|
|
279
|
+
const hasCopies = (id) => sql.get("SELECT 1 FROM pdpp_replica_copies WHERE id=? LIMIT 1", id);
|
|
280
|
+
// One engine per database. The lock row fences a step left over from an
|
|
281
|
+
// earlier boot; this flag serializes steps of this boot.
|
|
282
|
+
let active = false;
|
|
283
|
+
const door = {
|
|
284
|
+
admitted: () => true,
|
|
285
|
+
beginStep(parent) {
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
const abort = () => controller.abort(parent?.reason);
|
|
288
|
+
parent?.addEventListener("abort", abort, { once: true });
|
|
289
|
+
if (parent?.aborted)
|
|
290
|
+
abort();
|
|
291
|
+
return {
|
|
292
|
+
signal: controller.signal,
|
|
293
|
+
close: () => parent?.removeEventListener("abort", abort),
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
assertStepOpen(signal) {
|
|
297
|
+
signal.throwIfAborted();
|
|
298
|
+
},
|
|
299
|
+
assertTransactionOpen(signal) {
|
|
300
|
+
signal.throwIfAborted();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const host = {
|
|
304
|
+
run,
|
|
305
|
+
// Every Lite effect already runs as one native transaction.
|
|
306
|
+
immediate: (effect) => effect,
|
|
307
|
+
transaction: (effect) => effect,
|
|
308
|
+
commit(effect, context, signal) {
|
|
309
|
+
door.assertTransactionOpen(signal);
|
|
310
|
+
return runEffectAsync(effect, context);
|
|
311
|
+
},
|
|
312
|
+
cleanup(effect) {
|
|
313
|
+
void run(effect).catch(() => undefined);
|
|
314
|
+
},
|
|
315
|
+
captureWrites: (effect) => effect,
|
|
316
|
+
door,
|
|
317
|
+
randomId: () => globalThis.crypto.randomUUID(),
|
|
318
|
+
lock: {
|
|
319
|
+
busy: () => active,
|
|
320
|
+
async acquire(token) {
|
|
321
|
+
active = true;
|
|
322
|
+
try {
|
|
323
|
+
await migrate();
|
|
324
|
+
await persistence.transaction(async (tx) => {
|
|
325
|
+
await tx.sql.run("INSERT INTO pdpp_transport_lock (id,pid,token) VALUES (1,0,?) ON CONFLICT(id) DO UPDATE SET pid=0,token=excluded.token", [token]);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
active = false;
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
async release(token) {
|
|
334
|
+
active = false;
|
|
335
|
+
await persistence.transaction((tx) => tx.sql.run("DELETE FROM pdpp_transport_lock WHERE id=1 AND token=?", [
|
|
336
|
+
token,
|
|
337
|
+
]));
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
identity: {
|
|
341
|
+
current: () => currentReplicaWriterIdentity(),
|
|
342
|
+
enable: (request) => enableReplicaWriterIdentity({
|
|
343
|
+
...request,
|
|
344
|
+
current: () => run(currentReplicaWriterIdentity()),
|
|
345
|
+
establish: (record) => run((function* () {
|
|
346
|
+
const identity = yield* establishReplicaWriterIdentity({
|
|
347
|
+
// The runtime admits only an open door (`replication-runtime.ts`).
|
|
348
|
+
admitted: function* () {
|
|
349
|
+
return true;
|
|
350
|
+
},
|
|
351
|
+
// Local writes before the first identity are re-exported
|
|
352
|
+
// under it (canonical `rebaseline`).
|
|
353
|
+
journalEmpty: function* () {
|
|
354
|
+
return true;
|
|
355
|
+
},
|
|
356
|
+
}, { owner: request.owner, server: request.server, record });
|
|
357
|
+
yield* store((s) => s.rebaseline(identity.operationWriter));
|
|
358
|
+
return identity;
|
|
359
|
+
})()),
|
|
360
|
+
}),
|
|
361
|
+
},
|
|
362
|
+
coordinatorState({ owner, signal, enabled, }) {
|
|
363
|
+
const port = createLiteReplicaCoordinatorState({ persistence, owner });
|
|
364
|
+
return {
|
|
365
|
+
...port,
|
|
366
|
+
async commit(expected, next, checkpoint) {
|
|
367
|
+
const step = signal();
|
|
368
|
+
if (!step || !enabled())
|
|
369
|
+
throw new ReplicaCoordinatorConflict();
|
|
370
|
+
door.assertStepOpen(step);
|
|
371
|
+
return port.commit(expected, next, async (tx) => {
|
|
372
|
+
const result = await checkpoint(tx);
|
|
373
|
+
door.assertTransactionOpen(step);
|
|
374
|
+
return result;
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
staging({ repositoryId, owner, assertOpen }) {
|
|
380
|
+
const open = new Map();
|
|
381
|
+
return {
|
|
382
|
+
begin(admission) {
|
|
383
|
+
if (admission.repositoryId !== repositoryId ||
|
|
384
|
+
admission.owner !== owner)
|
|
385
|
+
throw new Error("admission authority mismatch");
|
|
386
|
+
const stream = stageStream(persistence);
|
|
387
|
+
const writers = open.get(admission) ?? new Set();
|
|
388
|
+
writers.add(stream);
|
|
389
|
+
open.set(admission, writers);
|
|
390
|
+
let sizeBytes = 0;
|
|
391
|
+
let closed = false;
|
|
392
|
+
return {
|
|
393
|
+
async write(bytes) {
|
|
394
|
+
if (closed)
|
|
395
|
+
throw new Error("staged object writer is closed");
|
|
396
|
+
assertOpen();
|
|
397
|
+
const next = sizeBytes + bytes.length;
|
|
398
|
+
if (!Number.isSafeInteger(next) ||
|
|
399
|
+
next > admission.payloadBytes) {
|
|
400
|
+
stream.fail(new Error("staged object exceeds admitted payload"));
|
|
401
|
+
throw new Error("staged object exceeds admitted payload");
|
|
402
|
+
}
|
|
403
|
+
await stream.write(bytes);
|
|
404
|
+
sizeBytes = next;
|
|
405
|
+
},
|
|
406
|
+
async complete() {
|
|
407
|
+
if (closed)
|
|
408
|
+
throw new Error("staged object writer is closed");
|
|
409
|
+
assertOpen();
|
|
410
|
+
closed = true;
|
|
411
|
+
writers.delete(stream);
|
|
412
|
+
return { bytesId: await stream.complete(), sizeBytes };
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
verify(handle, expected) {
|
|
417
|
+
if (handle.sizeBytes !== expected.sizeBytes)
|
|
418
|
+
throw new Error("staged object verification failed");
|
|
419
|
+
},
|
|
420
|
+
attach(handle, expected) {
|
|
421
|
+
const { bytesId } = handle;
|
|
422
|
+
return inTx(async (tx) => {
|
|
423
|
+
await tx.attachBytes(bytesId, {
|
|
424
|
+
length: expected.sizeBytes,
|
|
425
|
+
sha256: expected.sha256.replace(/^sha256:/, ""),
|
|
426
|
+
});
|
|
427
|
+
return bytesId;
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
abort(admission) {
|
|
431
|
+
for (const writer of open.get(admission) ?? [])
|
|
432
|
+
writer.fail(new Error("staged object aborted"));
|
|
433
|
+
open.delete(admission);
|
|
434
|
+
},
|
|
435
|
+
// Unattached native stages are discarded by the host; none outlive
|
|
436
|
+
// it. Bytes the storage adapter attached and a crash kept from its
|
|
437
|
+
// release are reclaimed before the step's first transfer.
|
|
438
|
+
reclaimAdmission: () => 0,
|
|
439
|
+
cleanup: () => 0,
|
|
440
|
+
cleanupAttachedTemporaryBytes: () => run(inTx((tx) => reclaimLiteTransientBytes(tx, input.transientBytesInUse))),
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
// Lite staging keeps no rows that could lose their bytes.
|
|
444
|
+
*sweepStages() { },
|
|
445
|
+
exporter: {
|
|
446
|
+
// A new Lite writer re-exports its state into the outbox
|
|
447
|
+
// (`rebaseline`), so its baseline is empty and every operation is in
|
|
448
|
+
// the tail.
|
|
449
|
+
*readBaselinePage({ afterSequence }) {
|
|
450
|
+
return {
|
|
451
|
+
operations: [],
|
|
452
|
+
nextAfterSequence: afterSequence ?? 0,
|
|
453
|
+
hasMore: false,
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
*readTailPage({ afterSequence, limit }) {
|
|
457
|
+
const page = yield* store((s) => s.readOutboxPage(afterSequence ?? 0));
|
|
458
|
+
const taken = page.entries.slice(0, limit);
|
|
459
|
+
return {
|
|
460
|
+
operations: taken.map((entry) => entry.operation),
|
|
461
|
+
nextAfterSequence: taken.at(-1)?.sequence ?? afterSequence ?? 0,
|
|
462
|
+
hasMore: page.hasMore || taken.length < page.entries.length,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
*getBaselineCheckpoint() {
|
|
466
|
+
return 0;
|
|
467
|
+
},
|
|
468
|
+
*acknowledgeBaselinePage() { },
|
|
469
|
+
// `establish` already re-exported for the current identity; this is
|
|
470
|
+
// a no-op unless the writer changed since.
|
|
471
|
+
*rebaseline() {
|
|
472
|
+
const identity = yield* currentReplicaWriterIdentity();
|
|
473
|
+
if (identity)
|
|
474
|
+
yield* store((s) => s.rebaseline(identity.operationWriter));
|
|
475
|
+
return undefined;
|
|
476
|
+
},
|
|
477
|
+
// Lite keeps no binding history: it publishes under current bindings.
|
|
478
|
+
*declarationHistory() {
|
|
479
|
+
return [];
|
|
480
|
+
},
|
|
481
|
+
acknowledgePublished: (next) => store((s) => s.acknowledgePublished(next)),
|
|
482
|
+
readPinnedBlobChunks: (blobId) => persistence.readBlob(blobId),
|
|
483
|
+
},
|
|
484
|
+
receiver: {
|
|
485
|
+
// The canonical store always admits pending previews.
|
|
486
|
+
setPendingEnabled() { },
|
|
487
|
+
*quarantine({ credential, id, reason }) {
|
|
488
|
+
authorize(credential);
|
|
489
|
+
// A copy-backed batch fails closed through the terminal transition.
|
|
490
|
+
if (yield* hasCopies(id)) {
|
|
491
|
+
yield* copies.terminate(id, "finalized_conflict");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const pending = yield* store((s) => s.pendingBatch(id));
|
|
495
|
+
if (pending?.state === "pending")
|
|
496
|
+
yield* store((s) => s.quarantinePending({ id, credential, reason }));
|
|
497
|
+
},
|
|
498
|
+
// One step runs the whole pass, a connection per store transaction,
|
|
499
|
+
// so no page is ever left queued.
|
|
500
|
+
*reconcileStep({ credential }) {
|
|
501
|
+
let after;
|
|
502
|
+
do {
|
|
503
|
+
after = (yield* store((s) => s.retry({ credential, after }))).after;
|
|
504
|
+
} while (after !== undefined);
|
|
505
|
+
// The canonical store does not report what still waits.
|
|
506
|
+
return { plan: { pending: [] }, complete: true };
|
|
507
|
+
},
|
|
508
|
+
*reconcileQueued() {
|
|
509
|
+
return false;
|
|
510
|
+
},
|
|
511
|
+
copies: {
|
|
512
|
+
list: (id) => copies.copies(id),
|
|
513
|
+
decide: (id, refetch) => copies.decide(id, refetch),
|
|
514
|
+
*apply({ credential, id, decision }) {
|
|
515
|
+
authorize(credential);
|
|
516
|
+
yield* copies.apply(id, decision);
|
|
517
|
+
},
|
|
518
|
+
*stage({ credential, copy, stagedBlobs, refetch }) {
|
|
519
|
+
authorize(credential);
|
|
520
|
+
const envelope = replicaEnvelopeSchema.parse(copy.envelope);
|
|
521
|
+
const owner = canonical.context().owner;
|
|
522
|
+
if (envelope.owner !== owner ||
|
|
523
|
+
envelope.operations.some((op) => op.owner !== owner))
|
|
524
|
+
throw new Error("wrong replica owner");
|
|
525
|
+
for (const blob of stagedBlobs) {
|
|
526
|
+
const held = yield* sql.get("SELECT 1 FROM pdpp_lite_replica_custody WHERE id=? AND blob_id=?", copy.id, blob.blobId);
|
|
527
|
+
if (held)
|
|
528
|
+
yield* inTx((tx) => tx.releaseBytes(blob.key));
|
|
529
|
+
else
|
|
530
|
+
yield* sql.run("INSERT INTO pdpp_lite_replica_custody (id,blob_id,bytes_id) VALUES (?,?,?)", copy.id, blob.blobId, blob.key);
|
|
531
|
+
}
|
|
532
|
+
return yield* copies.stage({
|
|
533
|
+
...copy,
|
|
534
|
+
envelope,
|
|
535
|
+
blobs: stagedBlobs.map((blob) => ({
|
|
536
|
+
blobId: blob.blobId,
|
|
537
|
+
size: blob.sizeBytes,
|
|
538
|
+
})),
|
|
539
|
+
}, refetch ?? { attempts: 0, budget: REPLICA_REFETCH_BUDGET });
|
|
540
|
+
},
|
|
541
|
+
*recordFact({ credential, ...fact }) {
|
|
542
|
+
authorize(credential);
|
|
543
|
+
return yield* copies.recordFact(fact);
|
|
544
|
+
},
|
|
545
|
+
*promote({ credential, id, copyKey }) {
|
|
546
|
+
authorize(credential);
|
|
547
|
+
return yield* copies.promote({ id, copyKey });
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
*bindings() {
|
|
552
|
+
const connections = [];
|
|
553
|
+
let after;
|
|
554
|
+
do {
|
|
555
|
+
const page = yield* inTx((tx) => tx.scan("connections/", after));
|
|
556
|
+
connections.push(...page.entries);
|
|
557
|
+
after = page.after;
|
|
558
|
+
} while (after !== undefined);
|
|
559
|
+
return connections;
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
// Constructing does no storage work: the first call creates the engine,
|
|
563
|
+
// which migrates. A runtime that is off never calls it.
|
|
564
|
+
let created;
|
|
565
|
+
// The phone admits no operation its canonical store cannot retain.
|
|
566
|
+
const maxOperationBytes = Math.min(config.resourceLimits?.maxOperationBytes ??
|
|
567
|
+
LITE_CANONICAL_MAX_OPERATION_BYTES, LITE_CANONICAL_MAX_OPERATION_BYTES);
|
|
568
|
+
const engine = () => (created ??= createReplicaEngine(host, {
|
|
569
|
+
...config,
|
|
570
|
+
resourceLimits: { ...config.resourceLimits, maxOperationBytes },
|
|
571
|
+
}));
|
|
572
|
+
return {
|
|
573
|
+
rotateWriter: (request = {}) => run(rotateReplicaWriterIdentity({
|
|
574
|
+
*admitted() {
|
|
575
|
+
return door.admitted();
|
|
576
|
+
},
|
|
577
|
+
}, request)),
|
|
578
|
+
get ready() {
|
|
579
|
+
return engine().ready;
|
|
580
|
+
},
|
|
581
|
+
step: (request) => engine().step(request),
|
|
582
|
+
cycle: () => engine().cycle(),
|
|
583
|
+
retryVersionSquats: () => engine().retryVersionSquats(),
|
|
584
|
+
readmitUnsupported: () => engine().readmitUnsupported(),
|
|
585
|
+
readmitFragments: () => engine().readmitFragments(),
|
|
586
|
+
onDependencyChanged: (key) => engine().onDependencyChanged(key),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
//# sourceMappingURL=replication-engine.js.map
|