@powerhousedao/reactor 6.2.3-dev.3 → 6.2.3-dev.30
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/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-Bvoba-AV.js} +24 -8
- package/dist/build-worker-executor-Bvoba-AV.js.map +1 -0
- package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-BVTGbEB0.js} +190 -40
- package/dist/document-indexer-BVTGbEB0.js.map +1 -0
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +10 -6
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-yZrksiJR.js → forwarding-logger-CnuVDVze.js} +1297 -241
- package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
- package/dist/index.d.ts +2707 -2040
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1708 -648
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +11 -17
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BpcqDtnQ.js} +4 -4
- package/dist/projection-shard-manager-BpcqDtnQ.js.map +1 -0
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-CrERzl8s.js → worker-handle-C2rLUHwv.js} +22 -16
- package/dist/worker-handle-C2rLUHwv.js.map +1 -0
- package/package.json +19 -10
- package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
- package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
- package/dist/drive-container-types-yZrksiJR.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
- package/dist/types-DMKLa0Ok.js +0 -33
- package/dist/types-DMKLa0Ok.js.map +0 -1
- package/dist/worker-HPysBfhx.js +0 -22
- package/dist/worker-HPysBfhx.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError as DowngradeNotSupportedError$1, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, isDenied, isUndoRedo, mentionedGroupIds, normalizeDocumentModelVersion, referencedGroupIds, sortOperations } from "@powerhousedao/shared/document-model";
|
|
1
|
+
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isUndoRedo, isV2ActionHash, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
|
|
3
2
|
import { v4 } from "uuid";
|
|
4
3
|
import { Migrator, sql } from "kysely";
|
|
5
4
|
//#region \0rolldown/runtime.js
|
|
@@ -52,6 +51,37 @@ function parsePagingOptions(paging, defaultLimit) {
|
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
53
|
//#endregion
|
|
54
|
+
//#region src/events/types.ts
|
|
55
|
+
/**
|
|
56
|
+
* Custom error class that aggregates multiple errors from event subscribers.
|
|
57
|
+
*/
|
|
58
|
+
var EventBusAggregateError = class extends Error {
|
|
59
|
+
errors;
|
|
60
|
+
constructor(errors) {
|
|
61
|
+
const message = `EventBus emit failed with ${errors.length} error(s): ${errors.map((e) => {
|
|
62
|
+
if (e && typeof e === "object" && "message" in e) return e.message;
|
|
63
|
+
return String(e);
|
|
64
|
+
}).join("; ")}`;
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "EventBusAggregateError";
|
|
67
|
+
this.errors = errors;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Event types for reactor lifecycle events.
|
|
72
|
+
*/
|
|
73
|
+
const ReactorEventTypes = {
|
|
74
|
+
JOB_PENDING: 10001,
|
|
75
|
+
JOB_RUNNING: 10002,
|
|
76
|
+
JOB_WRITE_READY: 10003,
|
|
77
|
+
JOB_READ_READY: 10004,
|
|
78
|
+
JOB_FAILED: 10005,
|
|
79
|
+
READMODEL_BATCH_COMPLETED: 10006,
|
|
80
|
+
READMODEL_INDEXED: 10007,
|
|
81
|
+
MODEL_LOADED: 10008,
|
|
82
|
+
SIGNATURE_REFUSED: 10009
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
55
85
|
//#region src/shared/errors.ts
|
|
56
86
|
/**
|
|
57
87
|
* Error thrown when attempting to access a deleted document.
|
|
@@ -167,19 +197,22 @@ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
|
|
|
167
197
|
return Error.isError(error) && error.name === "ExcessiveReshuffleError";
|
|
168
198
|
}
|
|
169
199
|
};
|
|
170
|
-
/**
|
|
171
|
-
* Error thrown when an operation has an invalid signature.
|
|
172
|
-
*/
|
|
200
|
+
/** The message carries the code: only name and message cross the queue. */
|
|
173
201
|
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
174
202
|
documentId;
|
|
203
|
+
code;
|
|
175
204
|
reason;
|
|
176
|
-
constructor(documentId, reason) {
|
|
177
|
-
super(`Invalid signature in document ${documentId}: ${reason}`);
|
|
205
|
+
constructor(documentId, code, reason) {
|
|
206
|
+
super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
|
|
178
207
|
this.name = "InvalidSignatureError";
|
|
179
208
|
this.documentId = documentId;
|
|
209
|
+
this.code = code;
|
|
180
210
|
this.reason = reason;
|
|
181
211
|
Error.captureStackTrace(this, InvalidSignatureError);
|
|
182
212
|
}
|
|
213
|
+
static isError(error) {
|
|
214
|
+
return Error.isError(error) && error.name === "InvalidSignatureError";
|
|
215
|
+
}
|
|
183
216
|
};
|
|
184
217
|
/**
|
|
185
218
|
* An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
|
|
@@ -250,6 +283,30 @@ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Er
|
|
|
250
283
|
return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
|
|
251
284
|
}
|
|
252
285
|
};
|
|
286
|
+
/**
|
|
287
|
+
* Error thrown when a relationship edge an operation names does not exist.
|
|
288
|
+
*
|
|
289
|
+
* Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
|
|
290
|
+
* rebuilds a thrown error from `{ name, message, stack, cause }` alone
|
|
291
|
+
* (`reactor-browser/src/rpc/error-info.ts`), so the class identity is lost in
|
|
292
|
+
* transit.
|
|
293
|
+
*/
|
|
294
|
+
var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
|
|
295
|
+
sourceId;
|
|
296
|
+
targetId;
|
|
297
|
+
relationshipType;
|
|
298
|
+
constructor(sourceId, targetId, relationshipType) {
|
|
299
|
+
super(`No ${relationshipType} relationship from ${sourceId} to ${targetId}`);
|
|
300
|
+
this.name = "RelationshipNotFoundError";
|
|
301
|
+
this.sourceId = sourceId;
|
|
302
|
+
this.targetId = targetId;
|
|
303
|
+
this.relationshipType = relationshipType;
|
|
304
|
+
Error.captureStackTrace(this, RelationshipNotFoundError);
|
|
305
|
+
}
|
|
306
|
+
static isError(error) {
|
|
307
|
+
return Error.isError(error) && error.name === "RelationshipNotFoundError";
|
|
308
|
+
}
|
|
309
|
+
};
|
|
253
310
|
//#endregion
|
|
254
311
|
//#region src/storage/interfaces.ts
|
|
255
312
|
/**
|
|
@@ -302,6 +359,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
|
302
359
|
* conflict, not a fault: the caller retries against the new stream heads.
|
|
303
360
|
*/
|
|
304
361
|
var AppendConditionFailedError = class extends Error {
|
|
362
|
+
condition;
|
|
305
363
|
constructor(condition) {
|
|
306
364
|
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
307
365
|
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
@@ -690,34 +748,13 @@ function selectDecisionModel(flags, registry) {
|
|
|
690
748
|
//#endregion
|
|
691
749
|
//#region src/executor/util.ts
|
|
692
750
|
/** Actions the reactor reduces itself, onto the document scope. */
|
|
693
|
-
const DOCUMENT_SCOPE_ACTIONS =
|
|
694
|
-
"CREATE_DOCUMENT",
|
|
695
|
-
"DELETE_DOCUMENT",
|
|
696
|
-
"UPGRADE_DOCUMENT",
|
|
697
|
-
"ADD_RELATIONSHIP",
|
|
698
|
-
"REMOVE_RELATIONSHIP",
|
|
699
|
-
"UPDATE_RELATIONSHIP"
|
|
700
|
-
]);
|
|
751
|
+
const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
|
|
701
752
|
/**
|
|
702
753
|
* `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
|
|
703
754
|
* so building a decision model would throw and defer the job forever.
|
|
704
755
|
*/
|
|
705
756
|
const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
|
|
706
757
|
/**
|
|
707
|
-
* The document a document-scope action writes to, which is not always the job's
|
|
708
|
-
* own document: delete and upgrade name it in `input.documentId`, and the
|
|
709
|
-
* relationship actions in `input.sourceId`. `execute` only checks that a batch
|
|
710
|
-
* shares one scope, so a caller can submit an action whose target is a document
|
|
711
|
-
* other than the one the job is keyed by. The policy gate has to follow the
|
|
712
|
-
* action rather than the job, or it decides against a policy the caller may
|
|
713
|
-
* control instead of the one guarding the write.
|
|
714
|
-
*/
|
|
715
|
-
function targetDocumentId(action, fallback) {
|
|
716
|
-
const input = action.input;
|
|
717
|
-
if (action.type === "ADD_RELATIONSHIP" || action.type === "REMOVE_RELATIONSHIP" || action.type === "UPDATE_RELATIONSHIP") return typeof input?.sourceId === "string" && input.sourceId.length > 0 ? input.sourceId : fallback;
|
|
718
|
-
return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
758
|
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
722
759
|
* Reconstructs the document header and initializes the base state.
|
|
723
760
|
*
|
|
@@ -896,6 +933,40 @@ var TouchedStreams = class {
|
|
|
896
933
|
return this.streams.values();
|
|
897
934
|
}
|
|
898
935
|
};
|
|
936
|
+
/**
|
|
937
|
+
* The ids of the actions the caller handed to a job. Load and reevaluation
|
|
938
|
+
* jobs write operations nobody submitted, so they report none.
|
|
939
|
+
*/
|
|
940
|
+
function submittedActionIds(job) {
|
|
941
|
+
return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Reports what became of each submitted action.
|
|
945
|
+
*
|
|
946
|
+
* A job's operations can include ones it only moved to a new index, so only
|
|
947
|
+
* those carrying a submitted action are reported. Returns undefined when the
|
|
948
|
+
* job submitted nothing, which keeps `JobInfo.result` null for the jobs that
|
|
949
|
+
* have no caller to answer to.
|
|
950
|
+
*/
|
|
951
|
+
function summarizeSubmittedActions(operations, submitted) {
|
|
952
|
+
if (!submitted || submitted.length === 0) return;
|
|
953
|
+
const ids = new Set(submitted);
|
|
954
|
+
const actions = [];
|
|
955
|
+
for (const { operation, context } of operations) {
|
|
956
|
+
if (!ids.has(operation.action.id)) continue;
|
|
957
|
+
actions.push({
|
|
958
|
+
actionId: operation.action.id,
|
|
959
|
+
scope: context.scope,
|
|
960
|
+
index: operation.index,
|
|
961
|
+
...operationOutcome(operation)
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
if (actions.length === 0) return;
|
|
965
|
+
return {
|
|
966
|
+
actions,
|
|
967
|
+
allApplied: actions.every((action) => action.kind === "applied")
|
|
968
|
+
};
|
|
969
|
+
}
|
|
899
970
|
//#endregion
|
|
900
971
|
//#region src/registry/errors.ts
|
|
901
972
|
/**
|
|
@@ -993,6 +1064,7 @@ const JobExecutorEventTypes = {
|
|
|
993
1064
|
//#endregion
|
|
994
1065
|
//#region src/cache/collection-membership-cache.ts
|
|
995
1066
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
1067
|
+
operationIndex;
|
|
996
1068
|
cache = /* @__PURE__ */ new Map();
|
|
997
1069
|
constructor(operationIndex) {
|
|
998
1070
|
this.operationIndex = operationIndex;
|
|
@@ -1206,10 +1278,23 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1206
1278
|
return {
|
|
1207
1279
|
state: document.state.document,
|
|
1208
1280
|
documentType,
|
|
1281
|
+
protocolVersions: createAction.input.protocolVersions,
|
|
1209
1282
|
documentScopeRevision: documentScopeRevision + 1
|
|
1210
1283
|
};
|
|
1211
1284
|
}
|
|
1212
1285
|
};
|
|
1286
|
+
/** Bind slots per statement: 16-bit on the wire, read signed in-process. */
|
|
1287
|
+
const MAX_BIND_PARAMETERS = 32767;
|
|
1288
|
+
/** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
|
|
1289
|
+
function chunkRows(rows) {
|
|
1290
|
+
if (rows.length === 0) return [];
|
|
1291
|
+
const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
|
|
1292
|
+
const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
|
|
1293
|
+
if (rows.length <= perChunk) return [rows];
|
|
1294
|
+
const chunks = [];
|
|
1295
|
+
for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
|
|
1296
|
+
return chunks;
|
|
1297
|
+
}
|
|
1213
1298
|
var KyselyOperationIndexTxn = class {
|
|
1214
1299
|
collections = [];
|
|
1215
1300
|
collectionMemberships = [];
|
|
@@ -1275,6 +1360,7 @@ var KyselyOperationIndexTxn = class {
|
|
|
1275
1360
|
}
|
|
1276
1361
|
};
|
|
1277
1362
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1363
|
+
db;
|
|
1278
1364
|
trx;
|
|
1279
1365
|
constructor(db) {
|
|
1280
1366
|
this.db = db;
|
|
@@ -1331,7 +1417,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1331
1417
|
leftOrdinal: null
|
|
1332
1418
|
}));
|
|
1333
1419
|
for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
|
|
1334
|
-
await trx.insertInto("document_collections").values(
|
|
1420
|
+
for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
|
|
1335
1421
|
}
|
|
1336
1422
|
let operationOrdinals = [];
|
|
1337
1423
|
if (operations.length > 0) {
|
|
@@ -1349,7 +1435,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1349
1435
|
deniedReason: op.deniedReason ?? null,
|
|
1350
1436
|
sourceRemote: op.sourceRemote
|
|
1351
1437
|
}));
|
|
1352
|
-
|
|
1438
|
+
for (const chunk of chunkRows(operationRows)) {
|
|
1439
|
+
const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
|
|
1440
|
+
operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
|
|
1441
|
+
}
|
|
1353
1442
|
}
|
|
1354
1443
|
if (memberships.length > 0) for (const m of memberships) {
|
|
1355
1444
|
const ordinal = operationOrdinals[m.operationIndex];
|
|
@@ -1386,6 +1475,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1386
1475
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1387
1476
|
return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1388
1477
|
}
|
|
1478
|
+
async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
|
|
1479
|
+
signal?.throwIfAborted();
|
|
1480
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1481
|
+
const rows = await this.queryExecutor.selectFrom("operation_index_operations").select("opId").select((eb) => eb.fn.max("ordinal").as("ordinal")).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", scope).where("opId", "in", opIds).groupBy("opId").execute();
|
|
1482
|
+
return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
|
|
1483
|
+
}
|
|
1389
1484
|
async find(collectionId, cursor, view, paging, signal) {
|
|
1390
1485
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1391
1486
|
const outerCursor = cursor ?? -1;
|
|
@@ -1605,21 +1700,6 @@ var RingBuffer = class {
|
|
|
1605
1700
|
}
|
|
1606
1701
|
};
|
|
1607
1702
|
//#endregion
|
|
1608
|
-
//#region src/cache/write-cache-types.ts
|
|
1609
|
-
/**
|
|
1610
|
-
* Where a snapshot sits in its stream.
|
|
1611
|
-
*
|
|
1612
|
-
* - `Head`: the newest revision of the stream when it was stored. Only these
|
|
1613
|
-
* can answer a read that asks for the head.
|
|
1614
|
-
* - `Historical`: state at an earlier revision. Usable as a starting point to
|
|
1615
|
-
* replay forward from, and as an answer to a read for that same revision.
|
|
1616
|
-
*/
|
|
1617
|
-
let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
|
|
1618
|
-
SnapshotPosition["Head"] = "head";
|
|
1619
|
-
SnapshotPosition["Historical"] = "historical";
|
|
1620
|
-
return SnapshotPosition;
|
|
1621
|
-
}({});
|
|
1622
|
-
//#endregion
|
|
1623
1703
|
//#region src/cache/kysely-write-cache.ts
|
|
1624
1704
|
/**
|
|
1625
1705
|
* The last operation index a keyframe's document reflects for the scope. A
|
|
@@ -1758,13 +1838,13 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1758
1838
|
const snapshots = stream.ringBuffer.getAll();
|
|
1759
1839
|
if (targetRevision === void 0) {
|
|
1760
1840
|
const newest = highestRevision(snapshots);
|
|
1761
|
-
if (newest?.position ===
|
|
1841
|
+
if (newest?.position === "head") {
|
|
1762
1842
|
this.lruTracker.touch(streamKey);
|
|
1763
1843
|
return copyDocument(newest.document);
|
|
1764
1844
|
}
|
|
1765
1845
|
if (newest) {
|
|
1766
1846
|
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1767
|
-
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document,
|
|
1847
|
+
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
1768
1848
|
this.lruTracker.touch(streamKey);
|
|
1769
1849
|
return document;
|
|
1770
1850
|
}
|
|
@@ -1777,7 +1857,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1777
1857
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1778
1858
|
if (newestOlder) {
|
|
1779
1859
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1780
|
-
this.store(documentId, scope, branch, targetRevision, document,
|
|
1860
|
+
this.store(documentId, scope, branch, targetRevision, document, "historical");
|
|
1781
1861
|
this.lruTracker.touch(streamKey);
|
|
1782
1862
|
return document;
|
|
1783
1863
|
}
|
|
@@ -1785,7 +1865,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1785
1865
|
}
|
|
1786
1866
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1787
1867
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1788
|
-
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ?
|
|
1868
|
+
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
1789
1869
|
return document;
|
|
1790
1870
|
}
|
|
1791
1871
|
/**
|
|
@@ -1820,7 +1900,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1820
1900
|
if (run.length === 0) return;
|
|
1821
1901
|
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1822
1902
|
const head = run[run.length - 1];
|
|
1823
|
-
this.store(documentId, scope, branch, head.revision, head.document,
|
|
1903
|
+
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
1824
1904
|
}
|
|
1825
1905
|
store(documentId, scope, branch, revision, document, position) {
|
|
1826
1906
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
@@ -2026,10 +2106,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2026
2106
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2027
2107
|
} else {
|
|
2028
2108
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2029
|
-
document = docModule.reducer(document, operation.action, void 0, {
|
|
2109
|
+
document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
|
|
2030
2110
|
skip: operation.skip,
|
|
2031
2111
|
protocolVersion
|
|
2032
|
-
});
|
|
2112
|
+
}), document.header.protocolVersions);
|
|
2033
2113
|
}
|
|
2034
2114
|
}
|
|
2035
2115
|
}
|
|
@@ -2078,12 +2158,12 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2078
2158
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2079
2159
|
else {
|
|
2080
2160
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2081
|
-
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2161
|
+
document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2082
2162
|
skip: operation.skip,
|
|
2083
2163
|
protocolVersion,
|
|
2084
2164
|
replayOptions: { operation },
|
|
2085
2165
|
skipIndexValidation: true
|
|
2086
|
-
});
|
|
2166
|
+
}), document.header.protocolVersions);
|
|
2087
2167
|
}
|
|
2088
2168
|
}
|
|
2089
2169
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
@@ -2207,12 +2287,12 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2207
2287
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2208
2288
|
else {
|
|
2209
2289
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2210
|
-
document = module.reducer(document, operation.action, void 0, {
|
|
2290
|
+
document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
|
|
2211
2291
|
skip: operation.skip,
|
|
2212
2292
|
protocolVersion,
|
|
2213
2293
|
replayOptions: { operation },
|
|
2214
2294
|
skipIndexValidation: true
|
|
2215
|
-
});
|
|
2295
|
+
}), document.header.protocolVersions);
|
|
2216
2296
|
}
|
|
2217
2297
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2218
2298
|
}
|
|
@@ -2339,6 +2419,11 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2339
2419
|
//#endregion
|
|
2340
2420
|
//#region src/executor/execution-scope.ts
|
|
2341
2421
|
var DefaultExecutionScope = class {
|
|
2422
|
+
operationStore;
|
|
2423
|
+
operationIndex;
|
|
2424
|
+
writeCache;
|
|
2425
|
+
documentMetaCache;
|
|
2426
|
+
collectionMembershipCache;
|
|
2342
2427
|
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2343
2428
|
this.operationStore = operationStore;
|
|
2344
2429
|
this.operationIndex = operationIndex;
|
|
@@ -2358,6 +2443,13 @@ var DefaultExecutionScope = class {
|
|
|
2358
2443
|
}
|
|
2359
2444
|
};
|
|
2360
2445
|
var KyselyExecutionScope = class {
|
|
2446
|
+
db;
|
|
2447
|
+
operationStore;
|
|
2448
|
+
operationIndex;
|
|
2449
|
+
keyframeStore;
|
|
2450
|
+
writeCache;
|
|
2451
|
+
documentMetaCache;
|
|
2452
|
+
collectionMembershipCache;
|
|
2361
2453
|
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2362
2454
|
this.db = db;
|
|
2363
2455
|
this.operationStore = operationStore;
|
|
@@ -2385,7 +2477,7 @@ var KyselyExecutionScope = class {
|
|
|
2385
2477
|
};
|
|
2386
2478
|
//#endregion
|
|
2387
2479
|
//#region src/utils/reshuffle.ts
|
|
2388
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
2480
|
+
const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2389
2481
|
"CREATE_DOCUMENT",
|
|
2390
2482
|
"DELETE_DOCUMENT",
|
|
2391
2483
|
"UPGRADE_DOCUMENT",
|
|
@@ -2716,6 +2808,8 @@ const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
|
2716
2808
|
* rows and persisted remotes remain valid without migration.
|
|
2717
2809
|
*/
|
|
2718
2810
|
var DriveCollectionId = class DriveCollectionId {
|
|
2811
|
+
driveId;
|
|
2812
|
+
branch;
|
|
2719
2813
|
constructor(driveId, branch) {
|
|
2720
2814
|
this.driveId = driveId;
|
|
2721
2815
|
this.branch = branch;
|
|
@@ -2748,6 +2842,11 @@ var DriveCollectionId = class DriveCollectionId {
|
|
|
2748
2842
|
//#endregion
|
|
2749
2843
|
//#region src/executor/document-action-handler.ts
|
|
2750
2844
|
var DocumentActionHandler = class {
|
|
2845
|
+
registry;
|
|
2846
|
+
logger;
|
|
2847
|
+
driveContainerTypes;
|
|
2848
|
+
featureFlags;
|
|
2849
|
+
decisionModel;
|
|
2751
2850
|
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
|
|
2752
2851
|
this.registry = registry;
|
|
2753
2852
|
this.logger = logger;
|
|
@@ -2840,7 +2939,7 @@ var DocumentActionHandler = class {
|
|
|
2840
2939
|
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2841
2940
|
};
|
|
2842
2941
|
executing.touchedStreams.add(job.documentId, job.scope, job.branch);
|
|
2843
|
-
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing,
|
|
2942
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
|
|
2844
2943
|
indexTxn.write([{
|
|
2845
2944
|
...operation,
|
|
2846
2945
|
documentId: job.documentId,
|
|
@@ -2852,6 +2951,7 @@ var DocumentActionHandler = class {
|
|
|
2852
2951
|
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2853
2952
|
state: standing.state.document,
|
|
2854
2953
|
documentType: standing.header.documentType,
|
|
2954
|
+
protocolVersions: standing.header.protocolVersions,
|
|
2855
2955
|
documentScopeRevision: operation.index + 1
|
|
2856
2956
|
});
|
|
2857
2957
|
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
@@ -2875,8 +2975,8 @@ var DocumentActionHandler = class {
|
|
|
2875
2975
|
branch: job.branch
|
|
2876
2976
|
});
|
|
2877
2977
|
const resultingStateObj = {
|
|
2878
|
-
|
|
2879
|
-
|
|
2978
|
+
...document.state,
|
|
2979
|
+
header: document.header
|
|
2880
2980
|
};
|
|
2881
2981
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
2882
2982
|
const writeResult = await this.writeOperationToStore({
|
|
@@ -2893,7 +2993,7 @@ var DocumentActionHandler = class {
|
|
|
2893
2993
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2894
2994
|
};
|
|
2895
2995
|
executing.touchedStreams.add(document.header.id, job.scope, job.branch);
|
|
2896
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document,
|
|
2996
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
|
|
2897
2997
|
indexTxn.write([{
|
|
2898
2998
|
...operation,
|
|
2899
2999
|
documentId: document.header.id,
|
|
@@ -2910,6 +3010,7 @@ var DocumentActionHandler = class {
|
|
|
2910
3010
|
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
2911
3011
|
state: document.state.document,
|
|
2912
3012
|
documentType: document.header.documentType,
|
|
3013
|
+
protocolVersions: document.header.protocolVersions,
|
|
2913
3014
|
documentScopeRevision: 1
|
|
2914
3015
|
});
|
|
2915
3016
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
@@ -2953,7 +3054,7 @@ var DocumentActionHandler = class {
|
|
|
2953
3054
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2954
3055
|
};
|
|
2955
3056
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
2956
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3057
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
2957
3058
|
indexTxn.write([{
|
|
2958
3059
|
...operation,
|
|
2959
3060
|
documentId,
|
|
@@ -2965,6 +3066,7 @@ var DocumentActionHandler = class {
|
|
|
2965
3066
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
2966
3067
|
state: document.state.document,
|
|
2967
3068
|
documentType: document.header.documentType,
|
|
3069
|
+
protocolVersions: document.header.protocolVersions,
|
|
2968
3070
|
documentScopeRevision: operation.index + 1
|
|
2969
3071
|
});
|
|
2970
3072
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3003,7 +3105,7 @@ var DocumentActionHandler = class {
|
|
|
3003
3105
|
} catch (error) {
|
|
3004
3106
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3005
3107
|
}
|
|
3006
|
-
const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3108
|
+
const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3007
3109
|
for (const revisionScope of revisionScopes) {
|
|
3008
3110
|
const snapshot = input.revision[revisionScope] ?? 0;
|
|
3009
3111
|
const actual = actualRevisions[revisionScope] ?? 0;
|
|
@@ -3045,8 +3147,8 @@ var DocumentActionHandler = class {
|
|
|
3045
3147
|
branch: job.branch
|
|
3046
3148
|
});
|
|
3047
3149
|
const resultingStateObj = {
|
|
3048
|
-
|
|
3049
|
-
|
|
3150
|
+
...document.state,
|
|
3151
|
+
header: document.header
|
|
3050
3152
|
};
|
|
3051
3153
|
if (fromVersion > 0) resultingStateObj.__migrated = true;
|
|
3052
3154
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
@@ -3064,7 +3166,7 @@ var DocumentActionHandler = class {
|
|
|
3064
3166
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3065
3167
|
};
|
|
3066
3168
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3067
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3169
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3068
3170
|
for (const scope of otherScopes) executing.postCommitInvalidations.push({
|
|
3069
3171
|
documentId,
|
|
3070
3172
|
scope,
|
|
@@ -3081,6 +3183,7 @@ var DocumentActionHandler = class {
|
|
|
3081
3183
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3082
3184
|
state: document.state.document,
|
|
3083
3185
|
documentType: document.header.documentType,
|
|
3186
|
+
protocolVersions: document.header.protocolVersions,
|
|
3084
3187
|
documentScopeRevision: operation.index + 1
|
|
3085
3188
|
});
|
|
3086
3189
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3150,7 +3253,7 @@ var DocumentActionHandler = class {
|
|
|
3150
3253
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
3151
3254
|
executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
|
|
3152
3255
|
executing.touchedStreams.add(input.targetId, job.scope, job.branch);
|
|
3153
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc,
|
|
3256
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
|
|
3154
3257
|
indexTxn.write([{
|
|
3155
3258
|
...operation,
|
|
3156
3259
|
documentId: input.sourceId,
|
|
@@ -3169,6 +3272,7 @@ var DocumentActionHandler = class {
|
|
|
3169
3272
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3170
3273
|
state: sourceDoc.state.document,
|
|
3171
3274
|
documentType: sourceDoc.header.documentType,
|
|
3275
|
+
protocolVersions: sourceDoc.header.protocolVersions,
|
|
3172
3276
|
documentScopeRevision: operation.index + 1
|
|
3173
3277
|
});
|
|
3174
3278
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
@@ -3197,55 +3301,655 @@ var DocumentActionHandler = class {
|
|
|
3197
3301
|
}
|
|
3198
3302
|
};
|
|
3199
3303
|
//#endregion
|
|
3200
|
-
//#region src/
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3304
|
+
//#region src/signer/passthrough-signer.ts
|
|
3305
|
+
/**
|
|
3306
|
+
* A no-op signer that returns empty values for all methods.
|
|
3307
|
+
* Used when signing is not required.
|
|
3308
|
+
*/
|
|
3309
|
+
var PassthroughSigner = class {
|
|
3310
|
+
publicKey = {};
|
|
3311
|
+
sign() {
|
|
3312
|
+
return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
|
|
3313
|
+
}
|
|
3314
|
+
verify() {
|
|
3315
|
+
return Promise.resolve();
|
|
3316
|
+
}
|
|
3317
|
+
signAction() {
|
|
3318
|
+
return Promise.resolve([
|
|
3319
|
+
"",
|
|
3320
|
+
"",
|
|
3321
|
+
"",
|
|
3322
|
+
"",
|
|
3323
|
+
""
|
|
3324
|
+
]);
|
|
3325
|
+
}
|
|
3326
|
+
};
|
|
3327
|
+
//#endregion
|
|
3328
|
+
//#region src/signer/trust-policy.ts
|
|
3329
|
+
/**
|
|
3330
|
+
* The policy admission asks: the reactor's own key signing as its own user is
|
|
3331
|
+
* accepted, then the host's policy decides, else the default does. `own` is
|
|
3332
|
+
* read on every ask, since its user changes on login.
|
|
3333
|
+
*/
|
|
3334
|
+
function admissionTrustPolicy(own, authEnforcement, policy) {
|
|
3335
|
+
return { authorizeSigner(signer, key, documentId) {
|
|
3336
|
+
const identity = actionSignerIdentity(own);
|
|
3337
|
+
if (identity.app.key !== "" && key === identity.app.key) {
|
|
3338
|
+
if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
|
|
3228
3339
|
}
|
|
3340
|
+
if (policy) return policy.authorizeSigner(signer, key, documentId);
|
|
3341
|
+
return Promise.resolve(!authEnforcement);
|
|
3342
|
+
} };
|
|
3343
|
+
}
|
|
3344
|
+
function sameUser(a, b) {
|
|
3345
|
+
return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
|
|
3346
|
+
}
|
|
3347
|
+
//#endregion
|
|
3348
|
+
//#region src/signer/did-key.ts
|
|
3349
|
+
const DID_KEY_PREFIX = "did:key:z";
|
|
3350
|
+
const P256_MULTICODEC = [128, 36];
|
|
3351
|
+
const MAX_CACHED_KEYS = 1024;
|
|
3352
|
+
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
|
|
3353
|
+
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
|
|
3354
|
+
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
|
|
3355
|
+
/** A did:key that is not a compressed P-256 public key. */
|
|
3356
|
+
var MalformedDidKeyError = class extends Error {
|
|
3357
|
+
constructor(did, detail) {
|
|
3358
|
+
super(`Malformed did:key ${did}: ${detail}`);
|
|
3359
|
+
this.name = "MalformedDidKeyError";
|
|
3229
3360
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3361
|
+
};
|
|
3362
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
3363
|
+
/** Cached per process: decompressing the point costs a BigInt modPow. */
|
|
3364
|
+
async function importDidKey(did) {
|
|
3365
|
+
const cached = keyCache.get(did);
|
|
3366
|
+
if (cached) {
|
|
3367
|
+
keyCache.delete(did);
|
|
3368
|
+
keyCache.set(did, cached);
|
|
3369
|
+
return cached;
|
|
3370
|
+
}
|
|
3371
|
+
const raw = decompressP256(compressedPointFromDid(did), did);
|
|
3372
|
+
const key = await crypto.subtle.importKey("raw", raw.buffer, {
|
|
3373
|
+
name: "ECDSA",
|
|
3374
|
+
namedCurve: "P-256"
|
|
3375
|
+
}, false, ["verify"]);
|
|
3376
|
+
keyCache.set(did, key);
|
|
3377
|
+
if (keyCache.size > MAX_CACHED_KEYS) {
|
|
3378
|
+
const oldest = keyCache.keys().next().value;
|
|
3379
|
+
if (oldest !== void 0) keyCache.delete(oldest);
|
|
3380
|
+
}
|
|
3381
|
+
return key;
|
|
3382
|
+
}
|
|
3383
|
+
function compressedPointFromDid(did) {
|
|
3384
|
+
if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
|
|
3385
|
+
const decoded = base58Decode(did.slice(9));
|
|
3386
|
+
if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
|
|
3387
|
+
if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
|
|
3388
|
+
return decoded.subarray(2);
|
|
3389
|
+
}
|
|
3390
|
+
function decompressP256(compressed, did) {
|
|
3391
|
+
const prefix = compressed[0];
|
|
3392
|
+
if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
|
|
3393
|
+
let x = BigInt(0);
|
|
3394
|
+
for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
|
|
3395
|
+
if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
|
|
3396
|
+
const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
|
|
3397
|
+
let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
|
|
3398
|
+
if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
|
|
3399
|
+
if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
|
|
3400
|
+
const uncompressed = /* @__PURE__ */ new Uint8Array(65);
|
|
3401
|
+
uncompressed[0] = 4;
|
|
3402
|
+
uncompressed.set(bigIntToBytes(x), 1);
|
|
3403
|
+
uncompressed.set(bigIntToBytes(y), 33);
|
|
3404
|
+
return uncompressed;
|
|
3405
|
+
}
|
|
3406
|
+
function modPow(base, exp, mod) {
|
|
3407
|
+
let result = BigInt(1);
|
|
3408
|
+
let b = base % mod;
|
|
3409
|
+
let e = exp;
|
|
3410
|
+
while (e > BigInt(0)) {
|
|
3411
|
+
if (e & BigInt(1)) result = result * b % mod;
|
|
3412
|
+
e >>= BigInt(1);
|
|
3413
|
+
b = b * b % mod;
|
|
3414
|
+
}
|
|
3415
|
+
return result;
|
|
3416
|
+
}
|
|
3417
|
+
function bigIntToBytes(value) {
|
|
3418
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
3419
|
+
let v = value;
|
|
3420
|
+
for (let i = 31; i >= 0; i--) {
|
|
3421
|
+
bytes[i] = Number(v & BigInt(255));
|
|
3422
|
+
v >>= BigInt(8);
|
|
3423
|
+
}
|
|
3424
|
+
return bytes;
|
|
3425
|
+
}
|
|
3426
|
+
//#endregion
|
|
3427
|
+
//#region src/signer/verify-action-signature.ts
|
|
3428
|
+
const RENOWN_HASH_LENGTH = 44;
|
|
3429
|
+
const SHARED_HASH_LENGTH = 28;
|
|
3430
|
+
const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
|
|
3431
|
+
/**
|
|
3432
|
+
* Integrity only: identity binding and the live-id check sit with the caller.
|
|
3433
|
+
* `operation` is the incoming operation at load admission.
|
|
3434
|
+
*/
|
|
3435
|
+
async function verifyActionSignature(action, target, path, operation) {
|
|
3436
|
+
const policy = target.policy ?? "legacy";
|
|
3437
|
+
const refusal = policyRefusal(action, policy);
|
|
3438
|
+
if (refusal) return refusal;
|
|
3439
|
+
const shape = signerShape(action.context?.signer);
|
|
3440
|
+
if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
|
|
3441
|
+
ok: true,
|
|
3442
|
+
scheme: "unsigned"
|
|
3443
|
+
};
|
|
3444
|
+
if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
|
|
3445
|
+
const { signer, tuple } = shape;
|
|
3446
|
+
const scheme = schemeOf(tuple[2]);
|
|
3447
|
+
if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
|
|
3448
|
+
if (scheme !== "v2" && policy === "v2-required") return refuse(scheme, "SCHEME_BELOW_POLICY", `action ${action.id} carries a legacy tuple but ${target.documentId} requires v2 signatures`);
|
|
3449
|
+
if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
|
|
3450
|
+
if (scheme === "v2") {
|
|
3451
|
+
const refusal = await checkV2(action, tuple, target, operation);
|
|
3452
|
+
if (refusal) return refusal;
|
|
3453
|
+
} else if (path === "mutation") {
|
|
3454
|
+
if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
3455
|
+
}
|
|
3456
|
+
return verifyEcdsa(tuple, scheme, action.id);
|
|
3457
|
+
}
|
|
3458
|
+
/** Refusals a document's policy makes before any tuple is read. */
|
|
3459
|
+
function policyRefusal(action, policy) {
|
|
3460
|
+
if (action.type !== "CREATE_DOCUMENT") return;
|
|
3461
|
+
const scheme = schemeOfAction(action);
|
|
3462
|
+
const input = action.input;
|
|
3463
|
+
const documentId = input?.documentId ?? "";
|
|
3464
|
+
if (policy === "legacy") return isDerivedDocumentId(documentId) ? refuse(scheme, "ID_MISMATCH", `action ${action.id}: a legacy document cannot take the content-addressed id ${documentId}`) : void 0;
|
|
3465
|
+
const expected = derivedCreateId(input);
|
|
3466
|
+
if (expected === documentId) return;
|
|
3467
|
+
return refuse(scheme, "ID_MISMATCH", expected === void 0 ? `action ${action.id} creates v2-required ${documentId} without the header params its id is derived from` : `action ${action.id} creates v2-required ${documentId}, but its header params derive ${expected}`);
|
|
3468
|
+
}
|
|
3469
|
+
function derivedCreateId(input) {
|
|
3470
|
+
if (!input?.signing || !input.protocolVersions) return;
|
|
3471
|
+
try {
|
|
3472
|
+
return deriveDocumentId({
|
|
3473
|
+
documentType: input.model,
|
|
3474
|
+
createdAtUtcIso: input.signing.createdAtUtcIso,
|
|
3475
|
+
nonce: input.signing.nonce,
|
|
3476
|
+
protocolVersions: input.protocolVersions
|
|
3477
|
+
});
|
|
3478
|
+
} catch {
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
function schemeOfAction(action) {
|
|
3483
|
+
const shape = signerShape(action.context?.signer);
|
|
3484
|
+
if (shape.kind === "unsigned") return "unsigned";
|
|
3485
|
+
return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
|
|
3486
|
+
}
|
|
3487
|
+
/** Peers send arbitrary JSON, so nothing past `context` is assumed. */
|
|
3488
|
+
function signerShape(value) {
|
|
3489
|
+
if (value === void 0 || value === null) return { kind: "unsigned" };
|
|
3490
|
+
if (!isRecord(value)) return {
|
|
3491
|
+
kind: "malformed",
|
|
3492
|
+
reason: "has a signer that is not an object"
|
|
3493
|
+
};
|
|
3494
|
+
const { app, user, signatures } = value;
|
|
3495
|
+
if (app !== void 0 && app !== null && !isRecord(app)) return {
|
|
3496
|
+
kind: "malformed",
|
|
3497
|
+
reason: "has a signer.app that is not an object"
|
|
3498
|
+
};
|
|
3499
|
+
const key = isRecord(app) ? app.key : void 0;
|
|
3500
|
+
if (key !== void 0 && key !== null && typeof key !== "string") return {
|
|
3501
|
+
kind: "malformed",
|
|
3502
|
+
reason: "has a signer.app.key that is not a string"
|
|
3503
|
+
};
|
|
3504
|
+
if (!key) return { kind: "unsigned" };
|
|
3505
|
+
if (!isRecord(user) || typeof user.address !== "string") return {
|
|
3506
|
+
kind: "malformed",
|
|
3507
|
+
reason: "has a signer without a user address"
|
|
3508
|
+
};
|
|
3509
|
+
if (!Array.isArray(signatures)) return {
|
|
3510
|
+
kind: "malformed",
|
|
3511
|
+
reason: "has a signer whose signatures are not a list"
|
|
3512
|
+
};
|
|
3513
|
+
const tuple = signatures.at(-1);
|
|
3514
|
+
if (!isTuple(tuple)) return {
|
|
3515
|
+
kind: "malformed",
|
|
3516
|
+
reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
|
|
3517
|
+
};
|
|
3518
|
+
return {
|
|
3519
|
+
kind: "signed",
|
|
3520
|
+
signer: value,
|
|
3521
|
+
tuple
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
function isRecord(value) {
|
|
3525
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3526
|
+
}
|
|
3527
|
+
async function checkV2(action, tuple, target, operation) {
|
|
3528
|
+
const problem = v2TupleProblem(tuple);
|
|
3529
|
+
if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
|
|
3530
|
+
if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
|
|
3531
|
+
let expected;
|
|
3532
|
+
try {
|
|
3533
|
+
expected = await hashActionV2(action, target, action.context.signer);
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
|
|
3536
|
+
}
|
|
3537
|
+
if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
3538
|
+
}
|
|
3539
|
+
function sameInstant(a, b) {
|
|
3540
|
+
const left = Date.parse(a);
|
|
3541
|
+
return !Number.isNaN(left) && left === Date.parse(b);
|
|
3542
|
+
}
|
|
3543
|
+
function schemeOf(hash) {
|
|
3544
|
+
if (isV2ActionHash(hash)) return "v2";
|
|
3545
|
+
if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
|
|
3546
|
+
if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
|
|
3547
|
+
return "legacy-unknown";
|
|
3548
|
+
}
|
|
3549
|
+
async function legacyHash(scheme, action, target) {
|
|
3550
|
+
if (scheme === "legacy-renown") {
|
|
3551
|
+
const payload = [
|
|
3552
|
+
action.scope,
|
|
3553
|
+
action.type,
|
|
3554
|
+
JSON.stringify(action.input)
|
|
3555
|
+
].join("");
|
|
3556
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
3557
|
+
return bytesToBase64(new Uint8Array(digest));
|
|
3558
|
+
}
|
|
3559
|
+
const signer = action.context.signer;
|
|
3560
|
+
return buildOperationSignatureParams({
|
|
3561
|
+
documentId: target.documentId,
|
|
3562
|
+
signer,
|
|
3563
|
+
action,
|
|
3564
|
+
previousStateHash: ""
|
|
3565
|
+
})[2];
|
|
3566
|
+
}
|
|
3567
|
+
async function verifyEcdsa(tuple, scheme, actionId) {
|
|
3568
|
+
const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
|
|
3569
|
+
if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
|
|
3570
|
+
let cryptoKey;
|
|
3571
|
+
try {
|
|
3572
|
+
cryptoKey = await importDidKey(key);
|
|
3573
|
+
} catch (error) {
|
|
3574
|
+
return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3575
|
+
}
|
|
3576
|
+
const message = buildOperationSignatureMessage([
|
|
3577
|
+
timestamp,
|
|
3578
|
+
key,
|
|
3579
|
+
hash,
|
|
3580
|
+
prevStateHash
|
|
3581
|
+
]);
|
|
3582
|
+
let valid;
|
|
3583
|
+
try {
|
|
3584
|
+
valid = await crypto.subtle.verify({
|
|
3585
|
+
name: "ECDSA",
|
|
3586
|
+
hash: "SHA-256"
|
|
3587
|
+
}, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
|
|
3588
|
+
} catch {
|
|
3589
|
+
valid = false;
|
|
3590
|
+
}
|
|
3591
|
+
if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
|
|
3592
|
+
return {
|
|
3593
|
+
ok: true,
|
|
3594
|
+
scheme
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
function isTuple(value) {
|
|
3598
|
+
return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
|
|
3599
|
+
}
|
|
3600
|
+
function refuse(scheme, code, reason) {
|
|
3601
|
+
return {
|
|
3602
|
+
ok: false,
|
|
3603
|
+
scheme,
|
|
3604
|
+
code,
|
|
3605
|
+
reason
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
function hexToBytes(hex) {
|
|
3609
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
3610
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
3611
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
3612
|
+
return bytes;
|
|
3613
|
+
}
|
|
3614
|
+
function bytesToBase64(bytes) {
|
|
3615
|
+
let binary = "";
|
|
3616
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3617
|
+
return btoa(binary);
|
|
3618
|
+
}
|
|
3619
|
+
//#endregion
|
|
3620
|
+
//#region src/executor/synthesized-signing.ts
|
|
3621
|
+
/** Submitted types whose stored action the reducer writes in their place. */
|
|
3622
|
+
const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
|
|
3623
|
+
/**
|
|
3624
|
+
* The id of the action synthesized from `submittedId`. Derived, so the stored
|
|
3625
|
+
* operation answers for the submitted id at the live-id check.
|
|
3626
|
+
*/
|
|
3627
|
+
function synthesizedActionId(submittedId) {
|
|
3628
|
+
return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Whether `operation` stores an action the reducer made from `submitted`
|
|
3632
|
+
* rather than `submitted` itself or an operation already in `history`.
|
|
3633
|
+
*/
|
|
3634
|
+
function isSynthesized(submitted, operation, history) {
|
|
3635
|
+
if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
|
|
3636
|
+
const id = operation.action.id;
|
|
3637
|
+
if (!id) return true;
|
|
3638
|
+
if (id === submitted.id) return false;
|
|
3639
|
+
return !history.some((existing) => existing.action.id === id);
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Signs the synthesized action of `operation` in place as `signer`, under the
|
|
3643
|
+
* id {@link synthesizedActionId} derives from the submitted one. A REDO's
|
|
3644
|
+
* rebuilt action has no timestamp, so it takes the submitted one; the
|
|
3645
|
+
* operation timestamp is set to the action's, which a peer requires of a v2
|
|
3646
|
+
* tuple.
|
|
3647
|
+
*/
|
|
3648
|
+
async function signSynthesized(operation, submitted, target, signer, signal) {
|
|
3649
|
+
const synthesized = operation.action;
|
|
3650
|
+
const id = synthesizedActionId(submitted.id);
|
|
3651
|
+
const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
|
|
3652
|
+
const action = {
|
|
3653
|
+
id,
|
|
3654
|
+
timestampUtcMs,
|
|
3655
|
+
type: synthesized.type,
|
|
3656
|
+
input: synthesized.input,
|
|
3657
|
+
scope: synthesized.scope
|
|
3658
|
+
};
|
|
3659
|
+
const signature = await signer.signAction(action, {
|
|
3660
|
+
documentId: target.documentId,
|
|
3661
|
+
branch: target.branch
|
|
3662
|
+
}, signal);
|
|
3663
|
+
operation.action = {
|
|
3664
|
+
...action,
|
|
3665
|
+
context: { signer: {
|
|
3666
|
+
...actionSignerIdentity(signer),
|
|
3667
|
+
signatures: [signature]
|
|
3668
|
+
} }
|
|
3669
|
+
};
|
|
3670
|
+
operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
|
|
3671
|
+
operation.timestampUtcMs = timestampUtcMs;
|
|
3672
|
+
}
|
|
3673
|
+
//#endregion
|
|
3674
|
+
//#region src/executor/signature-admission.ts
|
|
3675
|
+
const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
|
|
3676
|
+
/** Runs once per write, when this reactor first stores it; never on re-appends. */
|
|
3677
|
+
var SignatureAdmission = class {
|
|
3678
|
+
mode;
|
|
3679
|
+
logger;
|
|
3680
|
+
eventBus;
|
|
3681
|
+
policySource;
|
|
3682
|
+
trustPolicy;
|
|
3683
|
+
trustTimeoutMs;
|
|
3684
|
+
constructor(mode, logger, eventBus, policySource = "meta", trust = {
|
|
3685
|
+
authEnforcement: false,
|
|
3686
|
+
timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
|
|
3687
|
+
}) {
|
|
3688
|
+
this.mode = mode;
|
|
3689
|
+
this.logger = logger;
|
|
3690
|
+
this.eventBus = eventBus;
|
|
3691
|
+
this.policySource = policySource;
|
|
3692
|
+
this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
|
|
3693
|
+
this.trustTimeoutMs = trust.timeoutMs;
|
|
3694
|
+
}
|
|
3695
|
+
/** The first refusal of a mutation's submitted actions, when enforcing. */
|
|
3696
|
+
async admitMutation(job, stores, signal) {
|
|
3697
|
+
const { operationStore } = stores;
|
|
3698
|
+
const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
|
|
3699
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
3700
|
+
const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
|
|
3701
|
+
await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
3702
|
+
const submitted = /* @__PURE__ */ new Set();
|
|
3703
|
+
for (const entry of candidates) {
|
|
3704
|
+
if (committed.has(entry)) continue;
|
|
3705
|
+
const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
|
|
3706
|
+
submitted.add(entry.opId);
|
|
3707
|
+
if (verdict.ok) continue;
|
|
3708
|
+
const refusal = this.record(job, entry, verdict, "mutation");
|
|
3709
|
+
if (refusal) return {
|
|
3710
|
+
kind: "refused",
|
|
3711
|
+
error: refusal
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
return {
|
|
3715
|
+
kind: "admitted",
|
|
3716
|
+
committed: [...committed.values()]
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
/** The incoming operations a load drops, empty unless enforcing. */
|
|
3720
|
+
async admitLoad(job, operations, stores, signal) {
|
|
3721
|
+
const { operationStore } = stores;
|
|
3722
|
+
const stream = {
|
|
3723
|
+
documentId: job.documentId,
|
|
3724
|
+
scope: job.scope,
|
|
3725
|
+
branch: job.branch
|
|
3726
|
+
};
|
|
3727
|
+
const candidates = operations.map((operation) => ({
|
|
3728
|
+
...candidate(operation.action, stream),
|
|
3729
|
+
operation
|
|
3730
|
+
}));
|
|
3731
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
3732
|
+
const redelivered = await this.redelivered(candidates, live, operationStore, signal);
|
|
3733
|
+
await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
3734
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
3735
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
3736
|
+
const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
|
|
3737
|
+
if (verdict.ok) continue;
|
|
3738
|
+
if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
|
|
3739
|
+
}
|
|
3740
|
+
return dropped;
|
|
3741
|
+
}
|
|
3742
|
+
async verdict(entry, live, submitted, path, signal, known = false) {
|
|
3743
|
+
const verdict = await verifyActionSignature(entry.action, {
|
|
3744
|
+
documentId: entry.stream.documentId,
|
|
3745
|
+
branch: entry.stream.branch,
|
|
3746
|
+
policy: entry.policy
|
|
3747
|
+
}, path, entry.operation);
|
|
3748
|
+
if (!verdict.ok) return verdict;
|
|
3749
|
+
if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
|
|
3750
|
+
ok: false,
|
|
3751
|
+
scheme: verdict.scheme,
|
|
3752
|
+
code: "DUPLICATE_ACTION",
|
|
3753
|
+
reason: `action ${entry.action.id} is already in the stream`
|
|
3754
|
+
};
|
|
3755
|
+
if (verdict.scheme === "unsigned") {
|
|
3756
|
+
const address = entry.action.context?.signer?.user?.address;
|
|
3757
|
+
return address ? {
|
|
3758
|
+
ok: false,
|
|
3759
|
+
scheme: "unsigned",
|
|
3760
|
+
code: "UNSIGNED_IDENTITY",
|
|
3761
|
+
reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
|
|
3762
|
+
} : verdict;
|
|
3763
|
+
}
|
|
3764
|
+
if (known) return verdict;
|
|
3765
|
+
const signer = entry.action.context.signer;
|
|
3766
|
+
if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
|
|
3767
|
+
ok: false,
|
|
3768
|
+
scheme: verdict.scheme,
|
|
3769
|
+
code: "SIGNER_UNAUTHORIZED",
|
|
3770
|
+
reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
|
|
3771
|
+
};
|
|
3772
|
+
return verdict;
|
|
3773
|
+
}
|
|
3774
|
+
/** Throws when the policy does, times out or the job is aborted. */
|
|
3775
|
+
async authorize(signer, documentId, signal) {
|
|
3776
|
+
signal?.throwIfAborted();
|
|
3777
|
+
let timer;
|
|
3778
|
+
let onAbort;
|
|
3779
|
+
const stop = new Promise((_, reject) => {
|
|
3780
|
+
timer = setTimeout(() => {
|
|
3781
|
+
reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
|
|
3782
|
+
}, this.trustTimeoutMs);
|
|
3783
|
+
if (signal) {
|
|
3784
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
3785
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3244
3786
|
}
|
|
3245
|
-
|
|
3787
|
+
});
|
|
3788
|
+
try {
|
|
3789
|
+
return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
|
|
3790
|
+
} finally {
|
|
3791
|
+
clearTimeout(timer);
|
|
3792
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
/**
|
|
3796
|
+
* The live candidates stored exactly as submitted and still standing: one
|
|
3797
|
+
* an undo or a reshuffle has since superseded is refused, not re-emitted with
|
|
3798
|
+
* the state it once produced. A synthesized operation
|
|
3799
|
+
* holds another action, so it cannot be compared; only a retry, whose first
|
|
3800
|
+
* attempt this job was, takes it as committed.
|
|
3801
|
+
*/
|
|
3802
|
+
async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
|
|
3803
|
+
const committed = /* @__PURE__ */ new Map();
|
|
3804
|
+
const toCompare = [];
|
|
3805
|
+
for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
|
|
3806
|
+
if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
|
|
3807
|
+
} else if (live.has(entry.opId)) toCompare.push(entry);
|
|
3808
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
3809
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
3810
|
+
const byId = new Map(stored.map((operation) => [operation.id, operation]));
|
|
3811
|
+
const matching = entries.filter((entry) => {
|
|
3812
|
+
const operation = byId.get(entry.opId);
|
|
3813
|
+
return operation && sameContent(operation.action, entry.action);
|
|
3814
|
+
});
|
|
3815
|
+
if (matching.length === 0) continue;
|
|
3816
|
+
const later = await operationStore.getSince(stream.documentId, stream.scope, stream.branch, Math.min(...matching.map((entry) => byId.get(entry.opId).index)), void 0, void 0, signal);
|
|
3817
|
+
for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
|
|
3818
|
+
}
|
|
3819
|
+
return committed;
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* The load candidates stored exactly as they arrive, timestamp included: a
|
|
3823
|
+
* peer re-sending what this stream holds, not a replay of its action.
|
|
3824
|
+
*/
|
|
3825
|
+
async redelivered(candidates, live, operationStore, signal) {
|
|
3826
|
+
const found = /* @__PURE__ */ new Set();
|
|
3827
|
+
const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
|
|
3828
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
3829
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
3830
|
+
for (const entry of entries) if (stored.some((operation) => operation.id === entry.opId && Date.parse(operation.timestampUtcMs) === Date.parse(entry.operation?.timestampUtcMs ?? "") && sameContent(operation.action, entry.action))) found.add(entry);
|
|
3831
|
+
}
|
|
3832
|
+
return found;
|
|
3833
|
+
}
|
|
3834
|
+
async liveOperationIds(candidates, operationStore, signal) {
|
|
3835
|
+
const live = /* @__PURE__ */ new Set();
|
|
3836
|
+
for (const [stream, entries] of byStream(candidates)) {
|
|
3837
|
+
const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
|
|
3838
|
+
for (const opId of found) live.add(opId);
|
|
3246
3839
|
}
|
|
3840
|
+
return live;
|
|
3841
|
+
}
|
|
3842
|
+
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
3843
|
+
record(job, entry, refusal, path) {
|
|
3844
|
+
const enforced = this.mode === "enforce";
|
|
3845
|
+
const event = {
|
|
3846
|
+
jobId: job.id,
|
|
3847
|
+
documentId: entry.stream.documentId,
|
|
3848
|
+
scope: entry.stream.scope,
|
|
3849
|
+
branch: entry.stream.branch,
|
|
3850
|
+
actionId: entry.action.id,
|
|
3851
|
+
code: refusal.code,
|
|
3852
|
+
scheme: refusal.scheme,
|
|
3853
|
+
path,
|
|
3854
|
+
enforced,
|
|
3855
|
+
reason: refusal.reason
|
|
3856
|
+
};
|
|
3857
|
+
this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
|
|
3858
|
+
this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
|
|
3859
|
+
this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
|
|
3860
|
+
});
|
|
3861
|
+
return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
|
|
3247
3862
|
}
|
|
3248
3863
|
};
|
|
3864
|
+
/**
|
|
3865
|
+
* A CREATE_DOCUMENT is verified under its own input. Anything else takes the
|
|
3866
|
+
* stored document's policy, or, for a document this job creates, that CREATE's.
|
|
3867
|
+
*/
|
|
3868
|
+
async function resolvePolicies(candidates, jobActions, stored) {
|
|
3869
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
3870
|
+
for (const entry of candidates) {
|
|
3871
|
+
if (entry.action.type === "CREATE_DOCUMENT") {
|
|
3872
|
+
entry.policy = createPolicy(entry.action);
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
const { documentId, branch } = entry.stream;
|
|
3876
|
+
const key = `${documentId}\u0000${branch}`;
|
|
3877
|
+
let policy = resolved.get(key);
|
|
3878
|
+
if (policy === void 0) {
|
|
3879
|
+
policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
|
|
3880
|
+
resolved.set(key, policy);
|
|
3881
|
+
}
|
|
3882
|
+
entry.policy = policy;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
async function storedPolicy(stores, source, documentId, branch, signal) {
|
|
3886
|
+
try {
|
|
3887
|
+
if (source === "write-cache") {
|
|
3888
|
+
const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
|
|
3889
|
+
return signaturePolicyOf(document.header);
|
|
3890
|
+
}
|
|
3891
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
3892
|
+
return signaturePolicyOf(meta.protocolVersions);
|
|
3893
|
+
} catch (error) {
|
|
3894
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
3895
|
+
throw error;
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
function createdPolicy(actions, documentId) {
|
|
3899
|
+
const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
|
|
3900
|
+
return create ? createPolicy(create) : void 0;
|
|
3901
|
+
}
|
|
3902
|
+
function createPolicy(action) {
|
|
3903
|
+
return signaturePolicyOf(action.input);
|
|
3904
|
+
}
|
|
3905
|
+
function candidate(action, stream) {
|
|
3906
|
+
const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
|
|
3907
|
+
return {
|
|
3908
|
+
action,
|
|
3909
|
+
stream,
|
|
3910
|
+
opId: opId(action.id),
|
|
3911
|
+
synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
function isLive(entry, live) {
|
|
3915
|
+
return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
|
|
3916
|
+
}
|
|
3917
|
+
function mutationStream(action, job) {
|
|
3918
|
+
return {
|
|
3919
|
+
...actionSigningTarget(action, job.documentId, job.branch),
|
|
3920
|
+
scope: job.scope
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
function byStream(candidates) {
|
|
3924
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3925
|
+
for (const entry of candidates) {
|
|
3926
|
+
const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
|
|
3927
|
+
const group = groups.get(key) ?? [entry.stream, []];
|
|
3928
|
+
group[1].push(entry);
|
|
3929
|
+
groups.set(key, group);
|
|
3930
|
+
}
|
|
3931
|
+
return [...groups.values()];
|
|
3932
|
+
}
|
|
3933
|
+
function isRetry(job) {
|
|
3934
|
+
return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
|
|
3935
|
+
}
|
|
3936
|
+
function committedWrite(entry, opId) {
|
|
3937
|
+
return {
|
|
3938
|
+
...entry.stream,
|
|
3939
|
+
actionId: entry.action.id,
|
|
3940
|
+
opId
|
|
3941
|
+
};
|
|
3942
|
+
}
|
|
3943
|
+
function isSuperseded(operation, later) {
|
|
3944
|
+
return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
|
|
3945
|
+
}
|
|
3946
|
+
function sameContent(stored, submitted) {
|
|
3947
|
+
try {
|
|
3948
|
+
return canonicalJson(stored) === canonicalJson(submitted);
|
|
3949
|
+
} catch {
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3249
3953
|
//#endregion
|
|
3250
3954
|
//#region src/executor/simple-job-executor.ts
|
|
3251
3955
|
const MAX_SKIP_THRESHOLD = 1e3;
|
|
@@ -3263,6 +3967,7 @@ function isValidISOTimestamp(value) {
|
|
|
3263
3967
|
* every test expect a failed job to look like.
|
|
3264
3968
|
*/
|
|
3265
3969
|
var JobRollbackSignal = class extends Error {
|
|
3970
|
+
result;
|
|
3266
3971
|
constructor(result) {
|
|
3267
3972
|
super("job rolled back");
|
|
3268
3973
|
this.result = result;
|
|
@@ -3270,13 +3975,27 @@ var JobRollbackSignal = class extends Error {
|
|
|
3270
3975
|
}
|
|
3271
3976
|
};
|
|
3272
3977
|
var SimpleJobExecutor = class {
|
|
3978
|
+
logger;
|
|
3979
|
+
registry;
|
|
3980
|
+
operationStore;
|
|
3981
|
+
eventBus;
|
|
3982
|
+
writeCache;
|
|
3983
|
+
operationIndex;
|
|
3984
|
+
documentMetaCache;
|
|
3985
|
+
collectionMembershipCache;
|
|
3986
|
+
driveContainerTypes;
|
|
3273
3987
|
config;
|
|
3274
3988
|
featureFlags;
|
|
3275
3989
|
decisionModel;
|
|
3276
|
-
|
|
3990
|
+
signatureAdmission;
|
|
3277
3991
|
documentActionHandler;
|
|
3278
3992
|
executionScope;
|
|
3279
|
-
|
|
3993
|
+
signer;
|
|
3994
|
+
/**
|
|
3995
|
+
* `signer` signs the operations the reducer synthesizes; unsigned if omitted.
|
|
3996
|
+
* `trustPolicy` decides which keys may sign as which users at admission.
|
|
3997
|
+
*/
|
|
3998
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
|
|
3280
3999
|
this.logger = logger;
|
|
3281
4000
|
this.registry = registry;
|
|
3282
4001
|
this.operationStore = operationStore;
|
|
@@ -3286,6 +4005,7 @@ var SimpleJobExecutor = class {
|
|
|
3286
4005
|
this.documentMetaCache = documentMetaCache;
|
|
3287
4006
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3288
4007
|
this.driveContainerTypes = driveContainerTypes;
|
|
4008
|
+
this.signer = signer ?? new PassthroughSigner();
|
|
3289
4009
|
this.config = {
|
|
3290
4010
|
featureFlags: config.featureFlags ?? {},
|
|
3291
4011
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
@@ -3295,11 +4015,17 @@ var SimpleJobExecutor = class {
|
|
|
3295
4015
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3296
4016
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3297
4017
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3298
|
-
batchApplies: config.batchApplies ?? true
|
|
4018
|
+
batchApplies: config.batchApplies ?? true,
|
|
4019
|
+
signatureVerification: config.signatureVerification ?? "enforce"
|
|
3299
4020
|
};
|
|
3300
4021
|
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3301
4022
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3302
|
-
this.
|
|
4023
|
+
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
4024
|
+
signer: this.signer,
|
|
4025
|
+
authEnforcement: this.featureFlags.authEnforcement,
|
|
4026
|
+
policy: trustPolicy,
|
|
4027
|
+
timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
|
|
4028
|
+
});
|
|
3303
4029
|
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
|
|
3304
4030
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3305
4031
|
}
|
|
@@ -3429,10 +4155,29 @@ var SimpleJobExecutor = class {
|
|
|
3429
4155
|
pendingEvent
|
|
3430
4156
|
};
|
|
3431
4157
|
}
|
|
3432
|
-
|
|
4158
|
+
let admission;
|
|
4159
|
+
try {
|
|
4160
|
+
admission = await this.signatureAdmission.admitMutation(job, stores, signal);
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4163
|
+
}
|
|
4164
|
+
if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
|
|
4165
|
+
let committed;
|
|
4166
|
+
try {
|
|
4167
|
+
committed = await this.reloadCommitted(admission.committed, stores, signal);
|
|
4168
|
+
} catch (error) {
|
|
4169
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4170
|
+
}
|
|
4171
|
+
const committedIds = new Set(admission.committed.map((write) => write.actionId));
|
|
4172
|
+
const fresh = committedIds.size === 0 ? job : {
|
|
4173
|
+
...job,
|
|
4174
|
+
actions: job.actions.filter((action) => !committedIds.has(action.id))
|
|
4175
|
+
};
|
|
4176
|
+
if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
|
|
4177
|
+
const positioned = await this.positionByTimestamp(fresh, stores, signal);
|
|
3433
4178
|
if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
|
|
3434
4179
|
const executing = {
|
|
3435
|
-
job,
|
|
4180
|
+
job: fresh,
|
|
3436
4181
|
startTime,
|
|
3437
4182
|
indexTxn,
|
|
3438
4183
|
stores,
|
|
@@ -3462,13 +4207,23 @@ var SimpleJobExecutor = class {
|
|
|
3462
4207
|
} };
|
|
3463
4208
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
3464
4209
|
postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
4210
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
4211
|
+
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* A successful mutation's result and the write-ready event it owes, carrying
|
|
4215
|
+
* what earlier attempts committed ahead of what this one wrote.
|
|
4216
|
+
*/
|
|
4217
|
+
async committedOutcome(job, reloaded, written, generated, stores, startTime) {
|
|
4218
|
+
const operationsWithContext = [...reloaded, ...written];
|
|
4219
|
+
let pendingEvent;
|
|
4220
|
+
if (operationsWithContext.length > 0) {
|
|
4221
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
|
|
3468
4222
|
pendingEvent = {
|
|
3469
4223
|
jobId: job.id,
|
|
3470
|
-
operations:
|
|
4224
|
+
operations: operationsWithContext,
|
|
3471
4225
|
jobMeta: job.meta,
|
|
4226
|
+
submittedActionIds: submittedActionIds(job),
|
|
3472
4227
|
collectionMemberships
|
|
3473
4228
|
};
|
|
3474
4229
|
}
|
|
@@ -3476,14 +4231,62 @@ var SimpleJobExecutor = class {
|
|
|
3476
4231
|
result: {
|
|
3477
4232
|
job,
|
|
3478
4233
|
success: true,
|
|
3479
|
-
operations:
|
|
3480
|
-
operationsWithContext
|
|
4234
|
+
operations: [...reloaded.map((entry) => entry.operation), ...generated],
|
|
4235
|
+
operationsWithContext,
|
|
3481
4236
|
duration: Date.now() - startTime
|
|
3482
4237
|
},
|
|
3483
4238
|
pendingEvent
|
|
3484
4239
|
};
|
|
3485
4240
|
}
|
|
3486
4241
|
/**
|
|
4242
|
+
* The stored operations of writes an earlier attempt committed, as a fresh
|
|
4243
|
+
* write would report them. State is rebuilt from the write cache, and left
|
|
4244
|
+
* out when it cannot be.
|
|
4245
|
+
*/
|
|
4246
|
+
async reloadCommitted(writes, stores, signal) {
|
|
4247
|
+
const streams = /* @__PURE__ */ new Map();
|
|
4248
|
+
for (const write of writes) {
|
|
4249
|
+
const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
|
|
4250
|
+
streams.set(key, [...streams.get(key) ?? [], write]);
|
|
4251
|
+
}
|
|
4252
|
+
const reloaded = [];
|
|
4253
|
+
for (const group of streams.values()) {
|
|
4254
|
+
const { documentId, scope, branch } = group[0];
|
|
4255
|
+
const opIds = [...new Set(group.map((write) => write.opId))];
|
|
4256
|
+
const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
|
|
4257
|
+
const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
|
|
4258
|
+
const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4259
|
+
for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
|
|
4260
|
+
operation,
|
|
4261
|
+
context: {
|
|
4262
|
+
documentId,
|
|
4263
|
+
scope,
|
|
4264
|
+
branch,
|
|
4265
|
+
documentType,
|
|
4266
|
+
resultingState: await this.rebuiltState(stores, {
|
|
4267
|
+
documentId,
|
|
4268
|
+
scope,
|
|
4269
|
+
branch
|
|
4270
|
+
}, operation, signal),
|
|
4271
|
+
ordinal: ordinals.get(operation.id) ?? 0
|
|
4272
|
+
}
|
|
4273
|
+
});
|
|
4274
|
+
}
|
|
4275
|
+
return reloaded;
|
|
4276
|
+
}
|
|
4277
|
+
async rebuiltState(stores, stream, operation, signal) {
|
|
4278
|
+
try {
|
|
4279
|
+
const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
|
|
4280
|
+
return JSON.stringify({
|
|
4281
|
+
...document.state,
|
|
4282
|
+
header: document.header
|
|
4283
|
+
});
|
|
4284
|
+
} catch (error) {
|
|
4285
|
+
this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
/**
|
|
3487
4290
|
* Drops the cached state of every stream a job wrote, after its transaction
|
|
3488
4291
|
* did not commit.
|
|
3489
4292
|
*
|
|
@@ -3512,16 +4315,6 @@ var SimpleJobExecutor = class {
|
|
|
3512
4315
|
const actions = writes.map((write) => write.action);
|
|
3513
4316
|
const generatedOperations = [];
|
|
3514
4317
|
const operationsWithContext = [];
|
|
3515
|
-
try {
|
|
3516
|
-
await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
|
|
3517
|
-
} catch (error) {
|
|
3518
|
-
return {
|
|
3519
|
-
success: false,
|
|
3520
|
-
generatedOperations,
|
|
3521
|
-
operationsWithContext,
|
|
3522
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
3523
|
-
};
|
|
3524
|
-
}
|
|
3525
4318
|
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
3526
4319
|
success: false,
|
|
3527
4320
|
generatedOperations,
|
|
@@ -3619,7 +4412,7 @@ var SimpleJobExecutor = class {
|
|
|
3619
4412
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3620
4413
|
documentVersion = docMeta.state.version;
|
|
3621
4414
|
}
|
|
3622
|
-
if (isUndoRedo(action) ||
|
|
4415
|
+
if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3623
4416
|
let document;
|
|
3624
4417
|
if (baseDocument !== void 0) document = baseDocument;
|
|
3625
4418
|
else try {
|
|
@@ -3679,7 +4472,7 @@ var SimpleJobExecutor = class {
|
|
|
3679
4472
|
branch: job.branch,
|
|
3680
4473
|
protocolVersion
|
|
3681
4474
|
};
|
|
3682
|
-
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
4475
|
+
updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
|
|
3683
4476
|
} catch (error) {
|
|
3684
4477
|
const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3685
4478
|
const enhancedError = new Error(contextMessage);
|
|
@@ -3691,6 +4484,15 @@ var SimpleJobExecutor = class {
|
|
|
3691
4484
|
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
3692
4485
|
const newOperation = operations[operations.length - 1];
|
|
3693
4486
|
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
4487
|
+
if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
|
|
4488
|
+
await signSynthesized(newOperation, action, {
|
|
4489
|
+
documentId: job.documentId,
|
|
4490
|
+
scope,
|
|
4491
|
+
branch: job.branch
|
|
4492
|
+
}, this.signer, signal);
|
|
4493
|
+
} catch (error) {
|
|
4494
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4495
|
+
}
|
|
3694
4496
|
const resultingState = JSON.stringify({
|
|
3695
4497
|
...updatedDocument.state,
|
|
3696
4498
|
header: updatedDocument.header
|
|
@@ -3791,7 +4593,7 @@ var SimpleJobExecutor = class {
|
|
|
3791
4593
|
* apply and its own reasons for it.
|
|
3792
4594
|
* - A positional or replayed run carries skips and re-appended operations,
|
|
3793
4595
|
* whose indices are not a simple ascending run from the head.
|
|
3794
|
-
* - UNDO, REDO
|
|
4596
|
+
* - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
|
|
3795
4597
|
* force a full-history rebuild, so they cannot be reduced against state
|
|
3796
4598
|
* threaded from the write before them.
|
|
3797
4599
|
* - The auth scope decides later writes against the policy earlier ones
|
|
@@ -3809,7 +4611,7 @@ var SimpleJobExecutor = class {
|
|
|
3809
4611
|
if (scope === "auth" || scope === "document") return false;
|
|
3810
4612
|
return writes.every((write) => {
|
|
3811
4613
|
const type = write.action.type;
|
|
3812
|
-
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "
|
|
4614
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
|
|
3813
4615
|
});
|
|
3814
4616
|
}
|
|
3815
4617
|
/**
|
|
@@ -3932,13 +4734,22 @@ var SimpleJobExecutor = class {
|
|
|
3932
4734
|
}
|
|
3933
4735
|
if (!backdated) return plain();
|
|
3934
4736
|
const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
|
|
3935
|
-
|
|
4737
|
+
const nothingToMove = async () => {
|
|
3936
4738
|
if (!this.featureFlags.authEnforcement) return plain();
|
|
3937
4739
|
return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
|
|
3938
|
-
}
|
|
4740
|
+
};
|
|
4741
|
+
if (conflicting.length === 0) return nothingToMove();
|
|
3939
4742
|
const nextIndex = revisions.revision[job.scope] ?? 0;
|
|
3940
4743
|
let firstConflicting = conflicting[0].index;
|
|
3941
4744
|
for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
|
|
4745
|
+
const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
|
|
4746
|
+
const conflictingIds = new Set(conflicting.map((operation) => operation.id));
|
|
4747
|
+
const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
|
|
4748
|
+
const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
|
|
4749
|
+
const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
|
|
4750
|
+
if (moving.length === 0) return nothingToMove();
|
|
4751
|
+
let firstRetracted = moving[0].index;
|
|
4752
|
+
for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
|
|
3942
4753
|
const incoming = job.actions.map((action, i) => ({
|
|
3943
4754
|
id: action.id,
|
|
3944
4755
|
index: nextIndex + i,
|
|
@@ -3949,8 +4760,8 @@ var SimpleJobExecutor = class {
|
|
|
3949
4760
|
}));
|
|
3950
4761
|
const merged = reshuffleByTimestamp({
|
|
3951
4762
|
index: nextIndex,
|
|
3952
|
-
skip: retractionSkip(nextIndex,
|
|
3953
|
-
},
|
|
4763
|
+
skip: retractionSkip(nextIndex, firstRetracted)
|
|
4764
|
+
}, moving, incoming);
|
|
3954
4765
|
stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3955
4766
|
if (!this.featureFlags.authEnforcement) return {
|
|
3956
4767
|
writes: merged.map((operation) => ({
|
|
@@ -4098,7 +4909,7 @@ var SimpleJobExecutor = class {
|
|
|
4098
4909
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
4099
4910
|
const result = await this.processActions(tail.map((operation, i) => ({
|
|
4100
4911
|
action: operation.action,
|
|
4101
|
-
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
|
|
4912
|
+
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
|
|
4102
4913
|
sourceRemote: "",
|
|
4103
4914
|
deniedReason: reevaluated[firstChange + i]
|
|
4104
4915
|
})), {
|
|
@@ -4187,36 +4998,32 @@ var SimpleJobExecutor = class {
|
|
|
4187
4998
|
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
4188
4999
|
duration: Date.now() - startTime
|
|
4189
5000
|
};
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
const
|
|
4195
|
-
if (
|
|
4196
|
-
|
|
4197
|
-
let conflictingOps;
|
|
4198
|
-
try {
|
|
4199
|
-
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
4200
|
-
} catch {
|
|
4201
|
-
conflictingOps = [];
|
|
4202
|
-
}
|
|
4203
|
-
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
4204
|
-
if (conflictingOps.length > 0) {
|
|
4205
|
-
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
5001
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
5002
|
+
const admitted = /* @__PURE__ */ new Set();
|
|
5003
|
+
let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
|
|
5004
|
+
for (;;) {
|
|
5005
|
+
const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
|
|
5006
|
+
if (unadmitted.length === 0) break;
|
|
5007
|
+
let refused;
|
|
4206
5008
|
try {
|
|
4207
|
-
|
|
4208
|
-
} catch {
|
|
4209
|
-
|
|
5009
|
+
refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
|
|
5010
|
+
} catch (error) {
|
|
5011
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4210
5012
|
}
|
|
5013
|
+
for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
|
|
5014
|
+
else admitted.add(operation);
|
|
5015
|
+
if (refused.size === 0) break;
|
|
5016
|
+
const remaining = job.operations.filter((operation) => !dropped.has(operation));
|
|
5017
|
+
if (remaining.length === 0) return {
|
|
5018
|
+
job,
|
|
5019
|
+
success: true,
|
|
5020
|
+
operations: [],
|
|
5021
|
+
operationsWithContext: [],
|
|
5022
|
+
duration: Date.now() - startTime
|
|
5023
|
+
};
|
|
5024
|
+
selection = await this.selectLoadWrites(job, remaining, stores, signal);
|
|
4211
5025
|
}
|
|
4212
|
-
const
|
|
4213
|
-
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
4214
|
-
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
4215
|
-
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
4216
|
-
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
4217
|
-
}
|
|
4218
|
-
return true;
|
|
4219
|
-
});
|
|
5026
|
+
const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
|
|
4220
5027
|
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
4221
5028
|
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
4222
5029
|
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
@@ -4237,14 +5044,6 @@ var SimpleJobExecutor = class {
|
|
|
4237
5044
|
const logicalSkip = latestRevision - minLogicalIndex;
|
|
4238
5045
|
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
4239
5046
|
}
|
|
4240
|
-
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
4241
|
-
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
4242
|
-
const incomingOpsToApply = job.operations.filter((op) => {
|
|
4243
|
-
if (existingActionIds.has(op.action.id)) return false;
|
|
4244
|
-
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
4245
|
-
seenIncomingActionIds.add(op.action.id);
|
|
4246
|
-
return true;
|
|
4247
|
-
});
|
|
4248
5047
|
if (incomingOpsToApply.length === 0) return {
|
|
4249
5048
|
job,
|
|
4250
5049
|
success: true,
|
|
@@ -4272,7 +5071,8 @@ var SimpleJobExecutor = class {
|
|
|
4272
5071
|
...operation,
|
|
4273
5072
|
id: operation.id
|
|
4274
5073
|
})));
|
|
4275
|
-
|
|
5074
|
+
const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
|
|
5075
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
|
|
4276
5076
|
let deniedReasons;
|
|
4277
5077
|
if (this.featureFlags.documentDecisions) try {
|
|
4278
5078
|
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
@@ -4324,6 +5124,71 @@ var SimpleJobExecutor = class {
|
|
|
4324
5124
|
duration: Date.now() - startTime
|
|
4325
5125
|
};
|
|
4326
5126
|
}
|
|
5127
|
+
/** The conflicting window a load opens, and which of its operations are new. */
|
|
5128
|
+
async selectLoadWrites(job, operations, stores, signal) {
|
|
5129
|
+
const scope = job.scope;
|
|
5130
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
5131
|
+
let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
|
|
5132
|
+
for (const operation of operations) {
|
|
5133
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
5134
|
+
const ts = operation.timestampUtcMs || "";
|
|
5135
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
5136
|
+
}
|
|
5137
|
+
let conflictingOps;
|
|
5138
|
+
try {
|
|
5139
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
5140
|
+
} catch {
|
|
5141
|
+
conflictingOps = [];
|
|
5142
|
+
}
|
|
5143
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
5144
|
+
if (conflictingOps.length > 0) {
|
|
5145
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
5146
|
+
try {
|
|
5147
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
5148
|
+
} catch {
|
|
5149
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
const incomingActionIds = new Set(operations.map((op) => op.action.id));
|
|
5153
|
+
const supersededBy = (op) => {
|
|
5154
|
+
let nearest;
|
|
5155
|
+
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0 && laterOp.index - laterOp.skip <= op.index && (nearest === void 0 || laterOp.index < nearest.index)) nearest = laterOp;
|
|
5156
|
+
return nearest;
|
|
5157
|
+
};
|
|
5158
|
+
const undoneActionIds = /* @__PURE__ */ new Set();
|
|
5159
|
+
let predecessorBound = minIncomingIndex;
|
|
5160
|
+
const rowsByActionId = /* @__PURE__ */ new Map();
|
|
5161
|
+
for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
|
|
5162
|
+
const liveOps = /* @__PURE__ */ new Set();
|
|
5163
|
+
for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
|
|
5164
|
+
for (const [actionId, rows] of rowsByActionId) {
|
|
5165
|
+
if (rows.some((row) => liveOps.has(row))) continue;
|
|
5166
|
+
if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
|
|
5167
|
+
else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
|
|
5168
|
+
}
|
|
5169
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
5170
|
+
if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
|
|
5171
|
+
return liveOps.has(op);
|
|
5172
|
+
});
|
|
5173
|
+
let rewoundFrom = Number.POSITIVE_INFINITY;
|
|
5174
|
+
for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
|
|
5175
|
+
const reappended = new Set(nonSupersededOps.map((op) => op.index));
|
|
5176
|
+
for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
|
|
5177
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
5178
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
5179
|
+
const incomingOpsToApply = operations.filter((op) => {
|
|
5180
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
5181
|
+
if (undoneActionIds.has(op.action.id)) return false;
|
|
5182
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
5183
|
+
seenIncomingActionIds.add(op.action.id);
|
|
5184
|
+
return true;
|
|
5185
|
+
});
|
|
5186
|
+
return {
|
|
5187
|
+
nonSupersededOps,
|
|
5188
|
+
allOpsFromMinConflictingIndex,
|
|
5189
|
+
incomingOpsToApply
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
4327
5192
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
4328
5193
|
if (!result.success) return result;
|
|
4329
5194
|
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
@@ -4470,6 +5335,7 @@ var DocumentModelRegistry = class {
|
|
|
4470
5335
|
//#endregion
|
|
4471
5336
|
//#region src/storage/kysely/keyframe-store.ts
|
|
4472
5337
|
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
5338
|
+
db;
|
|
4473
5339
|
trx;
|
|
4474
5340
|
constructor(db) {
|
|
4475
5341
|
this.db = db;
|
|
@@ -4554,6 +5420,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
4554
5420
|
//#endregion
|
|
4555
5421
|
//#region src/storage/txn.ts
|
|
4556
5422
|
var AtomicTransaction = class {
|
|
5423
|
+
documentId;
|
|
5424
|
+
documentType;
|
|
5425
|
+
scope;
|
|
5426
|
+
branch;
|
|
5427
|
+
baseRevision;
|
|
4557
5428
|
operations = [];
|
|
4558
5429
|
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
4559
5430
|
this.documentId = documentId;
|
|
@@ -4587,6 +5458,11 @@ var AtomicTransaction = class {
|
|
|
4587
5458
|
//#endregion
|
|
4588
5459
|
//#region src/storage/kysely/store.ts
|
|
4589
5460
|
var _UniqueConstraintContext = class extends Error {
|
|
5461
|
+
documentId;
|
|
5462
|
+
scope;
|
|
5463
|
+
branch;
|
|
5464
|
+
revision;
|
|
5465
|
+
stagedOps;
|
|
4590
5466
|
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
4591
5467
|
super("unique constraint");
|
|
4592
5468
|
this.documentId = documentId;
|
|
@@ -4598,6 +5474,7 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
4598
5474
|
}
|
|
4599
5475
|
};
|
|
4600
5476
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
5477
|
+
db;
|
|
4601
5478
|
trx;
|
|
4602
5479
|
constructor(db) {
|
|
4603
5480
|
this.db = db;
|
|
@@ -4697,12 +5574,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4697
5574
|
* otherwise read a snapshot taken before the locks were held.
|
|
4698
5575
|
*/
|
|
4699
5576
|
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
4700
|
-
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
5577
|
+
const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
|
|
4701
5578
|
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
5579
|
+
const sortedKeys = sql.join([...keys].sort());
|
|
4702
5580
|
await sql`
|
|
4703
5581
|
with ordered as materialized (
|
|
4704
5582
|
select key
|
|
4705
|
-
from unnest(array[${
|
|
5583
|
+
from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
|
|
4706
5584
|
order by ord
|
|
4707
5585
|
)
|
|
4708
5586
|
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
@@ -4779,6 +5657,14 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4779
5657
|
}
|
|
4780
5658
|
return false;
|
|
4781
5659
|
}
|
|
5660
|
+
/**
|
|
5661
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
5662
|
+
* last row returned), not the last row's own index. This keeps "0" an
|
|
5663
|
+
* unambiguous start-of-stream sentinel even when a page ends at index 0
|
|
5664
|
+
* (e.g. `limit: 1` on a fresh stream), which would otherwise make
|
|
5665
|
+
* `nextCursor` equal the start cursor and loop a caller that walks pages
|
|
5666
|
+
* forever.
|
|
5667
|
+
*/
|
|
4782
5668
|
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
4783
5669
|
throwIfAborted(signal);
|
|
4784
5670
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
@@ -4793,10 +5679,10 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4793
5679
|
}
|
|
4794
5680
|
if (paging) {
|
|
4795
5681
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4796
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
5682
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4797
5683
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4798
5684
|
}
|
|
4799
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
5685
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
4800
5686
|
cursor,
|
|
4801
5687
|
limit
|
|
4802
5688
|
}, signal));
|
|
@@ -4814,26 +5700,28 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4814
5700
|
limit
|
|
4815
5701
|
}, signal));
|
|
4816
5702
|
}
|
|
5703
|
+
/**
|
|
5704
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
5705
|
+
* last row returned), not the last row's own index, for the same reason as
|
|
5706
|
+
* `getSince`: a page ending at index 0 must not produce a cursor that is
|
|
5707
|
+
* indistinguishable from the start-of-stream sentinel.
|
|
5708
|
+
*/
|
|
4817
5709
|
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
4818
5710
|
throwIfAborted(signal);
|
|
4819
5711
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
|
|
4820
5712
|
if (paging) {
|
|
4821
5713
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4822
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
5714
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4823
5715
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4824
5716
|
}
|
|
4825
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
5717
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
4826
5718
|
cursor,
|
|
4827
5719
|
limit
|
|
4828
5720
|
}, signal));
|
|
4829
5721
|
}
|
|
4830
5722
|
async getRevisions(documentId, branch, signal) {
|
|
4831
5723
|
throwIfAborted(signal);
|
|
4832
|
-
const scopeRevisions = await this.queryExecutor.selectFrom("Operation as
|
|
4833
|
-
"o1.scope",
|
|
4834
|
-
"o1.index",
|
|
4835
|
-
"o1.timestampUtcMs"
|
|
4836
|
-
]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
|
|
5724
|
+
const scopeRevisions = await this.queryExecutor.selectFrom("Operation").select("scope").select((eb) => eb.fn.max("index").as("index")).where("documentId", "=", documentId).where("branch", "=", branch).groupBy("scope").execute();
|
|
4837
5725
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
|
|
4838
5726
|
const revision = {};
|
|
4839
5727
|
for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
|
|
@@ -4846,6 +5734,20 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4846
5734
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).executeTakeFirst();
|
|
4847
5735
|
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
4848
5736
|
}
|
|
5737
|
+
async findOperationIds(documentId, scope, branch, opIds, signal) {
|
|
5738
|
+
throwIfAborted(signal);
|
|
5739
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Set();
|
|
5740
|
+
const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
|
|
5741
|
+
return new Set(rows.map((row) => row.opId));
|
|
5742
|
+
}
|
|
5743
|
+
async getOperationsByIds(documentId, scope, branch, opIds, signal) {
|
|
5744
|
+
throwIfAborted(signal);
|
|
5745
|
+
if (opIds.length === 0) return [];
|
|
5746
|
+
const rows = await this.queryExecutor.selectFrom("Operation").selectAll().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "asc").execute();
|
|
5747
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5748
|
+
for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
|
|
5749
|
+
return [...latest.values()];
|
|
5750
|
+
}
|
|
4849
5751
|
rowToOperation(row) {
|
|
4850
5752
|
return {
|
|
4851
5753
|
index: row.index,
|
|
@@ -4939,8 +5841,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
4939
5841
|
}
|
|
4940
5842
|
//#endregion
|
|
4941
5843
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
4942
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4943
|
-
async function up$
|
|
5844
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
|
|
5845
|
+
async function up$19(db) {
|
|
4944
5846
|
await db.schema.createTable("Operation").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("jobId", "text", (col) => col.notNull()).addColumn("opId", "text", (col) => col.notNull()).addColumn("prevOpId", "text", (col) => col.notNull()).addColumn("writeTimestampUtcMs", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("timestampUtcMs", "timestamptz", (col) => col.notNull()).addColumn("index", "integer", (col) => col.notNull()).addColumn("action", "jsonb", (col) => col.notNull()).addColumn("skip", "integer", (col) => col.notNull()).addColumn("error", "text").addColumn("hash", "text", (col) => col.notNull()).addUniqueConstraint("unique_revision", [
|
|
4945
5847
|
"documentId",
|
|
4946
5848
|
"scope",
|
|
@@ -4965,8 +5867,8 @@ async function up$18(db) {
|
|
|
4965
5867
|
}
|
|
4966
5868
|
//#endregion
|
|
4967
5869
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
4968
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4969
|
-
async function up$
|
|
5870
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
|
|
5871
|
+
async function up$18(db) {
|
|
4970
5872
|
await db.schema.createTable("Keyframe").addColumn("id", "serial", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("revision", "integer", (col) => col.notNull()).addColumn("document", "jsonb", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_keyframe", [
|
|
4971
5873
|
"documentId",
|
|
4972
5874
|
"scope",
|
|
@@ -4982,14 +5884,14 @@ async function up$17(db) {
|
|
|
4982
5884
|
}
|
|
4983
5885
|
//#endregion
|
|
4984
5886
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
4985
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4986
|
-
async function up$
|
|
5887
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
|
|
5888
|
+
async function up$17(db) {
|
|
4987
5889
|
await db.schema.createTable("Document").addColumn("id", "text", (col) => col.primaryKey()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
4988
5890
|
}
|
|
4989
5891
|
//#endregion
|
|
4990
5892
|
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
4991
|
-
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4992
|
-
async function up$
|
|
5893
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
|
|
5894
|
+
async function up$16(db) {
|
|
4993
5895
|
await db.schema.createTable("DocumentRelationship").addColumn("id", "text", (col) => col.primaryKey()).addColumn("sourceId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("targetId", "text", (col) => col.notNull().references("Document.id").onDelete("cascade")).addColumn("relationshipType", "text", (col) => col.notNull()).addColumn("metadata", "jsonb").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_source_target_type", [
|
|
4994
5896
|
"sourceId",
|
|
4995
5897
|
"targetId",
|
|
@@ -5001,14 +5903,14 @@ async function up$15(db) {
|
|
|
5001
5903
|
}
|
|
5002
5904
|
//#endregion
|
|
5003
5905
|
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
5004
|
-
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5005
|
-
async function up$
|
|
5906
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
5907
|
+
async function up$15(db) {
|
|
5006
5908
|
await db.schema.createTable("IndexerState").addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()).addColumn("lastOperationId", "integer", (col) => col.notNull()).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
5007
5909
|
}
|
|
5008
5910
|
//#endregion
|
|
5009
5911
|
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
5010
|
-
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5011
|
-
async function up$
|
|
5912
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
5913
|
+
async function up$14(db) {
|
|
5012
5914
|
await db.schema.createTable("DocumentSnapshot").addColumn("id", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("slug", "text").addColumn("name", "text").addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("content", "jsonb", (col) => col.notNull()).addColumn("documentType", "text", (col) => col.notNull()).addColumn("lastOperationIndex", "integer", (col) => col.notNull()).addColumn("lastOperationHash", "text", (col) => col.notNull()).addColumn("lastUpdatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("snapshotVersion", "integer", (col) => col.notNull().defaultTo(1)).addColumn("identifiers", "jsonb").addColumn("metadata", "jsonb").addColumn("isDeleted", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("deletedAt", "timestamptz").addUniqueConstraint("unique_doc_scope_branch", [
|
|
5013
5915
|
"documentId",
|
|
5014
5916
|
"scope",
|
|
@@ -5029,8 +5931,8 @@ async function up$13(db) {
|
|
|
5029
5931
|
}
|
|
5030
5932
|
//#endregion
|
|
5031
5933
|
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
5032
|
-
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5033
|
-
async function up$
|
|
5934
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
5935
|
+
async function up$13(db) {
|
|
5034
5936
|
await db.schema.createTable("SlugMapping").addColumn("slug", "text", (col) => col.primaryKey()).addColumn("documentId", "text", (col) => col.notNull()).addColumn("scope", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addUniqueConstraint("unique_docid_scope_branch", [
|
|
5035
5937
|
"documentId",
|
|
5036
5938
|
"scope",
|
|
@@ -5040,14 +5942,14 @@ async function up$12(db) {
|
|
|
5040
5942
|
}
|
|
5041
5943
|
//#endregion
|
|
5042
5944
|
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
5043
|
-
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5044
|
-
async function up$
|
|
5945
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
5946
|
+
async function up$12(db) {
|
|
5045
5947
|
await db.schema.createTable("ViewState").addColumn("readModelId", "text", (col) => col.primaryKey()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(0)).addColumn("lastOperationTimestamp", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
5046
5948
|
}
|
|
5047
5949
|
//#endregion
|
|
5048
5950
|
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
5049
|
-
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5050
|
-
async function up$
|
|
5951
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
|
|
5952
|
+
async function up$11(db) {
|
|
5051
5953
|
await db.schema.createTable("document_collections").addColumn("documentId", "text", (col) => col.notNull()).addColumn("collectionId", "text", (col) => col.notNull()).addColumn("joinedOrdinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("leftOrdinal", "bigint").addPrimaryKeyConstraint("document_collections_pkey", ["documentId", "collectionId"]).execute();
|
|
5052
5954
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
5053
5955
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -5061,8 +5963,8 @@ async function up$10(db) {
|
|
|
5061
5963
|
}
|
|
5062
5964
|
//#endregion
|
|
5063
5965
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
5064
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5065
|
-
async function up$
|
|
5966
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
|
|
5967
|
+
async function up$10(db) {
|
|
5066
5968
|
await db.schema.createTable("sync_remotes").addColumn("name", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("channel_type", "text", (col) => col.notNull()).addColumn("channel_id", "text", (col) => col.notNull().defaultTo("")).addColumn("remote_name", "text", (col) => col.notNull().defaultTo("")).addColumn("channel_parameters", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("filter_document_ids", "jsonb").addColumn("filter_scopes", "jsonb").addColumn("filter_branch", "text", (col) => col.notNull().defaultTo("main")).addColumn("push_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("push_last_success_utc_ms", "text").addColumn("push_last_failure_utc_ms", "text").addColumn("push_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("pull_state", "text", (col) => col.notNull().defaultTo("idle")).addColumn("pull_last_success_utc_ms", "text").addColumn("pull_last_failure_utc_ms", "text").addColumn("pull_failure_count", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
5067
5969
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
5068
5970
|
await db.schema.createTable("sync_cursors").addColumn("remote_name", "text", (col) => col.primaryKey().references("sync_remotes.name").onDelete("cascade")).addColumn("cursor_ordinal", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_synced_at_utc_ms", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
@@ -5070,8 +5972,8 @@ async function up$9(db) {
|
|
|
5070
5972
|
}
|
|
5071
5973
|
//#endregion
|
|
5072
5974
|
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
5073
|
-
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5074
|
-
async function up$
|
|
5975
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
|
|
5976
|
+
async function up$9(db) {
|
|
5075
5977
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
5076
5978
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
5077
5979
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -5080,64 +5982,64 @@ async function up$8(db) {
|
|
|
5080
5982
|
}
|
|
5081
5983
|
//#endregion
|
|
5082
5984
|
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
5083
|
-
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5084
|
-
async function up$
|
|
5985
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
|
|
5986
|
+
async function up$8(db) {
|
|
5085
5987
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
5086
5988
|
}
|
|
5087
5989
|
//#endregion
|
|
5088
5990
|
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
5089
|
-
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5090
|
-
async function up$
|
|
5991
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
|
|
5992
|
+
async function up$7(db) {
|
|
5091
5993
|
await db.schema.createTable("sync_dead_letters").addColumn("ordinal", "serial", (col) => col.primaryKey()).addColumn("id", "text", (col) => col.unique().notNull()).addColumn("job_id", "text", (col) => col.notNull()).addColumn("job_dependencies", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("scopes", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("branch", "text", (col) => col.notNull()).addColumn("operations", "jsonb", (col) => col.notNull().defaultTo(sql`'[]'::jsonb`)).addColumn("error_source", "text", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
5092
5994
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
5093
5995
|
}
|
|
5094
5996
|
//#endregion
|
|
5095
5997
|
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
5096
|
-
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5097
|
-
async function up$
|
|
5998
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
|
|
5999
|
+
async function up$6(db) {
|
|
5098
6000
|
await db.schema.createTable("ProcessorCursor").addColumn("processorId", "text", (col) => col.primaryKey()).addColumn("factoryId", "text", (col) => col.notNull()).addColumn("driveId", "text", (col) => col.notNull()).addColumn("processorIndex", "integer", (col) => col.notNull()).addColumn("lastOrdinal", "integer", (col) => col.notNull().defaultTo(sql`0`)).addColumn("status", "text", (col) => col.notNull().defaultTo(sql`'active'`)).addColumn("lastError", "text").addColumn("lastErrorTimestamp", "timestamptz").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
5099
6001
|
}
|
|
5100
6002
|
//#endregion
|
|
5101
6003
|
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
5102
6004
|
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
5103
|
-
down: () => down$
|
|
5104
|
-
up: () => up$
|
|
6005
|
+
down: () => down$5,
|
|
6006
|
+
up: () => up$5
|
|
5105
6007
|
});
|
|
5106
6008
|
/**
|
|
5107
6009
|
* Records why authorization refused an operation. Separate from `error` so a
|
|
5108
6010
|
* denial is distinguishable from a reducer failure without matching on a
|
|
5109
6011
|
* message. Null for every operation written before decisions were enforced.
|
|
5110
6012
|
*/
|
|
5111
|
-
async function up$
|
|
6013
|
+
async function up$5(db) {
|
|
5112
6014
|
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
5113
6015
|
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
5114
6016
|
}
|
|
5115
|
-
async function down$
|
|
6017
|
+
async function down$5(db) {
|
|
5116
6018
|
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
5117
6019
|
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
5118
6020
|
}
|
|
5119
6021
|
//#endregion
|
|
5120
6022
|
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
5121
6023
|
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
5122
|
-
down: () => down$
|
|
5123
|
-
up: () => up$
|
|
6024
|
+
down: () => down$4,
|
|
6025
|
+
up: () => up$4
|
|
5124
6026
|
});
|
|
5125
6027
|
/**
|
|
5126
6028
|
* The classification a dead letter falls into, stored because it decides whether
|
|
5127
6029
|
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
5128
6030
|
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
5129
6031
|
*/
|
|
5130
|
-
async function up$
|
|
6032
|
+
async function up$4(db) {
|
|
5131
6033
|
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
5132
6034
|
}
|
|
5133
|
-
async function down$
|
|
6035
|
+
async function down$4(db) {
|
|
5134
6036
|
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
5135
6037
|
}
|
|
5136
6038
|
//#endregion
|
|
5137
6039
|
//#region src/storage/migrations/017_create_group_references.ts
|
|
5138
6040
|
var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
5139
|
-
down: () => down$
|
|
5140
|
-
up: () => up$
|
|
6041
|
+
down: () => down$3,
|
|
6042
|
+
up: () => up$3
|
|
5141
6043
|
});
|
|
5142
6044
|
/**
|
|
5143
6045
|
* One row per (document, group) reference ever discovered from an auth
|
|
@@ -5148,18 +6050,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
|
5148
6050
|
* requires (sync), and by groupId for the documents a group change affects
|
|
5149
6051
|
* (re-evaluation).
|
|
5150
6052
|
*/
|
|
5151
|
-
async function up$
|
|
6053
|
+
async function up$3(db) {
|
|
5152
6054
|
await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
|
|
5153
6055
|
await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
|
|
5154
6056
|
}
|
|
5155
|
-
async function down$
|
|
6057
|
+
async function down$3(db) {
|
|
5156
6058
|
await db.schema.dropTable("group_references").execute();
|
|
5157
6059
|
}
|
|
5158
6060
|
//#endregion
|
|
5159
6061
|
//#region src/storage/migrations/018_add_sync_remote_bound_address.ts
|
|
5160
6062
|
var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
5161
|
-
down: () => down$
|
|
5162
|
-
up: () => up$
|
|
6063
|
+
down: () => down$2,
|
|
6064
|
+
up: () => up$2
|
|
5163
6065
|
});
|
|
5164
6066
|
/**
|
|
5165
6067
|
* The address a sync channel is bound to, so a channel created by one subject
|
|
@@ -5170,17 +6072,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
|
5170
6072
|
* until its first authenticated poll adopts it. A default would claim them all
|
|
5171
6073
|
* for one address.
|
|
5172
6074
|
*/
|
|
5173
|
-
async function up$
|
|
6075
|
+
async function up$2(db) {
|
|
5174
6076
|
await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
|
|
5175
6077
|
}
|
|
5176
|
-
async function down$
|
|
6078
|
+
async function down$2(db) {
|
|
5177
6079
|
await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
|
|
5178
6080
|
}
|
|
5179
6081
|
//#endregion
|
|
5180
6082
|
//#region src/storage/migrations/019_require_action_id.ts
|
|
5181
6083
|
var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
5182
|
-
down: () => down,
|
|
5183
|
-
up: () => up
|
|
6084
|
+
down: () => down$1,
|
|
6085
|
+
up: () => up$1
|
|
5184
6086
|
});
|
|
5185
6087
|
/**
|
|
5186
6088
|
* Makes an operation whose action carries no id physically unstorable.
|
|
@@ -5207,7 +6109,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
|
5207
6109
|
* The empty string is rejected alongside null. It derives the same colliding
|
|
5208
6110
|
* operation id as an absent id, so admitting it would leave the hole open.
|
|
5209
6111
|
*/
|
|
5210
|
-
async function up(db) {
|
|
6112
|
+
async function up$1(db) {
|
|
5211
6113
|
await db.updateTable("Operation").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
|
|
5212
6114
|
await db.updateTable("operation_index_operations as oio").from("Operation as op").set({ action: sql`jsonb_set(oio.action, '{id}', to_jsonb(op.action->>'id'))` }).whereRef("oio.opId", "=", "op.opId").whereRef("oio.index", "=", "op.index").whereRef("oio.skip", "=", "op.skip").where(sql`jsonb_typeof(oio.action) = 'object' and coalesce(oio.action->>'id', '') = ''`).where(sql`coalesce(op.action->>'id', '') <> ''`).execute();
|
|
5213
6115
|
await db.updateTable("operation_index_operations").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
|
|
@@ -5219,11 +6121,24 @@ async function up(db) {
|
|
|
5219
6121
|
* their operations are now known by, and reverting them would reintroduce the
|
|
5220
6122
|
* collision the migration removed.
|
|
5221
6123
|
*/
|
|
5222
|
-
async function down(db) {
|
|
6124
|
+
async function down$1(db) {
|
|
5223
6125
|
await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
|
|
5224
6126
|
await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
|
|
5225
6127
|
}
|
|
5226
6128
|
//#endregion
|
|
6129
|
+
//#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
|
|
6130
|
+
var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
|
|
6131
|
+
down: () => down,
|
|
6132
|
+
up: () => up
|
|
6133
|
+
});
|
|
6134
|
+
/** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
|
|
6135
|
+
async function up(db) {
|
|
6136
|
+
await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
6137
|
+
}
|
|
6138
|
+
async function down(db) {
|
|
6139
|
+
await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
|
|
6140
|
+
}
|
|
6141
|
+
//#endregion
|
|
5227
6142
|
//#region src/storage/migrations/migrator.ts
|
|
5228
6143
|
const REACTOR_SCHEMA = "reactor";
|
|
5229
6144
|
const migrations = {
|
|
@@ -5245,7 +6160,8 @@ const migrations = {
|
|
|
5245
6160
|
"016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
|
|
5246
6161
|
"017_create_group_references": _017_create_group_references_exports,
|
|
5247
6162
|
"018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
|
|
5248
|
-
"019_require_action_id": _019_require_action_id_exports
|
|
6163
|
+
"019_require_action_id": _019_require_action_id_exports,
|
|
6164
|
+
"020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports
|
|
5249
6165
|
};
|
|
5250
6166
|
var ProgrammaticMigrationProvider = class {
|
|
5251
6167
|
getMigrations() {
|
|
@@ -5303,8 +6219,148 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
5303
6219
|
}
|
|
5304
6220
|
//#endregion
|
|
5305
6221
|
//#region src/core/drive-container-types.ts
|
|
5306
|
-
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6222
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6223
|
+
//#endregion
|
|
6224
|
+
//#region src/executor/worker/error-info.ts
|
|
6225
|
+
const MAX_STACK_LENGTH = 8192;
|
|
6226
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
6227
|
+
function toErrorInfo(err, depth = 0) {
|
|
6228
|
+
if (depth > MAX_CAUSE_DEPTH) return {
|
|
6229
|
+
name: "Error",
|
|
6230
|
+
message: "cause chain truncated"
|
|
6231
|
+
};
|
|
6232
|
+
if (err instanceof Error) {
|
|
6233
|
+
const info = {
|
|
6234
|
+
name: err.name || "Error",
|
|
6235
|
+
message: err.message
|
|
6236
|
+
};
|
|
6237
|
+
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
6238
|
+
if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
|
|
6239
|
+
return info;
|
|
6240
|
+
}
|
|
6241
|
+
if (typeof err === "string") return {
|
|
6242
|
+
name: "Error",
|
|
6243
|
+
message: err
|
|
6244
|
+
};
|
|
6245
|
+
if (err && typeof err === "object") {
|
|
6246
|
+
const record = err;
|
|
6247
|
+
return {
|
|
6248
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
6249
|
+
message: typeof record.message === "string" ? record.message : safeStringify(err)
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
return {
|
|
6253
|
+
name: "Error",
|
|
6254
|
+
message: safeStringify(err)
|
|
6255
|
+
};
|
|
6256
|
+
}
|
|
6257
|
+
function safeStringify(value) {
|
|
6258
|
+
if (value === null || value === void 0) return String(value);
|
|
6259
|
+
if (typeof value === "string") return value;
|
|
6260
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
6261
|
+
try {
|
|
6262
|
+
return JSON.stringify(value);
|
|
6263
|
+
} catch {
|
|
6264
|
+
return "[unserializable]";
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
function fromErrorInfo(info) {
|
|
6268
|
+
const err = new Error(info.message);
|
|
6269
|
+
Object.defineProperty(err, "name", {
|
|
6270
|
+
value: info.name,
|
|
6271
|
+
configurable: true,
|
|
6272
|
+
writable: true
|
|
6273
|
+
});
|
|
6274
|
+
if (info.stack !== void 0) err.stack = info.stack;
|
|
6275
|
+
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
6276
|
+
value: fromErrorInfo(info.cause),
|
|
6277
|
+
configurable: true,
|
|
6278
|
+
writable: true
|
|
6279
|
+
});
|
|
6280
|
+
return err;
|
|
6281
|
+
}
|
|
6282
|
+
//#endregion
|
|
6283
|
+
//#region src/executor/worker/sanitize.ts
|
|
6284
|
+
const MAX_DEPTH = 8;
|
|
6285
|
+
function errorToInfo(err) {
|
|
6286
|
+
return toErrorInfo(err);
|
|
6287
|
+
}
|
|
6288
|
+
function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
6289
|
+
if (depth > MAX_DEPTH) return "[Truncated]";
|
|
6290
|
+
if (value === null || value === void 0) return null;
|
|
6291
|
+
if (typeof value === "boolean") return value;
|
|
6292
|
+
if (typeof value === "number") return value;
|
|
6293
|
+
if (typeof value === "string") return value;
|
|
6294
|
+
if (typeof value === "bigint") return value.toString();
|
|
6295
|
+
if (typeof value === "function" || typeof value === "symbol") return null;
|
|
6296
|
+
if (value instanceof Error) return toErrorInfo(value);
|
|
6297
|
+
if (value instanceof Date) return value.toISOString();
|
|
6298
|
+
if (value instanceof Map) {
|
|
6299
|
+
const obj = {};
|
|
6300
|
+
for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
|
|
6301
|
+
return obj;
|
|
6302
|
+
}
|
|
6303
|
+
if (value instanceof Set) {
|
|
6304
|
+
const arr = [];
|
|
6305
|
+
for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
|
|
6306
|
+
return arr;
|
|
6307
|
+
}
|
|
6308
|
+
if (Array.isArray(value)) {
|
|
6309
|
+
if (seen.has(value)) return "[Circular]";
|
|
6310
|
+
seen.add(value);
|
|
6311
|
+
const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
|
|
6312
|
+
seen.delete(value);
|
|
6313
|
+
return result;
|
|
6314
|
+
}
|
|
6315
|
+
if (typeof value === "object") {
|
|
6316
|
+
const proto = Object.getPrototypeOf(value);
|
|
6317
|
+
if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
|
|
6318
|
+
if (seen.has(value)) return "[Circular]";
|
|
6319
|
+
seen.add(value);
|
|
6320
|
+
const result = {};
|
|
6321
|
+
for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
|
|
6322
|
+
seen.delete(value);
|
|
6323
|
+
return result;
|
|
6324
|
+
}
|
|
6325
|
+
return null;
|
|
6326
|
+
}
|
|
6327
|
+
//#endregion
|
|
6328
|
+
//#region src/executor/worker/forwarding-logger.ts
|
|
6329
|
+
function createForwardingLogger(post, level = "info", tags = []) {
|
|
6330
|
+
function safePost(msg) {
|
|
6331
|
+
try {
|
|
6332
|
+
post(msg);
|
|
6333
|
+
} catch {
|
|
6334
|
+
try {
|
|
6335
|
+
process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
|
|
6336
|
+
} catch {}
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
function emit(wireLevel, message, args) {
|
|
6340
|
+
const sanitizedArgs = args.map((a) => sanitizeArg(a));
|
|
6341
|
+
safePost({
|
|
6342
|
+
type: "log",
|
|
6343
|
+
level: wireLevel,
|
|
6344
|
+
message,
|
|
6345
|
+
args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
|
|
6346
|
+
timestamp: Date.now()
|
|
6347
|
+
});
|
|
6348
|
+
}
|
|
6349
|
+
return {
|
|
6350
|
+
level,
|
|
6351
|
+
verbose: (message, ...args) => emit("debug", message, args),
|
|
6352
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
6353
|
+
info: (message, ...args) => emit("info", message, args),
|
|
6354
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
6355
|
+
error: (message, ...args) => emit("error", message, args),
|
|
6356
|
+
errorHandler: ((...data) => {
|
|
6357
|
+
const first = data[0];
|
|
6358
|
+
emit("error", typeof first === "string" ? first : "", data.slice(1));
|
|
6359
|
+
}),
|
|
6360
|
+
child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
5307
6363
|
//#endregion
|
|
5308
|
-
export {
|
|
6364
|
+
export { DocumentNotFoundError as $, ModuleNotFoundError as A, authDecisionModel as B, DocumentMetaCache as C, DuplicateManifestError as D, JobExecutorEventTypes as E, summarizeSubmittedActions as F, DocumentExistence as G, APPEND_CONDITION_FAILED_PREFIX as H, targetDocumentId as I, RevisionMismatchError as J, DuplicateOperationError as K, decideAtHead as L, createConsistencyToken as M, createEmptyConsistencyToken as N, DuplicateModuleError as O, submittedActionIds as P, DocumentDeletedError as Q, selectDecisionModel as R, KyselyOperationIndex as S, DEFAULT_DEFERRED_JOB_TTL_MS as T, AppendConditionFailedError as U, buildDecisionModel as V, DocumentAlreadyExistsError as W, AuthTimestampNotMonotonicError as X, AuthEnforcementDisabledError as Y, AuthorizationDeniedError as Z, DriveCollectionId as _, DEFAULT_DRIVE_CONTAINER_TYPES as a, EventBusAggregateError as at, EventBus as b, runMigrations as c, parsePagingOptions as ct, KyselyOperationStore as d, __exportAll as dt, ExcessiveReshuffleError as et, KyselyKeyframeStore as f, PassthroughSigner as g, verifyActionSignature as h, fromErrorInfo as i, UpgradePreconditionFailedError as it, GATED_DOCUMENT_ACTIONS as j, InvalidModuleError as k, createForwardingPoolInstrumentation as l, throwIfAborted as lt, SimpleJobExecutor as m, errorToInfo as n, InvalidSignatureError as nt, REACTOR_SCHEMA as o, ReactorEventTypes as ot, DocumentModelRegistry as p, OptimisticLockError as q, sanitizeArg as r, RelationshipNotFoundError as rt, getMigrationStatus as s, matchesScope as st, createForwardingLogger as t, InvalidOperationTimestampError as tt, instrumentPgPool as u, yieldToMain as ut, KyselyExecutionScope as v, CollectionMembershipCache as w, KyselyWriteCache as x, resolveFeatureFlags as y, documentDecisionModel as z };
|
|
5309
6365
|
|
|
5310
|
-
//# sourceMappingURL=
|
|
6366
|
+
//# sourceMappingURL=forwarding-logger-CnuVDVze.js.map
|