@powerhousedao/reactor 6.2.3-dev.9 → 6.2.3-staging.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-worker-executor-BN30c2xw.js → build-worker-executor-BonPsKdx.js} +24 -8
- package/dist/build-worker-executor-BonPsKdx.js.map +1 -0
- package/dist/{document-indexer-CDX1D2RZ.js → document-indexer-I6i_YzsR.js} +866 -87
- package/dist/document-indexer-I6i_YzsR.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-aO-PASn7.js → forwarding-logger-BkwlvD9W.js} +1727 -306
- package/dist/forwarding-logger-BkwlvD9W.js.map +1 -0
- package/dist/index.d.ts +3946 -2817
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2557 -712
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +62 -20
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-CJIdjsd8.js} +24 -4
- package/dist/projection-shard-manager-CJIdjsd8.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-DjbEQkq-.js} +22 -16
- package/dist/worker-handle-DjbEQkq-.js.map +1 -0
- package/package.json +20 -10
- package/dist/build-worker-executor-BN30c2xw.js.map +0 -1
- package/dist/document-indexer-CDX1D2RZ.js.map +0 -1
- package/dist/drive-container-types-aO-PASn7.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-Bb0u-X9m.js +0 -22
- package/dist/worker-Bb0u-X9m.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
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, PEER_CAPABILITIES, 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, localSupports, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
|
|
3
2
|
import { v4 } from "uuid";
|
|
4
|
-
import { Migrator, sql } from "kysely";
|
|
3
|
+
import { Migrator, TableNode, sql } from "kysely";
|
|
5
4
|
//#region \0rolldown/runtime.js
|
|
6
5
|
var __defProp = Object.defineProperty;
|
|
7
6
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -52,6 +51,38 @@ 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
|
+
CATCHUP_SWEPT: 10010
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
55
86
|
//#region src/shared/errors.ts
|
|
56
87
|
/**
|
|
57
88
|
* Error thrown when attempting to access a deleted document.
|
|
@@ -143,6 +174,23 @@ var InvalidOperationTimestampError = class InvalidOperationTimestampError extend
|
|
|
143
174
|
return Error.isError(error) && error.name === "InvalidOperationTimestampError";
|
|
144
175
|
}
|
|
145
176
|
};
|
|
177
|
+
/** A document requires a protocol version this reactor does not run. Terminal. */
|
|
178
|
+
var UnsupportedProtocolVersionError = class UnsupportedProtocolVersionError extends Error {
|
|
179
|
+
documentId;
|
|
180
|
+
protocol;
|
|
181
|
+
version;
|
|
182
|
+
constructor(documentId, protocol, version) {
|
|
183
|
+
super(`Document ${documentId} requires ${protocol} ${version}, which this reactor does not support`);
|
|
184
|
+
this.name = "UnsupportedProtocolVersionError";
|
|
185
|
+
this.documentId = documentId;
|
|
186
|
+
this.protocol = protocol;
|
|
187
|
+
this.version = version;
|
|
188
|
+
Error.captureStackTrace(this, UnsupportedProtocolVersionError);
|
|
189
|
+
}
|
|
190
|
+
static isError(error) {
|
|
191
|
+
return Error.isError(error) && error.name === "UnsupportedProtocolVersionError";
|
|
192
|
+
}
|
|
193
|
+
};
|
|
146
194
|
/**
|
|
147
195
|
* A load would move more operations than the bound allows, indicating a real
|
|
148
196
|
* divergence between local and incoming history. Counts only first-time moves,
|
|
@@ -167,19 +215,22 @@ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
|
|
|
167
215
|
return Error.isError(error) && error.name === "ExcessiveReshuffleError";
|
|
168
216
|
}
|
|
169
217
|
};
|
|
170
|
-
/**
|
|
171
|
-
* Error thrown when an operation has an invalid signature.
|
|
172
|
-
*/
|
|
218
|
+
/** The message carries the code: only name and message cross the queue. */
|
|
173
219
|
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
174
220
|
documentId;
|
|
221
|
+
code;
|
|
175
222
|
reason;
|
|
176
|
-
constructor(documentId, reason) {
|
|
177
|
-
super(`Invalid signature in document ${documentId}: ${reason}`);
|
|
223
|
+
constructor(documentId, code, reason) {
|
|
224
|
+
super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
|
|
178
225
|
this.name = "InvalidSignatureError";
|
|
179
226
|
this.documentId = documentId;
|
|
227
|
+
this.code = code;
|
|
180
228
|
this.reason = reason;
|
|
181
229
|
Error.captureStackTrace(this, InvalidSignatureError);
|
|
182
230
|
}
|
|
231
|
+
static isError(error) {
|
|
232
|
+
return Error.isError(error) && error.name === "InvalidSignatureError";
|
|
233
|
+
}
|
|
183
234
|
};
|
|
184
235
|
/**
|
|
185
236
|
* An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
|
|
@@ -250,6 +301,46 @@ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Er
|
|
|
250
301
|
return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
|
|
251
302
|
}
|
|
252
303
|
};
|
|
304
|
+
/**
|
|
305
|
+
* Error thrown when a relationship edge an operation names does not exist.
|
|
306
|
+
*
|
|
307
|
+
* Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
|
|
308
|
+
* rebuilds a thrown error from `{ name, message, stack, cause }` alone
|
|
309
|
+
* (`reactor-browser/src/rpc/error-info.ts`), so the class identity is lost in
|
|
310
|
+
* transit.
|
|
311
|
+
*/
|
|
312
|
+
var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
|
|
313
|
+
sourceId;
|
|
314
|
+
targetId;
|
|
315
|
+
relationshipType;
|
|
316
|
+
constructor(sourceId, targetId, relationshipType) {
|
|
317
|
+
super(`No ${relationshipType} relationship from ${sourceId} to ${targetId}`);
|
|
318
|
+
this.name = "RelationshipNotFoundError";
|
|
319
|
+
this.sourceId = sourceId;
|
|
320
|
+
this.targetId = targetId;
|
|
321
|
+
this.relationshipType = relationshipType;
|
|
322
|
+
Error.captureStackTrace(this, RelationshipNotFoundError);
|
|
323
|
+
}
|
|
324
|
+
static isError(error) {
|
|
325
|
+
return Error.isError(error) && error.name === "RelationshipNotFoundError";
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
/** The store holds documents at protocol versions this reactor does not run. */
|
|
329
|
+
var UnsupportedStoredProtocolError = class UnsupportedStoredProtocolError extends Error {
|
|
330
|
+
versions;
|
|
331
|
+
documents;
|
|
332
|
+
constructor(versions, documents) {
|
|
333
|
+
const named = versions.map(({ protocol, version }) => `${protocol} ${version}`).join(", ");
|
|
334
|
+
super(`${documents} stored document(s) require ${named}, which this reactor does not run. Start a build that runs them, or accept them read-only with withUnsupportedStoredDocuments("read-only")`);
|
|
335
|
+
this.name = "UnsupportedStoredProtocolError";
|
|
336
|
+
this.versions = versions;
|
|
337
|
+
this.documents = documents;
|
|
338
|
+
Error.captureStackTrace(this, UnsupportedStoredProtocolError);
|
|
339
|
+
}
|
|
340
|
+
static isError(error) {
|
|
341
|
+
return Error.isError(error) && error.name === "UnsupportedStoredProtocolError";
|
|
342
|
+
}
|
|
343
|
+
};
|
|
253
344
|
//#endregion
|
|
254
345
|
//#region src/storage/interfaces.ts
|
|
255
346
|
/**
|
|
@@ -302,6 +393,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
|
302
393
|
* conflict, not a fault: the caller retries against the new stream heads.
|
|
303
394
|
*/
|
|
304
395
|
var AppendConditionFailedError = class extends Error {
|
|
396
|
+
condition;
|
|
305
397
|
constructor(condition) {
|
|
306
398
|
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
307
399
|
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
@@ -335,6 +427,52 @@ let DocumentExistence = /* @__PURE__ */ function(DocumentExistence) {
|
|
|
335
427
|
return DocumentExistence;
|
|
336
428
|
}({});
|
|
337
429
|
//#endregion
|
|
430
|
+
//#region src/cache/operation-index-types.ts
|
|
431
|
+
const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
432
|
+
/**
|
|
433
|
+
* Identifies the collection a remote synchronizes. Collections are drive-level
|
|
434
|
+
* abstractions (document-drive and reactor-drive), so a collection id is the
|
|
435
|
+
* drive document id plus the branch it scopes to rather than an opaque string.
|
|
436
|
+
*
|
|
437
|
+
* The canonical string form (`drive.${branch}.${driveId}`) is produced only by
|
|
438
|
+
* `key` and parsed only by `fromKey`; that string is the wire and storage
|
|
439
|
+
* representation and is byte-for-byte identical to the legacy
|
|
440
|
+
* `driveCollectionId(branch, driveId)` output, so existing `document_collections`
|
|
441
|
+
* rows and persisted remotes remain valid without migration.
|
|
442
|
+
*/
|
|
443
|
+
var DriveCollectionId = class DriveCollectionId {
|
|
444
|
+
driveId;
|
|
445
|
+
branch;
|
|
446
|
+
constructor(driveId, branch) {
|
|
447
|
+
this.driveId = driveId;
|
|
448
|
+
this.branch = branch;
|
|
449
|
+
}
|
|
450
|
+
static forDrive(driveId, branch = "main") {
|
|
451
|
+
return new DriveCollectionId(driveId, branch);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* The single deserializer for the wire/storage form. `branch` may contain
|
|
455
|
+
* dots, while `driveId` is a dot-free document id, so the drive id is the
|
|
456
|
+
* final dot-delimited segment.
|
|
457
|
+
*/
|
|
458
|
+
static fromKey(key) {
|
|
459
|
+
if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
|
|
460
|
+
const rest = key.slice(6);
|
|
461
|
+
const lastDot = rest.lastIndexOf(".");
|
|
462
|
+
if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
|
|
463
|
+
return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
|
|
464
|
+
}
|
|
465
|
+
get key() {
|
|
466
|
+
return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
|
|
467
|
+
}
|
|
468
|
+
toString() {
|
|
469
|
+
return this.key;
|
|
470
|
+
}
|
|
471
|
+
equals(other) {
|
|
472
|
+
return this.driveId === other.driveId && this.branch === other.branch;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
338
476
|
//#region src/decision/build-decision-model.ts
|
|
339
477
|
/**
|
|
340
478
|
* Reads each projection's stream through the supplied reader, recording the
|
|
@@ -690,34 +828,13 @@ function selectDecisionModel(flags, registry) {
|
|
|
690
828
|
//#endregion
|
|
691
829
|
//#region src/executor/util.ts
|
|
692
830
|
/** 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
|
-
]);
|
|
831
|
+
const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
|
|
701
832
|
/**
|
|
702
833
|
* `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
|
|
703
834
|
* so building a decision model would throw and defer the job forever.
|
|
704
835
|
*/
|
|
705
836
|
const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
|
|
706
837
|
/**
|
|
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
838
|
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
722
839
|
* Reconstructs the document header and initializes the base state.
|
|
723
840
|
*
|
|
@@ -896,6 +1013,40 @@ var TouchedStreams = class {
|
|
|
896
1013
|
return this.streams.values();
|
|
897
1014
|
}
|
|
898
1015
|
};
|
|
1016
|
+
/**
|
|
1017
|
+
* The ids of the actions the caller handed to a job. Load and reevaluation
|
|
1018
|
+
* jobs write operations nobody submitted, so they report none.
|
|
1019
|
+
*/
|
|
1020
|
+
function submittedActionIds(job) {
|
|
1021
|
+
return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Reports what became of each submitted action.
|
|
1025
|
+
*
|
|
1026
|
+
* A job's operations can include ones it only moved to a new index, so only
|
|
1027
|
+
* those carrying a submitted action are reported. Returns undefined when the
|
|
1028
|
+
* job submitted nothing, which keeps `JobInfo.result` null for the jobs that
|
|
1029
|
+
* have no caller to answer to.
|
|
1030
|
+
*/
|
|
1031
|
+
function summarizeSubmittedActions(operations, submitted) {
|
|
1032
|
+
if (!submitted || submitted.length === 0) return;
|
|
1033
|
+
const ids = new Set(submitted);
|
|
1034
|
+
const actions = [];
|
|
1035
|
+
for (const { operation, context } of operations) {
|
|
1036
|
+
if (!ids.has(operation.action.id)) continue;
|
|
1037
|
+
actions.push({
|
|
1038
|
+
actionId: operation.action.id,
|
|
1039
|
+
scope: context.scope,
|
|
1040
|
+
index: operation.index,
|
|
1041
|
+
...operationOutcome(operation)
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
if (actions.length === 0) return;
|
|
1045
|
+
return {
|
|
1046
|
+
actions,
|
|
1047
|
+
allApplied: actions.every((action) => action.kind === "applied")
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
899
1050
|
//#endregion
|
|
900
1051
|
//#region src/registry/errors.ts
|
|
901
1052
|
/**
|
|
@@ -993,6 +1144,7 @@ const JobExecutorEventTypes = {
|
|
|
993
1144
|
//#endregion
|
|
994
1145
|
//#region src/cache/collection-membership-cache.ts
|
|
995
1146
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
1147
|
+
operationIndex;
|
|
996
1148
|
cache = /* @__PURE__ */ new Map();
|
|
997
1149
|
constructor(operationIndex) {
|
|
998
1150
|
this.operationIndex = operationIndex;
|
|
@@ -1206,10 +1358,231 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1206
1358
|
return {
|
|
1207
1359
|
state: document.state.document,
|
|
1208
1360
|
documentType,
|
|
1361
|
+
protocolVersions: createAction.input.protocolVersions,
|
|
1209
1362
|
documentScopeRevision: documentScopeRevision + 1
|
|
1210
1363
|
};
|
|
1211
1364
|
}
|
|
1212
1365
|
};
|
|
1366
|
+
//#endregion
|
|
1367
|
+
//#region src/catch-up/settled-watermark.ts
|
|
1368
|
+
const MAX_PENDING_PROBES = 64;
|
|
1369
|
+
/** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
|
|
1370
|
+
function snapshotFunctionsFor(serverVersionNum) {
|
|
1371
|
+
if (serverVersionNum >= 13e4) return {
|
|
1372
|
+
snapshot: "pg_current_snapshot",
|
|
1373
|
+
currentXid: "pg_current_xact_id",
|
|
1374
|
+
xidIfAssigned: "pg_current_xact_id_if_assigned"
|
|
1375
|
+
};
|
|
1376
|
+
if (serverVersionNum >= 1e5) return {
|
|
1377
|
+
snapshot: "txid_current_snapshot",
|
|
1378
|
+
currentXid: "txid_current",
|
|
1379
|
+
xidIfAssigned: "txid_current_if_assigned"
|
|
1380
|
+
};
|
|
1381
|
+
throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
|
|
1382
|
+
}
|
|
1383
|
+
/** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
|
|
1384
|
+
function parseSnapshot(text) {
|
|
1385
|
+
const parts = text.split(":");
|
|
1386
|
+
if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
|
|
1387
|
+
const [xmin, xmax, xip] = parts;
|
|
1388
|
+
return {
|
|
1389
|
+
xmin: BigInt(xmin),
|
|
1390
|
+
xmax: BigInt(xmax),
|
|
1391
|
+
xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
async function readSnapshotFunctions(db) {
|
|
1395
|
+
const result = await sql`select current_setting('server_version_num') as version`.execute(db);
|
|
1396
|
+
return snapshotFunctionsFor(Number(result.rows[0].version));
|
|
1397
|
+
}
|
|
1398
|
+
/** A probe settles once an xmin passes the xid it took after its head. */
|
|
1399
|
+
var ProbeSettler = class {
|
|
1400
|
+
settled = 0;
|
|
1401
|
+
lastHead = 0;
|
|
1402
|
+
covered = 0;
|
|
1403
|
+
lastXip = [];
|
|
1404
|
+
pending = [];
|
|
1405
|
+
get settledThrough() {
|
|
1406
|
+
return this.settled;
|
|
1407
|
+
}
|
|
1408
|
+
get head() {
|
|
1409
|
+
return this.lastHead;
|
|
1410
|
+
}
|
|
1411
|
+
/** The highest head a settled or pending probe covers. */
|
|
1412
|
+
get probedHead() {
|
|
1413
|
+
return this.covered;
|
|
1414
|
+
}
|
|
1415
|
+
observe(head, xid, snapshot, nowUtcMs) {
|
|
1416
|
+
this.lastHead = Math.max(this.lastHead, head);
|
|
1417
|
+
this.lastXip = snapshot.xip;
|
|
1418
|
+
if (xid !== null && head > this.covered) {
|
|
1419
|
+
this.covered = head;
|
|
1420
|
+
this.pending.push({
|
|
1421
|
+
head,
|
|
1422
|
+
xid,
|
|
1423
|
+
takenAtUtcMs: nowUtcMs
|
|
1424
|
+
});
|
|
1425
|
+
if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
|
|
1426
|
+
}
|
|
1427
|
+
const stillPending = [];
|
|
1428
|
+
for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
|
|
1429
|
+
else stillPending.push(probe);
|
|
1430
|
+
this.pending = stillPending.filter((probe) => probe.head > this.settled);
|
|
1431
|
+
return this.settled;
|
|
1432
|
+
}
|
|
1433
|
+
/** Xids an unsettled probe still waits on. */
|
|
1434
|
+
waitingOn() {
|
|
1435
|
+
if (this.pending.length === 0) return [];
|
|
1436
|
+
const xid = this.pending[0].xid;
|
|
1437
|
+
return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
|
|
1438
|
+
}
|
|
1439
|
+
stalledSinceUtcMs() {
|
|
1440
|
+
return this.pending[0]?.takenAtUtcMs;
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
function operationIndexTableName(db) {
|
|
1444
|
+
const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
|
|
1445
|
+
const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
|
|
1446
|
+
const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
1447
|
+
return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
|
|
1448
|
+
}
|
|
1449
|
+
/** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
|
|
1450
|
+
function createKyselyWatermarkProbe(db) {
|
|
1451
|
+
let resolved = null;
|
|
1452
|
+
const resolve = async () => {
|
|
1453
|
+
const fns = await readSnapshotFunctions(db);
|
|
1454
|
+
const table = operationIndexTableName(db);
|
|
1455
|
+
const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
|
|
1456
|
+
if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
|
|
1457
|
+
return {
|
|
1458
|
+
fns,
|
|
1459
|
+
sequence
|
|
1460
|
+
};
|
|
1461
|
+
};
|
|
1462
|
+
return async (probedHead) => {
|
|
1463
|
+
resolved ??= resolve().catch((error) => {
|
|
1464
|
+
resolved = null;
|
|
1465
|
+
throw error;
|
|
1466
|
+
});
|
|
1467
|
+
const { fns, sequence } = await resolved;
|
|
1468
|
+
const headRow = (await sql`select coalesce(pg_sequence_last_value(${sequence}::regclass), 0) as head, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
|
|
1469
|
+
const head = Number(headRow.head);
|
|
1470
|
+
let xid = null;
|
|
1471
|
+
if (headRow.outside_write && head > probedHead) {
|
|
1472
|
+
const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
|
|
1473
|
+
xid = BigInt(xidResult.rows[0].xid);
|
|
1474
|
+
}
|
|
1475
|
+
const row = (await sql`select ${sql.raw(fns.snapshot)}()::text as snapshot, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
|
|
1476
|
+
return {
|
|
1477
|
+
head,
|
|
1478
|
+
xid,
|
|
1479
|
+
snapshot: parseSnapshot(row.snapshot),
|
|
1480
|
+
outsideWrite: headRow.outside_write && row.outside_write
|
|
1481
|
+
};
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
/** Knows when an ordinal gap is final by asking which transactions are open. */
|
|
1485
|
+
var SettledWatermark = class {
|
|
1486
|
+
probe;
|
|
1487
|
+
logger;
|
|
1488
|
+
now;
|
|
1489
|
+
settler = new ProbeSettler();
|
|
1490
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1491
|
+
current;
|
|
1492
|
+
next;
|
|
1493
|
+
warnedInsideWrite = false;
|
|
1494
|
+
constructor(probe, logger, now = Date.now) {
|
|
1495
|
+
this.probe = probe;
|
|
1496
|
+
this.logger = logger;
|
|
1497
|
+
this.now = now;
|
|
1498
|
+
}
|
|
1499
|
+
get settledThrough() {
|
|
1500
|
+
return this.settler.settledThrough;
|
|
1501
|
+
}
|
|
1502
|
+
refresh(signal) {
|
|
1503
|
+
signal?.throwIfAborted();
|
|
1504
|
+
if (this.next !== void 0) return this.next;
|
|
1505
|
+
if (this.current === void 0) return this.start();
|
|
1506
|
+
const next = this.current.catch(() => this.settledThrough).then(() => {
|
|
1507
|
+
this.next = void 0;
|
|
1508
|
+
return this.start();
|
|
1509
|
+
});
|
|
1510
|
+
this.next = next;
|
|
1511
|
+
return next;
|
|
1512
|
+
}
|
|
1513
|
+
onAdvance(listener) {
|
|
1514
|
+
this.listeners.add(listener);
|
|
1515
|
+
return () => {
|
|
1516
|
+
this.listeners.delete(listener);
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
status() {
|
|
1520
|
+
const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
|
|
1521
|
+
return {
|
|
1522
|
+
head: this.settler.head,
|
|
1523
|
+
settledThrough: this.settledThrough,
|
|
1524
|
+
waitingOn: this.settler.waitingOn(),
|
|
1525
|
+
...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
start() {
|
|
1529
|
+
const run = this.probeOnce().finally(() => {
|
|
1530
|
+
if (this.current === run) this.current = void 0;
|
|
1531
|
+
});
|
|
1532
|
+
this.current = run;
|
|
1533
|
+
return run;
|
|
1534
|
+
}
|
|
1535
|
+
async probeOnce() {
|
|
1536
|
+
const reading = await this.probe(this.settler.probedHead);
|
|
1537
|
+
if (!reading.outsideWrite) {
|
|
1538
|
+
if (!this.warnedInsideWrite) {
|
|
1539
|
+
this.warnedInsideWrite = true;
|
|
1540
|
+
this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
|
|
1541
|
+
}
|
|
1542
|
+
return this.settledThrough;
|
|
1543
|
+
}
|
|
1544
|
+
const before = this.settledThrough;
|
|
1545
|
+
const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
|
|
1546
|
+
if (after > before) for (const listener of this.listeners) try {
|
|
1547
|
+
listener(after);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
this.logger.error("settled watermark listener failed: @error", error);
|
|
1550
|
+
}
|
|
1551
|
+
return after;
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
/** Sessions holding these xids open; empty without pg_stat_activity. */
|
|
1555
|
+
async function describeWaitingSessions(db, xids) {
|
|
1556
|
+
if (xids.length === 0) return [];
|
|
1557
|
+
const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
|
|
1558
|
+
try {
|
|
1559
|
+
return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
|
|
1560
|
+
from pg_stat_activity
|
|
1561
|
+
where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
|
|
1562
|
+
pid: Number(row.pid),
|
|
1563
|
+
applicationName: row.application_name ?? "",
|
|
1564
|
+
state: row.state ?? "",
|
|
1565
|
+
xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
|
|
1566
|
+
xid: row.xid
|
|
1567
|
+
}));
|
|
1568
|
+
} catch {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/** Bind slots per statement: 16-bit on the wire, read signed in-process. */
|
|
1573
|
+
const MAX_BIND_PARAMETERS = 32767;
|
|
1574
|
+
/** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
|
|
1575
|
+
function chunkRows(rows) {
|
|
1576
|
+
if (rows.length === 0) return [];
|
|
1577
|
+
const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
|
|
1578
|
+
const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
|
|
1579
|
+
if (rows.length <= perChunk) return [rows];
|
|
1580
|
+
const chunks = [];
|
|
1581
|
+
for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
|
|
1582
|
+
return chunks;
|
|
1583
|
+
}
|
|
1584
|
+
/** Per database: the function that assigns the transaction's xid. */
|
|
1585
|
+
const xidFunctions = /* @__PURE__ */ new WeakMap();
|
|
1213
1586
|
var KyselyOperationIndexTxn = class {
|
|
1214
1587
|
collections = [];
|
|
1215
1588
|
collectionMemberships = [];
|
|
@@ -1275,6 +1648,7 @@ var KyselyOperationIndexTxn = class {
|
|
|
1275
1648
|
}
|
|
1276
1649
|
};
|
|
1277
1650
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1651
|
+
db;
|
|
1278
1652
|
trx;
|
|
1279
1653
|
constructor(db) {
|
|
1280
1654
|
this.db = db;
|
|
@@ -1331,10 +1705,11 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1331
1705
|
leftOrdinal: null
|
|
1332
1706
|
}));
|
|
1333
1707
|
for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
|
|
1334
|
-
await trx.insertInto("document_collections").values(
|
|
1708
|
+
for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
|
|
1335
1709
|
}
|
|
1336
1710
|
let operationOrdinals = [];
|
|
1337
1711
|
if (operations.length > 0) {
|
|
1712
|
+
await this.assignXid(trx);
|
|
1338
1713
|
const operationRows = operations.map((op) => ({
|
|
1339
1714
|
opId: op.id || "",
|
|
1340
1715
|
documentId: op.documentId,
|
|
@@ -1349,7 +1724,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1349
1724
|
deniedReason: op.deniedReason ?? null,
|
|
1350
1725
|
sourceRemote: op.sourceRemote
|
|
1351
1726
|
}));
|
|
1352
|
-
|
|
1727
|
+
for (const chunk of chunkRows(operationRows)) {
|
|
1728
|
+
const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
|
|
1729
|
+
operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
|
|
1730
|
+
}
|
|
1353
1731
|
}
|
|
1354
1732
|
if (memberships.length > 0) for (const m of memberships) {
|
|
1355
1733
|
const ordinal = operationOrdinals[m.operationIndex];
|
|
@@ -1382,10 +1760,37 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1382
1760
|
}
|
|
1383
1761
|
return operationOrdinals;
|
|
1384
1762
|
}
|
|
1763
|
+
async getOrdinalsInRange(after, through, limit, signal) {
|
|
1764
|
+
signal?.throwIfAborted();
|
|
1765
|
+
if (through <= after || limit <= 0) return [];
|
|
1766
|
+
return (await this.queryExecutor.selectFrom("operation_index_operations").select("ordinal").where("ordinal", ">", after).where("ordinal", "<=", through).orderBy("ordinal", "asc").limit(limit).execute()).map((row) => Number(row.ordinal));
|
|
1767
|
+
}
|
|
1768
|
+
async getByOrdinals(ordinals, signal) {
|
|
1769
|
+
signal?.throwIfAborted();
|
|
1770
|
+
if (ordinals.length === 0) return [];
|
|
1771
|
+
const results = [];
|
|
1772
|
+
for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
|
|
1773
|
+
const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
|
|
1774
|
+
const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
|
|
1775
|
+
for (const row of rows) results.push(this.rowToOperationWithContext(row));
|
|
1776
|
+
}
|
|
1777
|
+
results.sort((a, b) => a.context.ordinal - b.context.ordinal);
|
|
1778
|
+
return results;
|
|
1779
|
+
}
|
|
1780
|
+
async getStreamAfter(stream, after, signal) {
|
|
1781
|
+
signal?.throwIfAborted();
|
|
1782
|
+
return (await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", stream.documentId).where("branch", "=", stream.branch).where("scope", "=", stream.scope).where("ordinal", ">", after).orderBy("ordinal", "asc").execute()).map((row) => this.rowToOperationWithContext(row));
|
|
1783
|
+
}
|
|
1385
1784
|
async getGroupReferencers(groupId, signal) {
|
|
1386
1785
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1387
1786
|
return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1388
1787
|
}
|
|
1788
|
+
async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
|
|
1789
|
+
signal?.throwIfAborted();
|
|
1790
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1791
|
+
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();
|
|
1792
|
+
return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
|
|
1793
|
+
}
|
|
1389
1794
|
async find(collectionId, cursor, view, paging, signal) {
|
|
1390
1795
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1391
1796
|
const outerCursor = cursor ?? -1;
|
|
@@ -1399,6 +1804,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1399
1804
|
if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
|
|
1400
1805
|
if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
|
|
1401
1806
|
if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
|
|
1807
|
+
if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
|
|
1402
1808
|
return qb;
|
|
1403
1809
|
};
|
|
1404
1810
|
const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
|
|
@@ -1487,6 +1893,17 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1487
1893
|
}, signal) : void 0
|
|
1488
1894
|
};
|
|
1489
1895
|
}
|
|
1896
|
+
/** Takes the xid before the first ordinal, as the settled watermark needs. */
|
|
1897
|
+
async assignXid(trx) {
|
|
1898
|
+
let fn = xidFunctions.get(this.db);
|
|
1899
|
+
if (fn === void 0) {
|
|
1900
|
+
fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
|
|
1901
|
+
xidFunctions.set(this.db, fn);
|
|
1902
|
+
fn.catch(() => xidFunctions.delete(this.db));
|
|
1903
|
+
}
|
|
1904
|
+
const name = await fn;
|
|
1905
|
+
await sql`select ${sql.raw(name)}()`.execute(trx);
|
|
1906
|
+
}
|
|
1490
1907
|
rowToOperationWithContext(row) {
|
|
1491
1908
|
return {
|
|
1492
1909
|
operation: {
|
|
@@ -1605,21 +2022,6 @@ var RingBuffer = class {
|
|
|
1605
2022
|
}
|
|
1606
2023
|
};
|
|
1607
2024
|
//#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
2025
|
//#region src/cache/kysely-write-cache.ts
|
|
1624
2026
|
/**
|
|
1625
2027
|
* The last operation index a keyframe's document reflects for the scope. A
|
|
@@ -1758,13 +2160,13 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1758
2160
|
const snapshots = stream.ringBuffer.getAll();
|
|
1759
2161
|
if (targetRevision === void 0) {
|
|
1760
2162
|
const newest = highestRevision(snapshots);
|
|
1761
|
-
if (newest?.position ===
|
|
2163
|
+
if (newest?.position === "head") {
|
|
1762
2164
|
this.lruTracker.touch(streamKey);
|
|
1763
2165
|
return copyDocument(newest.document);
|
|
1764
2166
|
}
|
|
1765
2167
|
if (newest) {
|
|
1766
2168
|
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,
|
|
2169
|
+
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
1768
2170
|
this.lruTracker.touch(streamKey);
|
|
1769
2171
|
return document;
|
|
1770
2172
|
}
|
|
@@ -1777,7 +2179,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1777
2179
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1778
2180
|
if (newestOlder) {
|
|
1779
2181
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1780
|
-
this.store(documentId, scope, branch, targetRevision, document,
|
|
2182
|
+
this.store(documentId, scope, branch, targetRevision, document, "historical");
|
|
1781
2183
|
this.lruTracker.touch(streamKey);
|
|
1782
2184
|
return document;
|
|
1783
2185
|
}
|
|
@@ -1785,7 +2187,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1785
2187
|
}
|
|
1786
2188
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1787
2189
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1788
|
-
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ?
|
|
2190
|
+
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
1789
2191
|
return document;
|
|
1790
2192
|
}
|
|
1791
2193
|
/**
|
|
@@ -1820,7 +2222,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1820
2222
|
if (run.length === 0) return;
|
|
1821
2223
|
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1822
2224
|
const head = run[run.length - 1];
|
|
1823
|
-
this.store(documentId, scope, branch, head.revision, head.document,
|
|
2225
|
+
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
1824
2226
|
}
|
|
1825
2227
|
store(documentId, scope, branch, revision, document, position) {
|
|
1826
2228
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
@@ -1924,9 +2326,9 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1924
2326
|
* the last replayed operation are applied at the end. Creation-time 0->N seed
|
|
1925
2327
|
* upgrades carry the initial state, so they still apply immediately.
|
|
1926
2328
|
*/
|
|
1927
|
-
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
2329
|
+
async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
|
|
1928
2330
|
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1929
|
-
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
2331
|
+
const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
|
|
1930
2332
|
const documentScopeBound = scope === "document" ? targetRevision : void 0;
|
|
1931
2333
|
let document;
|
|
1932
2334
|
let startRevision;
|
|
@@ -2026,10 +2428,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2026
2428
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2027
2429
|
} else {
|
|
2028
2430
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2029
|
-
document = docModule.reducer(document, operation.action, void 0, {
|
|
2431
|
+
document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
|
|
2030
2432
|
skip: operation.skip,
|
|
2031
2433
|
protocolVersion
|
|
2032
|
-
});
|
|
2434
|
+
}), document.header.protocolVersions);
|
|
2033
2435
|
}
|
|
2034
2436
|
}
|
|
2035
2437
|
}
|
|
@@ -2063,6 +2465,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2063
2465
|
let cursor = void 0;
|
|
2064
2466
|
const pageSize = 100;
|
|
2065
2467
|
let hasMorePages;
|
|
2468
|
+
let needsFullReplay = false;
|
|
2066
2469
|
do {
|
|
2067
2470
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2068
2471
|
const paging = {
|
|
@@ -2073,26 +2476,31 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2073
2476
|
const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
|
|
2074
2477
|
for (const operation of result.results) {
|
|
2075
2478
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2479
|
+
if (keyframe && operation.skip > 0) {
|
|
2480
|
+
needsFullReplay = true;
|
|
2481
|
+
break;
|
|
2482
|
+
}
|
|
2076
2483
|
const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
|
|
2077
2484
|
document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
|
|
2078
2485
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2079
2486
|
else {
|
|
2080
2487
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2081
|
-
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2488
|
+
document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2082
2489
|
skip: operation.skip,
|
|
2083
2490
|
protocolVersion,
|
|
2084
2491
|
replayOptions: { operation },
|
|
2085
2492
|
skipIndexValidation: true
|
|
2086
|
-
});
|
|
2493
|
+
}), document.header.protocolVersions);
|
|
2087
2494
|
}
|
|
2088
2495
|
}
|
|
2089
2496
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
2090
|
-
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
2497
|
+
hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
|
|
2091
2498
|
if (hasMorePages) cursor = result.nextCursor;
|
|
2092
2499
|
} catch (err) {
|
|
2093
2500
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2094
2501
|
}
|
|
2095
2502
|
} while (hasMorePages);
|
|
2503
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2096
2504
|
document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
|
|
2097
2505
|
document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
2098
2506
|
if (pendingUpgrades.length > 0) {
|
|
@@ -2199,26 +2607,32 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2199
2607
|
if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.length > 0) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
2200
2608
|
const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
|
|
2201
2609
|
let document = copyDocument(baseDocument);
|
|
2610
|
+
let needsFullReplay = false;
|
|
2202
2611
|
try {
|
|
2203
2612
|
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
2204
2613
|
for (const operation of pagedResults.results) {
|
|
2205
2614
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2206
2615
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2616
|
+
if (operation.skip > 0) {
|
|
2617
|
+
needsFullReplay = true;
|
|
2618
|
+
break;
|
|
2619
|
+
}
|
|
2207
2620
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2208
2621
|
else {
|
|
2209
2622
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2210
|
-
document = module.reducer(document, operation.action, void 0, {
|
|
2623
|
+
document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
|
|
2211
2624
|
skip: operation.skip,
|
|
2212
2625
|
protocolVersion,
|
|
2213
2626
|
replayOptions: { operation },
|
|
2214
2627
|
skipIndexValidation: true
|
|
2215
|
-
});
|
|
2628
|
+
}), document.header.protocolVersions);
|
|
2216
2629
|
}
|
|
2217
2630
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2218
2631
|
}
|
|
2219
2632
|
} catch (err) {
|
|
2220
2633
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2221
2634
|
}
|
|
2635
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2222
2636
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
2223
2637
|
document.header.revision = revisions.revision;
|
|
2224
2638
|
if (targetRevision !== void 0) document.header.revision = {
|
|
@@ -2339,6 +2753,11 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2339
2753
|
//#endregion
|
|
2340
2754
|
//#region src/executor/execution-scope.ts
|
|
2341
2755
|
var DefaultExecutionScope = class {
|
|
2756
|
+
operationStore;
|
|
2757
|
+
operationIndex;
|
|
2758
|
+
writeCache;
|
|
2759
|
+
documentMetaCache;
|
|
2760
|
+
collectionMembershipCache;
|
|
2342
2761
|
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2343
2762
|
this.operationStore = operationStore;
|
|
2344
2763
|
this.operationIndex = operationIndex;
|
|
@@ -2358,6 +2777,13 @@ var DefaultExecutionScope = class {
|
|
|
2358
2777
|
}
|
|
2359
2778
|
};
|
|
2360
2779
|
var KyselyExecutionScope = class {
|
|
2780
|
+
db;
|
|
2781
|
+
operationStore;
|
|
2782
|
+
operationIndex;
|
|
2783
|
+
keyframeStore;
|
|
2784
|
+
writeCache;
|
|
2785
|
+
documentMetaCache;
|
|
2786
|
+
collectionMembershipCache;
|
|
2361
2787
|
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2362
2788
|
this.db = db;
|
|
2363
2789
|
this.operationStore = operationStore;
|
|
@@ -2385,7 +2811,7 @@ var KyselyExecutionScope = class {
|
|
|
2385
2811
|
};
|
|
2386
2812
|
//#endregion
|
|
2387
2813
|
//#region src/utils/reshuffle.ts
|
|
2388
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
2814
|
+
const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2389
2815
|
"CREATE_DOCUMENT",
|
|
2390
2816
|
"DELETE_DOCUMENT",
|
|
2391
2817
|
"UPGRADE_DOCUMENT",
|
|
@@ -2702,58 +3128,36 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2702
3128
|
return operations.map((operation) => reasons.get(operation.id));
|
|
2703
3129
|
}
|
|
2704
3130
|
//#endregion
|
|
2705
|
-
//#region src/cache/operation-index-types.ts
|
|
2706
|
-
const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
2707
|
-
/**
|
|
2708
|
-
* Identifies the collection a remote synchronizes. Collections are drive-level
|
|
2709
|
-
* abstractions (document-drive and reactor-drive), so a collection id is the
|
|
2710
|
-
* drive document id plus the branch it scopes to rather than an opaque string.
|
|
2711
|
-
*
|
|
2712
|
-
* The canonical string form (`drive.${branch}.${driveId}`) is produced only by
|
|
2713
|
-
* `key` and parsed only by `fromKey`; that string is the wire and storage
|
|
2714
|
-
* representation and is byte-for-byte identical to the legacy
|
|
2715
|
-
* `driveCollectionId(branch, driveId)` output, so existing `document_collections`
|
|
2716
|
-
* rows and persisted remotes remain valid without migration.
|
|
2717
|
-
*/
|
|
2718
|
-
var DriveCollectionId = class DriveCollectionId {
|
|
2719
|
-
constructor(driveId, branch) {
|
|
2720
|
-
this.driveId = driveId;
|
|
2721
|
-
this.branch = branch;
|
|
2722
|
-
}
|
|
2723
|
-
static forDrive(driveId, branch = "main") {
|
|
2724
|
-
return new DriveCollectionId(driveId, branch);
|
|
2725
|
-
}
|
|
2726
|
-
/**
|
|
2727
|
-
* The single deserializer for the wire/storage form. `branch` may contain
|
|
2728
|
-
* dots, while `driveId` is a dot-free document id, so the drive id is the
|
|
2729
|
-
* final dot-delimited segment.
|
|
2730
|
-
*/
|
|
2731
|
-
static fromKey(key) {
|
|
2732
|
-
if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
|
|
2733
|
-
const rest = key.slice(6);
|
|
2734
|
-
const lastDot = rest.lastIndexOf(".");
|
|
2735
|
-
if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
|
|
2736
|
-
return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
|
|
2737
|
-
}
|
|
2738
|
-
get key() {
|
|
2739
|
-
return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
|
|
2740
|
-
}
|
|
2741
|
-
toString() {
|
|
2742
|
-
return this.key;
|
|
2743
|
-
}
|
|
2744
|
-
equals(other) {
|
|
2745
|
-
return this.driveId === other.driveId && this.branch === other.branch;
|
|
2746
|
-
}
|
|
2747
|
-
};
|
|
2748
|
-
//#endregion
|
|
2749
3131
|
//#region src/executor/document-action-handler.ts
|
|
2750
3132
|
var DocumentActionHandler = class {
|
|
2751
|
-
|
|
3133
|
+
registry;
|
|
3134
|
+
logger;
|
|
3135
|
+
driveContainerTypes;
|
|
3136
|
+
featureFlags;
|
|
3137
|
+
decisionModel;
|
|
3138
|
+
protocolSupport;
|
|
3139
|
+
loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
|
|
3140
|
+
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
|
|
2752
3141
|
this.registry = registry;
|
|
2753
3142
|
this.logger = logger;
|
|
2754
3143
|
this.driveContainerTypes = driveContainerTypes;
|
|
2755
3144
|
this.featureFlags = featureFlags;
|
|
2756
3145
|
this.decisionModel = decisionModel;
|
|
3146
|
+
this.protocolSupport = protocolSupport;
|
|
3147
|
+
}
|
|
3148
|
+
/** Keys this reactor does not register are admitted: it cannot judge them. */
|
|
3149
|
+
unsupportedProtocol(documentId, protocolVersions) {
|
|
3150
|
+
for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
|
|
3151
|
+
const supported = this.protocolSupport[protocol];
|
|
3152
|
+
if (supported === void 0) {
|
|
3153
|
+
if (!this.loggedUnregisteredProtocols.has(protocol)) {
|
|
3154
|
+
this.loggedUnregisteredProtocols.add(protocol);
|
|
3155
|
+
this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
|
|
3156
|
+
}
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
|
|
3160
|
+
}
|
|
2757
3161
|
}
|
|
2758
3162
|
/** Whether the write arrives with its evaluation already decided. */
|
|
2759
3163
|
alreadyEvaluated(executing) {
|
|
@@ -2840,7 +3244,7 @@ var DocumentActionHandler = class {
|
|
|
2840
3244
|
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2841
3245
|
};
|
|
2842
3246
|
executing.touchedStreams.add(job.documentId, job.scope, job.branch);
|
|
2843
|
-
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing,
|
|
3247
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
|
|
2844
3248
|
indexTxn.write([{
|
|
2845
3249
|
...operation,
|
|
2846
3250
|
documentId: job.documentId,
|
|
@@ -2852,6 +3256,7 @@ var DocumentActionHandler = class {
|
|
|
2852
3256
|
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2853
3257
|
state: standing.state.document,
|
|
2854
3258
|
documentType: standing.header.documentType,
|
|
3259
|
+
protocolVersions: standing.header.protocolVersions,
|
|
2855
3260
|
documentScopeRevision: operation.index + 1
|
|
2856
3261
|
});
|
|
2857
3262
|
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
@@ -2868,6 +3273,9 @@ var DocumentActionHandler = class {
|
|
|
2868
3273
|
error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
|
|
2869
3274
|
duration: Date.now() - startTime
|
|
2870
3275
|
};
|
|
3276
|
+
const input = action.input;
|
|
3277
|
+
const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
|
|
3278
|
+
if (unsupported) return buildErrorResult(job, unsupported, startTime);
|
|
2871
3279
|
const document = createDocumentFromAction(action);
|
|
2872
3280
|
let operation = createOperation(action, 0, skip, {
|
|
2873
3281
|
documentId: document.header.id,
|
|
@@ -2875,8 +3283,8 @@ var DocumentActionHandler = class {
|
|
|
2875
3283
|
branch: job.branch
|
|
2876
3284
|
});
|
|
2877
3285
|
const resultingStateObj = {
|
|
2878
|
-
|
|
2879
|
-
|
|
3286
|
+
...document.state,
|
|
3287
|
+
header: document.header
|
|
2880
3288
|
};
|
|
2881
3289
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
2882
3290
|
const writeResult = await this.writeOperationToStore({
|
|
@@ -2893,7 +3301,7 @@ var DocumentActionHandler = class {
|
|
|
2893
3301
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2894
3302
|
};
|
|
2895
3303
|
executing.touchedStreams.add(document.header.id, job.scope, job.branch);
|
|
2896
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document,
|
|
3304
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
|
|
2897
3305
|
indexTxn.write([{
|
|
2898
3306
|
...operation,
|
|
2899
3307
|
documentId: document.header.id,
|
|
@@ -2910,6 +3318,7 @@ var DocumentActionHandler = class {
|
|
|
2910
3318
|
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
2911
3319
|
state: document.state.document,
|
|
2912
3320
|
documentType: document.header.documentType,
|
|
3321
|
+
protocolVersions: document.header.protocolVersions,
|
|
2913
3322
|
documentScopeRevision: 1
|
|
2914
3323
|
});
|
|
2915
3324
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
@@ -2953,7 +3362,7 @@ var DocumentActionHandler = class {
|
|
|
2953
3362
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2954
3363
|
};
|
|
2955
3364
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
2956
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3365
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
2957
3366
|
indexTxn.write([{
|
|
2958
3367
|
...operation,
|
|
2959
3368
|
documentId,
|
|
@@ -2965,6 +3374,7 @@ var DocumentActionHandler = class {
|
|
|
2965
3374
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
2966
3375
|
state: document.state.document,
|
|
2967
3376
|
documentType: document.header.documentType,
|
|
3377
|
+
protocolVersions: document.header.protocolVersions,
|
|
2968
3378
|
documentScopeRevision: operation.index + 1
|
|
2969
3379
|
});
|
|
2970
3380
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3003,7 +3413,7 @@ var DocumentActionHandler = class {
|
|
|
3003
3413
|
} catch (error) {
|
|
3004
3414
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3005
3415
|
}
|
|
3006
|
-
const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3416
|
+
const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3007
3417
|
for (const revisionScope of revisionScopes) {
|
|
3008
3418
|
const snapshot = input.revision[revisionScope] ?? 0;
|
|
3009
3419
|
const actual = actualRevisions[revisionScope] ?? 0;
|
|
@@ -3045,8 +3455,8 @@ var DocumentActionHandler = class {
|
|
|
3045
3455
|
branch: job.branch
|
|
3046
3456
|
});
|
|
3047
3457
|
const resultingStateObj = {
|
|
3048
|
-
|
|
3049
|
-
|
|
3458
|
+
...document.state,
|
|
3459
|
+
header: document.header
|
|
3050
3460
|
};
|
|
3051
3461
|
if (fromVersion > 0) resultingStateObj.__migrated = true;
|
|
3052
3462
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
@@ -3064,7 +3474,7 @@ var DocumentActionHandler = class {
|
|
|
3064
3474
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3065
3475
|
};
|
|
3066
3476
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3067
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3477
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3068
3478
|
for (const scope of otherScopes) executing.postCommitInvalidations.push({
|
|
3069
3479
|
documentId,
|
|
3070
3480
|
scope,
|
|
@@ -3081,6 +3491,7 @@ var DocumentActionHandler = class {
|
|
|
3081
3491
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3082
3492
|
state: document.state.document,
|
|
3083
3493
|
documentType: document.header.documentType,
|
|
3494
|
+
protocolVersions: document.header.protocolVersions,
|
|
3084
3495
|
documentScopeRevision: operation.index + 1
|
|
3085
3496
|
});
|
|
3086
3497
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3150,7 +3561,7 @@ var DocumentActionHandler = class {
|
|
|
3150
3561
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
3151
3562
|
executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
|
|
3152
3563
|
executing.touchedStreams.add(input.targetId, job.scope, job.branch);
|
|
3153
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc,
|
|
3564
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
|
|
3154
3565
|
indexTxn.write([{
|
|
3155
3566
|
...operation,
|
|
3156
3567
|
documentId: input.sourceId,
|
|
@@ -3169,6 +3580,7 @@ var DocumentActionHandler = class {
|
|
|
3169
3580
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3170
3581
|
state: sourceDoc.state.document,
|
|
3171
3582
|
documentType: sourceDoc.header.documentType,
|
|
3583
|
+
protocolVersions: sourceDoc.header.protocolVersions,
|
|
3172
3584
|
documentScopeRevision: operation.index + 1
|
|
3173
3585
|
});
|
|
3174
3586
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
@@ -3197,86 +3609,701 @@ var DocumentActionHandler = class {
|
|
|
3197
3609
|
}
|
|
3198
3610
|
};
|
|
3199
3611
|
//#endregion
|
|
3200
|
-
//#region src/
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
if (!signer) continue;
|
|
3210
|
-
if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Action ${action.id} has signer but no signatures`);
|
|
3211
|
-
const publicKey = signer.app.key;
|
|
3212
|
-
let isValid;
|
|
3213
|
-
try {
|
|
3214
|
-
const tempOperation = {
|
|
3215
|
-
id: deriveOperationId(documentId, action.scope, branch, action.id),
|
|
3216
|
-
index: 0,
|
|
3217
|
-
timestampUtcMs: action.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3218
|
-
hash: "",
|
|
3219
|
-
skip: 0,
|
|
3220
|
-
action
|
|
3221
|
-
};
|
|
3222
|
-
isValid = await this.verifier(tempOperation, publicKey);
|
|
3223
|
-
} catch (error) {
|
|
3224
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3225
|
-
throw new InvalidSignatureError(documentId, `Action ${action.id} verification failed: ${errorMessage}`);
|
|
3226
|
-
}
|
|
3227
|
-
if (!isValid) throw new InvalidSignatureError(documentId, `Action ${action.id} signature verification returned false`);
|
|
3228
|
-
}
|
|
3612
|
+
//#region src/signer/passthrough-signer.ts
|
|
3613
|
+
/**
|
|
3614
|
+
* A no-op signer that returns empty values for all methods.
|
|
3615
|
+
* Used when signing is not required.
|
|
3616
|
+
*/
|
|
3617
|
+
var PassthroughSigner = class {
|
|
3618
|
+
publicKey = {};
|
|
3619
|
+
sign() {
|
|
3620
|
+
return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
|
|
3229
3621
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
} catch (error) {
|
|
3242
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3243
|
-
throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
3244
|
-
}
|
|
3245
|
-
if (!isValid) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
3246
|
-
}
|
|
3622
|
+
verify() {
|
|
3623
|
+
return Promise.resolve();
|
|
3624
|
+
}
|
|
3625
|
+
signAction() {
|
|
3626
|
+
return Promise.resolve([
|
|
3627
|
+
"",
|
|
3628
|
+
"",
|
|
3629
|
+
"",
|
|
3630
|
+
"",
|
|
3631
|
+
""
|
|
3632
|
+
]);
|
|
3247
3633
|
}
|
|
3248
3634
|
};
|
|
3249
3635
|
//#endregion
|
|
3250
|
-
//#region src/
|
|
3251
|
-
const MAX_SKIP_THRESHOLD = 1e3;
|
|
3252
|
-
const ISO_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
3253
|
-
function isValidISOTimestamp(value) {
|
|
3254
|
-
if (!ISO_TIMESTAMP_REGEX.test(value)) return false;
|
|
3255
|
-
return !isNaN(new Date(value).getTime());
|
|
3256
|
-
}
|
|
3636
|
+
//#region src/signer/trust-policy.ts
|
|
3257
3637
|
/**
|
|
3258
|
-
*
|
|
3259
|
-
*
|
|
3260
|
-
*
|
|
3261
|
-
* standing. Never escapes executeJob: the failure goes back to being a
|
|
3262
|
-
* returned JobResult there, which is what the queue, the worker protocol and
|
|
3263
|
-
* every test expect a failed job to look like.
|
|
3638
|
+
* The policy admission asks: the reactor's own key signing as its own user is
|
|
3639
|
+
* accepted, then the host's policy decides, else the default does. `own` is
|
|
3640
|
+
* read on every ask, since its user changes on login.
|
|
3264
3641
|
*/
|
|
3265
|
-
|
|
3266
|
-
|
|
3642
|
+
function admissionTrustPolicy(own, authEnforcement, policy) {
|
|
3643
|
+
return { authorizeSigner(signer, key, documentId) {
|
|
3644
|
+
const identity = actionSignerIdentity(own);
|
|
3645
|
+
if (identity.app.key !== "" && key === identity.app.key) {
|
|
3646
|
+
if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
|
|
3647
|
+
}
|
|
3648
|
+
if (policy) return policy.authorizeSigner(signer, key, documentId);
|
|
3649
|
+
return Promise.resolve(!authEnforcement);
|
|
3650
|
+
} };
|
|
3651
|
+
}
|
|
3652
|
+
function sameUser(a, b) {
|
|
3653
|
+
return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
|
|
3654
|
+
}
|
|
3655
|
+
//#endregion
|
|
3656
|
+
//#region src/signer/did-key.ts
|
|
3657
|
+
const DID_KEY_PREFIX = "did:key:z";
|
|
3658
|
+
const P256_MULTICODEC = [128, 36];
|
|
3659
|
+
const MAX_CACHED_KEYS = 1024;
|
|
3660
|
+
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
|
|
3661
|
+
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
|
|
3662
|
+
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
|
|
3663
|
+
/** A did:key that is not a compressed P-256 public key. */
|
|
3664
|
+
var MalformedDidKeyError = class extends Error {
|
|
3665
|
+
constructor(did, detail) {
|
|
3666
|
+
super(`Malformed did:key ${did}: ${detail}`);
|
|
3667
|
+
this.name = "MalformedDidKeyError";
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
3671
|
+
/** Cached per process: decompressing the point costs a BigInt modPow. */
|
|
3672
|
+
async function importDidKey(did) {
|
|
3673
|
+
const cached = keyCache.get(did);
|
|
3674
|
+
if (cached) {
|
|
3675
|
+
keyCache.delete(did);
|
|
3676
|
+
keyCache.set(did, cached);
|
|
3677
|
+
return cached;
|
|
3678
|
+
}
|
|
3679
|
+
const raw = decompressP256(compressedPointFromDid(did), did);
|
|
3680
|
+
const key = await crypto.subtle.importKey("raw", raw.buffer, {
|
|
3681
|
+
name: "ECDSA",
|
|
3682
|
+
namedCurve: "P-256"
|
|
3683
|
+
}, false, ["verify"]);
|
|
3684
|
+
keyCache.set(did, key);
|
|
3685
|
+
if (keyCache.size > MAX_CACHED_KEYS) {
|
|
3686
|
+
const oldest = keyCache.keys().next().value;
|
|
3687
|
+
if (oldest !== void 0) keyCache.delete(oldest);
|
|
3688
|
+
}
|
|
3689
|
+
return key;
|
|
3690
|
+
}
|
|
3691
|
+
function compressedPointFromDid(did) {
|
|
3692
|
+
if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
|
|
3693
|
+
const decoded = base58Decode(did.slice(9));
|
|
3694
|
+
if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
|
|
3695
|
+
if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
|
|
3696
|
+
return decoded.subarray(2);
|
|
3697
|
+
}
|
|
3698
|
+
function decompressP256(compressed, did) {
|
|
3699
|
+
const prefix = compressed[0];
|
|
3700
|
+
if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
|
|
3701
|
+
let x = BigInt(0);
|
|
3702
|
+
for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
|
|
3703
|
+
if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
|
|
3704
|
+
const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
|
|
3705
|
+
let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
|
|
3706
|
+
if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
|
|
3707
|
+
if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
|
|
3708
|
+
const uncompressed = /* @__PURE__ */ new Uint8Array(65);
|
|
3709
|
+
uncompressed[0] = 4;
|
|
3710
|
+
uncompressed.set(bigIntToBytes(x), 1);
|
|
3711
|
+
uncompressed.set(bigIntToBytes(y), 33);
|
|
3712
|
+
return uncompressed;
|
|
3713
|
+
}
|
|
3714
|
+
function modPow(base, exp, mod) {
|
|
3715
|
+
let result = BigInt(1);
|
|
3716
|
+
let b = base % mod;
|
|
3717
|
+
let e = exp;
|
|
3718
|
+
while (e > BigInt(0)) {
|
|
3719
|
+
if (e & BigInt(1)) result = result * b % mod;
|
|
3720
|
+
e >>= BigInt(1);
|
|
3721
|
+
b = b * b % mod;
|
|
3722
|
+
}
|
|
3723
|
+
return result;
|
|
3724
|
+
}
|
|
3725
|
+
function bigIntToBytes(value) {
|
|
3726
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
3727
|
+
let v = value;
|
|
3728
|
+
for (let i = 31; i >= 0; i--) {
|
|
3729
|
+
bytes[i] = Number(v & BigInt(255));
|
|
3730
|
+
v >>= BigInt(8);
|
|
3731
|
+
}
|
|
3732
|
+
return bytes;
|
|
3733
|
+
}
|
|
3734
|
+
//#endregion
|
|
3735
|
+
//#region src/signer/verify-action-signature.ts
|
|
3736
|
+
const RENOWN_HASH_LENGTH = 44;
|
|
3737
|
+
const SHARED_HASH_LENGTH = 28;
|
|
3738
|
+
const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
|
|
3739
|
+
/**
|
|
3740
|
+
* Integrity only: identity binding and the live-id check sit with the caller.
|
|
3741
|
+
* `operation` is the incoming operation at load admission.
|
|
3742
|
+
*/
|
|
3743
|
+
async function verifyActionSignature(action, target, path, operation) {
|
|
3744
|
+
const policy = target.policy ?? "legacy";
|
|
3745
|
+
const refusal = policyRefusal(action, policy);
|
|
3746
|
+
if (refusal) return refusal;
|
|
3747
|
+
const shape = signerShape(action.context?.signer);
|
|
3748
|
+
if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
|
|
3749
|
+
ok: true,
|
|
3750
|
+
scheme: "unsigned"
|
|
3751
|
+
};
|
|
3752
|
+
if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
|
|
3753
|
+
const { signer, tuple } = shape;
|
|
3754
|
+
const scheme = schemeOf(tuple[2]);
|
|
3755
|
+
if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
|
|
3756
|
+
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`);
|
|
3757
|
+
if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
|
|
3758
|
+
if (scheme === "v2") {
|
|
3759
|
+
const refusal = await checkV2(action, tuple, target, operation);
|
|
3760
|
+
if (refusal) return refusal;
|
|
3761
|
+
} else if (path === "mutation") {
|
|
3762
|
+
if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
3763
|
+
}
|
|
3764
|
+
return verifyEcdsa(tuple, scheme, action.id);
|
|
3765
|
+
}
|
|
3766
|
+
/** Refusals a document's policy makes before any tuple is read. */
|
|
3767
|
+
function policyRefusal(action, policy) {
|
|
3768
|
+
if (action.type !== "CREATE_DOCUMENT") return;
|
|
3769
|
+
const scheme = schemeOfAction(action);
|
|
3770
|
+
const input = action.input;
|
|
3771
|
+
const documentId = input?.documentId ?? "";
|
|
3772
|
+
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;
|
|
3773
|
+
const expected = derivedCreateId(input);
|
|
3774
|
+
if (expected === documentId) return;
|
|
3775
|
+
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}`);
|
|
3776
|
+
}
|
|
3777
|
+
function derivedCreateId(input) {
|
|
3778
|
+
if (!input?.signing || !input.protocolVersions) return;
|
|
3779
|
+
try {
|
|
3780
|
+
return deriveDocumentId({
|
|
3781
|
+
documentType: input.model,
|
|
3782
|
+
createdAtUtcIso: input.signing.createdAtUtcIso,
|
|
3783
|
+
nonce: input.signing.nonce,
|
|
3784
|
+
protocolVersions: input.protocolVersions
|
|
3785
|
+
});
|
|
3786
|
+
} catch {
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
function schemeOfAction(action) {
|
|
3791
|
+
const shape = signerShape(action.context?.signer);
|
|
3792
|
+
if (shape.kind === "unsigned") return "unsigned";
|
|
3793
|
+
return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
|
|
3794
|
+
}
|
|
3795
|
+
/** Peers send arbitrary JSON, so nothing past `context` is assumed. */
|
|
3796
|
+
function signerShape(value) {
|
|
3797
|
+
if (value === void 0 || value === null) return { kind: "unsigned" };
|
|
3798
|
+
if (!isRecord(value)) return {
|
|
3799
|
+
kind: "malformed",
|
|
3800
|
+
reason: "has a signer that is not an object"
|
|
3801
|
+
};
|
|
3802
|
+
const { app, user, signatures } = value;
|
|
3803
|
+
if (app !== void 0 && app !== null && !isRecord(app)) return {
|
|
3804
|
+
kind: "malformed",
|
|
3805
|
+
reason: "has a signer.app that is not an object"
|
|
3806
|
+
};
|
|
3807
|
+
const key = isRecord(app) ? app.key : void 0;
|
|
3808
|
+
if (key !== void 0 && key !== null && typeof key !== "string") return {
|
|
3809
|
+
kind: "malformed",
|
|
3810
|
+
reason: "has a signer.app.key that is not a string"
|
|
3811
|
+
};
|
|
3812
|
+
if (!key) return { kind: "unsigned" };
|
|
3813
|
+
if (!isRecord(user) || typeof user.address !== "string") return {
|
|
3814
|
+
kind: "malformed",
|
|
3815
|
+
reason: "has a signer without a user address"
|
|
3816
|
+
};
|
|
3817
|
+
if (!Array.isArray(signatures)) return {
|
|
3818
|
+
kind: "malformed",
|
|
3819
|
+
reason: "has a signer whose signatures are not a list"
|
|
3820
|
+
};
|
|
3821
|
+
const tuple = signatures.at(-1);
|
|
3822
|
+
if (!isTuple(tuple)) return {
|
|
3823
|
+
kind: "malformed",
|
|
3824
|
+
reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
|
|
3825
|
+
};
|
|
3826
|
+
return {
|
|
3827
|
+
kind: "signed",
|
|
3828
|
+
signer: value,
|
|
3829
|
+
tuple
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
function isRecord(value) {
|
|
3833
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3834
|
+
}
|
|
3835
|
+
async function checkV2(action, tuple, target, operation) {
|
|
3836
|
+
const problem = v2TupleProblem(tuple);
|
|
3837
|
+
if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
|
|
3838
|
+
if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
|
|
3839
|
+
let expected;
|
|
3840
|
+
try {
|
|
3841
|
+
expected = await hashActionV2(action, target, action.context.signer);
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
|
|
3844
|
+
}
|
|
3845
|
+
if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
3846
|
+
}
|
|
3847
|
+
function sameInstant(a, b) {
|
|
3848
|
+
const left = Date.parse(a);
|
|
3849
|
+
return !Number.isNaN(left) && left === Date.parse(b);
|
|
3850
|
+
}
|
|
3851
|
+
function schemeOf(hash) {
|
|
3852
|
+
if (isV2ActionHash(hash)) return "v2";
|
|
3853
|
+
if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
|
|
3854
|
+
if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
|
|
3855
|
+
return "legacy-unknown";
|
|
3856
|
+
}
|
|
3857
|
+
async function legacyHash(scheme, action, target) {
|
|
3858
|
+
if (scheme === "legacy-renown") {
|
|
3859
|
+
const payload = [
|
|
3860
|
+
action.scope,
|
|
3861
|
+
action.type,
|
|
3862
|
+
JSON.stringify(action.input)
|
|
3863
|
+
].join("");
|
|
3864
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
3865
|
+
return bytesToBase64(new Uint8Array(digest));
|
|
3866
|
+
}
|
|
3867
|
+
const signer = action.context.signer;
|
|
3868
|
+
return buildOperationSignatureParams({
|
|
3869
|
+
documentId: target.documentId,
|
|
3870
|
+
signer,
|
|
3871
|
+
action,
|
|
3872
|
+
previousStateHash: ""
|
|
3873
|
+
})[2];
|
|
3874
|
+
}
|
|
3875
|
+
async function verifyEcdsa(tuple, scheme, actionId) {
|
|
3876
|
+
const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
|
|
3877
|
+
if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
|
|
3878
|
+
let cryptoKey;
|
|
3879
|
+
try {
|
|
3880
|
+
cryptoKey = await importDidKey(key);
|
|
3881
|
+
} catch (error) {
|
|
3882
|
+
return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3883
|
+
}
|
|
3884
|
+
const message = buildOperationSignatureMessage([
|
|
3885
|
+
timestamp,
|
|
3886
|
+
key,
|
|
3887
|
+
hash,
|
|
3888
|
+
prevStateHash
|
|
3889
|
+
]);
|
|
3890
|
+
let valid;
|
|
3891
|
+
try {
|
|
3892
|
+
valid = await crypto.subtle.verify({
|
|
3893
|
+
name: "ECDSA",
|
|
3894
|
+
hash: "SHA-256"
|
|
3895
|
+
}, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
|
|
3896
|
+
} catch {
|
|
3897
|
+
valid = false;
|
|
3898
|
+
}
|
|
3899
|
+
if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
|
|
3900
|
+
return {
|
|
3901
|
+
ok: true,
|
|
3902
|
+
scheme
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
function isTuple(value) {
|
|
3906
|
+
return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
|
|
3907
|
+
}
|
|
3908
|
+
function refuse(scheme, code, reason) {
|
|
3909
|
+
return {
|
|
3910
|
+
ok: false,
|
|
3911
|
+
scheme,
|
|
3912
|
+
code,
|
|
3913
|
+
reason
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
function hexToBytes(hex) {
|
|
3917
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
3918
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
3919
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
3920
|
+
return bytes;
|
|
3921
|
+
}
|
|
3922
|
+
function bytesToBase64(bytes) {
|
|
3923
|
+
let binary = "";
|
|
3924
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3925
|
+
return btoa(binary);
|
|
3926
|
+
}
|
|
3927
|
+
//#endregion
|
|
3928
|
+
//#region src/executor/synthesized-signing.ts
|
|
3929
|
+
/** Submitted types whose stored action the reducer writes in their place. */
|
|
3930
|
+
const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
|
|
3931
|
+
/**
|
|
3932
|
+
* The id of the action synthesized from `submittedId`. Derived, so the stored
|
|
3933
|
+
* operation answers for the submitted id at the live-id check.
|
|
3934
|
+
*/
|
|
3935
|
+
function synthesizedActionId(submittedId) {
|
|
3936
|
+
return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Whether `operation` stores an action the reducer made from `submitted`
|
|
3940
|
+
* rather than `submitted` itself or an operation already in `history`.
|
|
3941
|
+
*/
|
|
3942
|
+
function isSynthesized(submitted, operation, history) {
|
|
3943
|
+
if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
|
|
3944
|
+
const id = operation.action.id;
|
|
3945
|
+
if (!id) return true;
|
|
3946
|
+
if (id === submitted.id) return false;
|
|
3947
|
+
return !history.some((existing) => existing.action.id === id);
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Signs the synthesized action of `operation` in place as `signer`, under the
|
|
3951
|
+
* id {@link synthesizedActionId} derives from the submitted one. A REDO's
|
|
3952
|
+
* rebuilt action has no timestamp, so it takes the submitted one; the
|
|
3953
|
+
* operation timestamp is set to the action's, which a peer requires of a v2
|
|
3954
|
+
* tuple.
|
|
3955
|
+
*/
|
|
3956
|
+
async function signSynthesized(operation, submitted, target, signer, signal) {
|
|
3957
|
+
const synthesized = operation.action;
|
|
3958
|
+
const id = synthesizedActionId(submitted.id);
|
|
3959
|
+
const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
|
|
3960
|
+
const action = {
|
|
3961
|
+
id,
|
|
3962
|
+
timestampUtcMs,
|
|
3963
|
+
type: synthesized.type,
|
|
3964
|
+
input: synthesized.input,
|
|
3965
|
+
scope: synthesized.scope
|
|
3966
|
+
};
|
|
3967
|
+
const signature = await signer.signAction(action, {
|
|
3968
|
+
documentId: target.documentId,
|
|
3969
|
+
branch: target.branch
|
|
3970
|
+
}, signal);
|
|
3971
|
+
operation.action = {
|
|
3972
|
+
...action,
|
|
3973
|
+
context: { signer: {
|
|
3974
|
+
...actionSignerIdentity(signer),
|
|
3975
|
+
signatures: [signature]
|
|
3976
|
+
} }
|
|
3977
|
+
};
|
|
3978
|
+
operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
|
|
3979
|
+
operation.timestampUtcMs = timestampUtcMs;
|
|
3980
|
+
}
|
|
3981
|
+
//#endregion
|
|
3982
|
+
//#region src/executor/signature-admission.ts
|
|
3983
|
+
const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
|
|
3984
|
+
/** Runs once per write, when this reactor first stores it; never on re-appends. */
|
|
3985
|
+
var SignatureAdmission = class {
|
|
3986
|
+
mode;
|
|
3987
|
+
logger;
|
|
3988
|
+
eventBus;
|
|
3989
|
+
policySource;
|
|
3990
|
+
trustPolicy;
|
|
3991
|
+
trustTimeoutMs;
|
|
3992
|
+
constructor(mode, logger, eventBus, policySource = "meta", trust = {
|
|
3993
|
+
authEnforcement: false,
|
|
3994
|
+
timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
|
|
3995
|
+
}) {
|
|
3996
|
+
this.mode = mode;
|
|
3997
|
+
this.logger = logger;
|
|
3998
|
+
this.eventBus = eventBus;
|
|
3999
|
+
this.policySource = policySource;
|
|
4000
|
+
this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
|
|
4001
|
+
this.trustTimeoutMs = trust.timeoutMs;
|
|
4002
|
+
}
|
|
4003
|
+
/** The first refusal of a mutation's submitted actions, when enforcing. */
|
|
4004
|
+
async admitMutation(job, stores, signal) {
|
|
4005
|
+
const { operationStore } = stores;
|
|
4006
|
+
const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
|
|
4007
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4008
|
+
const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
|
|
4009
|
+
await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4010
|
+
const submitted = /* @__PURE__ */ new Set();
|
|
4011
|
+
for (const entry of candidates) {
|
|
4012
|
+
if (committed.has(entry)) continue;
|
|
4013
|
+
const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
|
|
4014
|
+
submitted.add(entry.opId);
|
|
4015
|
+
if (verdict.ok) continue;
|
|
4016
|
+
const refusal = this.record(job, entry, verdict, "mutation");
|
|
4017
|
+
if (refusal) return {
|
|
4018
|
+
kind: "refused",
|
|
4019
|
+
error: refusal
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
return {
|
|
4023
|
+
kind: "admitted",
|
|
4024
|
+
committed: [...committed.values()]
|
|
4025
|
+
};
|
|
4026
|
+
}
|
|
4027
|
+
/** The incoming operations a load drops, empty unless enforcing. */
|
|
4028
|
+
async admitLoad(job, operations, stores, signal) {
|
|
4029
|
+
const { operationStore } = stores;
|
|
4030
|
+
const stream = {
|
|
4031
|
+
documentId: job.documentId,
|
|
4032
|
+
scope: job.scope,
|
|
4033
|
+
branch: job.branch
|
|
4034
|
+
};
|
|
4035
|
+
const candidates = operations.map((operation) => ({
|
|
4036
|
+
...candidate(operation.action, stream),
|
|
4037
|
+
operation
|
|
4038
|
+
}));
|
|
4039
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4040
|
+
const redelivered = await this.redelivered(candidates, live, operationStore, signal);
|
|
4041
|
+
await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4042
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
4043
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
4044
|
+
const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
|
|
4045
|
+
if (verdict.ok) continue;
|
|
4046
|
+
if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
|
|
4047
|
+
}
|
|
4048
|
+
return dropped;
|
|
4049
|
+
}
|
|
4050
|
+
async verdict(entry, live, submitted, path, signal, known = false) {
|
|
4051
|
+
const verdict = await verifyActionSignature(entry.action, {
|
|
4052
|
+
documentId: entry.stream.documentId,
|
|
4053
|
+
branch: entry.stream.branch,
|
|
4054
|
+
policy: entry.policy
|
|
4055
|
+
}, path, entry.operation);
|
|
4056
|
+
if (!verdict.ok) return verdict;
|
|
4057
|
+
if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
|
|
4058
|
+
ok: false,
|
|
4059
|
+
scheme: verdict.scheme,
|
|
4060
|
+
code: "DUPLICATE_ACTION",
|
|
4061
|
+
reason: `action ${entry.action.id} is already in the stream`
|
|
4062
|
+
};
|
|
4063
|
+
if (verdict.scheme === "unsigned") {
|
|
4064
|
+
const address = entry.action.context?.signer?.user?.address;
|
|
4065
|
+
return address ? {
|
|
4066
|
+
ok: false,
|
|
4067
|
+
scheme: "unsigned",
|
|
4068
|
+
code: "UNSIGNED_IDENTITY",
|
|
4069
|
+
reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
|
|
4070
|
+
} : verdict;
|
|
4071
|
+
}
|
|
4072
|
+
if (known) return verdict;
|
|
4073
|
+
const signer = entry.action.context.signer;
|
|
4074
|
+
if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
|
|
4075
|
+
ok: false,
|
|
4076
|
+
scheme: verdict.scheme,
|
|
4077
|
+
code: "SIGNER_UNAUTHORIZED",
|
|
4078
|
+
reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
|
|
4079
|
+
};
|
|
4080
|
+
return verdict;
|
|
4081
|
+
}
|
|
4082
|
+
/** Throws when the policy does, times out or the job is aborted. */
|
|
4083
|
+
async authorize(signer, documentId, signal) {
|
|
4084
|
+
signal?.throwIfAborted();
|
|
4085
|
+
let timer;
|
|
4086
|
+
let onAbort;
|
|
4087
|
+
const stop = new Promise((_, reject) => {
|
|
4088
|
+
timer = setTimeout(() => {
|
|
4089
|
+
reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
|
|
4090
|
+
}, this.trustTimeoutMs);
|
|
4091
|
+
if (signal) {
|
|
4092
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
4093
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4094
|
+
}
|
|
4095
|
+
});
|
|
4096
|
+
try {
|
|
4097
|
+
return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
|
|
4098
|
+
} finally {
|
|
4099
|
+
clearTimeout(timer);
|
|
4100
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* The live candidates stored exactly as submitted and still standing: one
|
|
4105
|
+
* an undo or a reshuffle has since superseded is refused, not re-emitted with
|
|
4106
|
+
* the state it once produced. A synthesized operation
|
|
4107
|
+
* holds another action, so it cannot be compared; only a retry, whose first
|
|
4108
|
+
* attempt this job was, takes it as committed.
|
|
4109
|
+
*/
|
|
4110
|
+
async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
|
|
4111
|
+
const committed = /* @__PURE__ */ new Map();
|
|
4112
|
+
const toCompare = [];
|
|
4113
|
+
for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
|
|
4114
|
+
if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
|
|
4115
|
+
} else if (live.has(entry.opId)) toCompare.push(entry);
|
|
4116
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4117
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4118
|
+
const byId = new Map(stored.map((operation) => [operation.id, operation]));
|
|
4119
|
+
const matching = entries.filter((entry) => {
|
|
4120
|
+
const operation = byId.get(entry.opId);
|
|
4121
|
+
return operation && sameContent(operation.action, entry.action);
|
|
4122
|
+
});
|
|
4123
|
+
if (matching.length === 0) continue;
|
|
4124
|
+
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);
|
|
4125
|
+
for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
|
|
4126
|
+
}
|
|
4127
|
+
return committed;
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* The load candidates stored exactly as they arrive, timestamp included: a
|
|
4131
|
+
* peer re-sending what this stream holds, not a replay of its action.
|
|
4132
|
+
*/
|
|
4133
|
+
async redelivered(candidates, live, operationStore, signal) {
|
|
4134
|
+
const found = /* @__PURE__ */ new Set();
|
|
4135
|
+
const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
|
|
4136
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4137
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4138
|
+
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);
|
|
4139
|
+
}
|
|
4140
|
+
return found;
|
|
4141
|
+
}
|
|
4142
|
+
async liveOperationIds(candidates, operationStore, signal) {
|
|
4143
|
+
const live = /* @__PURE__ */ new Set();
|
|
4144
|
+
for (const [stream, entries] of byStream(candidates)) {
|
|
4145
|
+
const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
|
|
4146
|
+
for (const opId of found) live.add(opId);
|
|
4147
|
+
}
|
|
4148
|
+
return live;
|
|
4149
|
+
}
|
|
4150
|
+
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
4151
|
+
record(job, entry, refusal, path) {
|
|
4152
|
+
const enforced = this.mode === "enforce";
|
|
4153
|
+
const event = {
|
|
4154
|
+
jobId: job.id,
|
|
4155
|
+
documentId: entry.stream.documentId,
|
|
4156
|
+
scope: entry.stream.scope,
|
|
4157
|
+
branch: entry.stream.branch,
|
|
4158
|
+
actionId: entry.action.id,
|
|
4159
|
+
code: refusal.code,
|
|
4160
|
+
scheme: refusal.scheme,
|
|
4161
|
+
path,
|
|
4162
|
+
enforced,
|
|
4163
|
+
reason: refusal.reason
|
|
4164
|
+
};
|
|
4165
|
+
this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
|
|
4166
|
+
this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
|
|
4167
|
+
this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
|
|
4168
|
+
});
|
|
4169
|
+
return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
/**
|
|
4173
|
+
* A CREATE_DOCUMENT is verified under its own input. Anything else takes the
|
|
4174
|
+
* stored document's policy, or, for a document this job creates, that CREATE's.
|
|
4175
|
+
*/
|
|
4176
|
+
async function resolvePolicies(candidates, jobActions, stored) {
|
|
4177
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4178
|
+
for (const entry of candidates) {
|
|
4179
|
+
if (entry.action.type === "CREATE_DOCUMENT") {
|
|
4180
|
+
entry.policy = createPolicy(entry.action);
|
|
4181
|
+
continue;
|
|
4182
|
+
}
|
|
4183
|
+
const { documentId, branch } = entry.stream;
|
|
4184
|
+
const key = `${documentId}\u0000${branch}`;
|
|
4185
|
+
let policy = resolved.get(key);
|
|
4186
|
+
if (policy === void 0) {
|
|
4187
|
+
policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
|
|
4188
|
+
resolved.set(key, policy);
|
|
4189
|
+
}
|
|
4190
|
+
entry.policy = policy;
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
async function storedPolicy(stores, source, documentId, branch, signal) {
|
|
4194
|
+
try {
|
|
4195
|
+
if (source === "write-cache") {
|
|
4196
|
+
const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
|
|
4197
|
+
return signaturePolicyOf(document.header);
|
|
4198
|
+
}
|
|
4199
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4200
|
+
return signaturePolicyOf(meta.protocolVersions);
|
|
4201
|
+
} catch (error) {
|
|
4202
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
4203
|
+
throw error;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
function createdPolicy(actions, documentId) {
|
|
4207
|
+
const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
|
|
4208
|
+
return create ? createPolicy(create) : void 0;
|
|
4209
|
+
}
|
|
4210
|
+
function createPolicy(action) {
|
|
4211
|
+
return signaturePolicyOf(action.input);
|
|
4212
|
+
}
|
|
4213
|
+
function candidate(action, stream) {
|
|
4214
|
+
const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
|
|
4215
|
+
return {
|
|
4216
|
+
action,
|
|
4217
|
+
stream,
|
|
4218
|
+
opId: opId(action.id),
|
|
4219
|
+
synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
function isLive(entry, live) {
|
|
4223
|
+
return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
|
|
4224
|
+
}
|
|
4225
|
+
function mutationStream(action, job) {
|
|
4226
|
+
return {
|
|
4227
|
+
...actionSigningTarget(action, job.documentId, job.branch),
|
|
4228
|
+
scope: job.scope
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
function byStream(candidates) {
|
|
4232
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4233
|
+
for (const entry of candidates) {
|
|
4234
|
+
const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
|
|
4235
|
+
const group = groups.get(key) ?? [entry.stream, []];
|
|
4236
|
+
group[1].push(entry);
|
|
4237
|
+
groups.set(key, group);
|
|
4238
|
+
}
|
|
4239
|
+
return [...groups.values()];
|
|
4240
|
+
}
|
|
4241
|
+
function isRetry(job) {
|
|
4242
|
+
return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
|
|
4243
|
+
}
|
|
4244
|
+
function committedWrite(entry, opId) {
|
|
4245
|
+
return {
|
|
4246
|
+
...entry.stream,
|
|
4247
|
+
actionId: entry.action.id,
|
|
4248
|
+
opId
|
|
4249
|
+
};
|
|
4250
|
+
}
|
|
4251
|
+
function isSuperseded(operation, later) {
|
|
4252
|
+
return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
|
|
4253
|
+
}
|
|
4254
|
+
function sameContent(stored, submitted) {
|
|
4255
|
+
try {
|
|
4256
|
+
return canonicalJson(stored) === canonicalJson(submitted);
|
|
4257
|
+
} catch {
|
|
4258
|
+
return false;
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
//#endregion
|
|
4262
|
+
//#region src/executor/simple-job-executor.ts
|
|
4263
|
+
const MAX_SKIP_THRESHOLD = 1e3;
|
|
4264
|
+
const ISO_TIMESTAMP_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
4265
|
+
function isValidISOTimestamp(value) {
|
|
4266
|
+
if (!ISO_TIMESTAMP_REGEX.test(value)) return false;
|
|
4267
|
+
return !isNaN(new Date(value).getTime());
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* Carries a failed job out of the execution scope so its transaction rolls
|
|
4271
|
+
* back. A scope callback that returns commits, whatever result it returns, so
|
|
4272
|
+
* a returned failure would leave the writes the job made before it failed
|
|
4273
|
+
* standing. Never escapes executeJob: the failure goes back to being a
|
|
4274
|
+
* returned JobResult there, which is what the queue, the worker protocol and
|
|
4275
|
+
* every test expect a failed job to look like.
|
|
4276
|
+
*/
|
|
4277
|
+
var JobRollbackSignal = class extends Error {
|
|
4278
|
+
result;
|
|
4279
|
+
constructor(result) {
|
|
3267
4280
|
super("job rolled back");
|
|
3268
4281
|
this.result = result;
|
|
3269
4282
|
this.name = "JobRollbackSignal";
|
|
3270
4283
|
}
|
|
3271
4284
|
};
|
|
3272
4285
|
var SimpleJobExecutor = class {
|
|
4286
|
+
logger;
|
|
4287
|
+
registry;
|
|
4288
|
+
operationStore;
|
|
4289
|
+
eventBus;
|
|
4290
|
+
writeCache;
|
|
4291
|
+
operationIndex;
|
|
4292
|
+
documentMetaCache;
|
|
4293
|
+
collectionMembershipCache;
|
|
4294
|
+
driveContainerTypes;
|
|
3273
4295
|
config;
|
|
3274
4296
|
featureFlags;
|
|
3275
4297
|
decisionModel;
|
|
3276
|
-
|
|
4298
|
+
signatureAdmission;
|
|
3277
4299
|
documentActionHandler;
|
|
3278
4300
|
executionScope;
|
|
3279
|
-
|
|
4301
|
+
signer;
|
|
4302
|
+
/**
|
|
4303
|
+
* `signer` signs the operations the reducer synthesizes; unsigned if omitted.
|
|
4304
|
+
* `trustPolicy` decides which keys may sign as which users at admission.
|
|
4305
|
+
*/
|
|
4306
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
|
|
3280
4307
|
this.logger = logger;
|
|
3281
4308
|
this.registry = registry;
|
|
3282
4309
|
this.operationStore = operationStore;
|
|
@@ -3286,6 +4313,8 @@ var SimpleJobExecutor = class {
|
|
|
3286
4313
|
this.documentMetaCache = documentMetaCache;
|
|
3287
4314
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3288
4315
|
this.driveContainerTypes = driveContainerTypes;
|
|
4316
|
+
this.signer = signer ?? new PassthroughSigner();
|
|
4317
|
+
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3289
4318
|
this.config = {
|
|
3290
4319
|
featureFlags: config.featureFlags ?? {},
|
|
3291
4320
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
@@ -3295,12 +4324,18 @@ var SimpleJobExecutor = class {
|
|
|
3295
4324
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3296
4325
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3297
4326
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3298
|
-
batchApplies: config.batchApplies ?? true
|
|
4327
|
+
batchApplies: config.batchApplies ?? true,
|
|
4328
|
+
signatureVerification: config.signatureVerification ?? "enforce",
|
|
4329
|
+
protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols
|
|
3299
4330
|
};
|
|
3300
|
-
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3301
4331
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3302
|
-
this.
|
|
3303
|
-
|
|
4332
|
+
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
4333
|
+
signer: this.signer,
|
|
4334
|
+
authEnforcement: this.featureFlags.authEnforcement,
|
|
4335
|
+
policy: trustPolicy,
|
|
4336
|
+
timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
|
|
4337
|
+
});
|
|
4338
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
|
|
3304
4339
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3305
4340
|
}
|
|
3306
4341
|
/**
|
|
@@ -3366,6 +4401,8 @@ var SimpleJobExecutor = class {
|
|
|
3366
4401
|
async executeInScope(params) {
|
|
3367
4402
|
const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
3368
4403
|
let pendingEvent;
|
|
4404
|
+
const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
|
|
4405
|
+
if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
|
|
3369
4406
|
const indexTxn = stores.operationIndex.start();
|
|
3370
4407
|
if (job.kind === "load") {
|
|
3371
4408
|
const loadResult = await this.executeLoadJob({
|
|
@@ -3429,10 +4466,29 @@ var SimpleJobExecutor = class {
|
|
|
3429
4466
|
pendingEvent
|
|
3430
4467
|
};
|
|
3431
4468
|
}
|
|
3432
|
-
|
|
4469
|
+
let admission;
|
|
4470
|
+
try {
|
|
4471
|
+
admission = await this.signatureAdmission.admitMutation(job, stores, signal);
|
|
4472
|
+
} catch (error) {
|
|
4473
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4474
|
+
}
|
|
4475
|
+
if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
|
|
4476
|
+
let committed;
|
|
4477
|
+
try {
|
|
4478
|
+
committed = await this.reloadCommitted(admission.committed, stores, signal);
|
|
4479
|
+
} catch (error) {
|
|
4480
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4481
|
+
}
|
|
4482
|
+
const committedIds = new Set(admission.committed.map((write) => write.actionId));
|
|
4483
|
+
const fresh = committedIds.size === 0 ? job : {
|
|
4484
|
+
...job,
|
|
4485
|
+
actions: job.actions.filter((action) => !committedIds.has(action.id))
|
|
4486
|
+
};
|
|
4487
|
+
if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
|
|
4488
|
+
const positioned = await this.positionByTimestamp(fresh, stores, signal);
|
|
3433
4489
|
if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
|
|
3434
4490
|
const executing = {
|
|
3435
|
-
job,
|
|
4491
|
+
job: fresh,
|
|
3436
4492
|
startTime,
|
|
3437
4493
|
indexTxn,
|
|
3438
4494
|
stores,
|
|
@@ -3462,13 +4518,33 @@ var SimpleJobExecutor = class {
|
|
|
3462
4518
|
} };
|
|
3463
4519
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
3464
4520
|
postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
4521
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
4522
|
+
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
4523
|
+
}
|
|
4524
|
+
/** A stored document at a version this reactor does not run is read-only here. */
|
|
4525
|
+
async unsupportedStoredProtocol(job, stores, signal) {
|
|
4526
|
+
let versions;
|
|
4527
|
+
try {
|
|
4528
|
+
versions = this.featureFlags.documentDecisions && job.kind !== "load" ? (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).header.protocolVersions : (await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal)).protocolVersions;
|
|
4529
|
+
} catch {
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* A successful mutation's result and the write-ready event it owes, carrying
|
|
4536
|
+
* what earlier attempts committed ahead of what this one wrote.
|
|
4537
|
+
*/
|
|
4538
|
+
async committedOutcome(job, reloaded, written, generated, stores, startTime) {
|
|
4539
|
+
const operationsWithContext = [...reloaded, ...written];
|
|
4540
|
+
let pendingEvent;
|
|
4541
|
+
if (operationsWithContext.length > 0) {
|
|
4542
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
|
|
3468
4543
|
pendingEvent = {
|
|
3469
4544
|
jobId: job.id,
|
|
3470
|
-
operations:
|
|
4545
|
+
operations: operationsWithContext,
|
|
3471
4546
|
jobMeta: job.meta,
|
|
4547
|
+
submittedActionIds: submittedActionIds(job),
|
|
3472
4548
|
collectionMemberships
|
|
3473
4549
|
};
|
|
3474
4550
|
}
|
|
@@ -3476,14 +4552,62 @@ var SimpleJobExecutor = class {
|
|
|
3476
4552
|
result: {
|
|
3477
4553
|
job,
|
|
3478
4554
|
success: true,
|
|
3479
|
-
operations:
|
|
3480
|
-
operationsWithContext
|
|
4555
|
+
operations: [...reloaded.map((entry) => entry.operation), ...generated],
|
|
4556
|
+
operationsWithContext,
|
|
3481
4557
|
duration: Date.now() - startTime
|
|
3482
4558
|
},
|
|
3483
4559
|
pendingEvent
|
|
3484
4560
|
};
|
|
3485
4561
|
}
|
|
3486
4562
|
/**
|
|
4563
|
+
* The stored operations of writes an earlier attempt committed, as a fresh
|
|
4564
|
+
* write would report them. State is rebuilt from the write cache, and left
|
|
4565
|
+
* out when it cannot be.
|
|
4566
|
+
*/
|
|
4567
|
+
async reloadCommitted(writes, stores, signal) {
|
|
4568
|
+
const streams = /* @__PURE__ */ new Map();
|
|
4569
|
+
for (const write of writes) {
|
|
4570
|
+
const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
|
|
4571
|
+
streams.set(key, [...streams.get(key) ?? [], write]);
|
|
4572
|
+
}
|
|
4573
|
+
const reloaded = [];
|
|
4574
|
+
for (const group of streams.values()) {
|
|
4575
|
+
const { documentId, scope, branch } = group[0];
|
|
4576
|
+
const opIds = [...new Set(group.map((write) => write.opId))];
|
|
4577
|
+
const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
|
|
4578
|
+
const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
|
|
4579
|
+
const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4580
|
+
for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
|
|
4581
|
+
operation,
|
|
4582
|
+
context: {
|
|
4583
|
+
documentId,
|
|
4584
|
+
scope,
|
|
4585
|
+
branch,
|
|
4586
|
+
documentType,
|
|
4587
|
+
resultingState: await this.rebuiltState(stores, {
|
|
4588
|
+
documentId,
|
|
4589
|
+
scope,
|
|
4590
|
+
branch
|
|
4591
|
+
}, operation, signal),
|
|
4592
|
+
ordinal: ordinals.get(operation.id) ?? 0
|
|
4593
|
+
}
|
|
4594
|
+
});
|
|
4595
|
+
}
|
|
4596
|
+
return reloaded;
|
|
4597
|
+
}
|
|
4598
|
+
async rebuiltState(stores, stream, operation, signal) {
|
|
4599
|
+
try {
|
|
4600
|
+
const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
|
|
4601
|
+
return JSON.stringify({
|
|
4602
|
+
...document.state,
|
|
4603
|
+
header: document.header
|
|
4604
|
+
});
|
|
4605
|
+
} catch (error) {
|
|
4606
|
+
this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
3487
4611
|
* Drops the cached state of every stream a job wrote, after its transaction
|
|
3488
4612
|
* did not commit.
|
|
3489
4613
|
*
|
|
@@ -3512,16 +4636,6 @@ var SimpleJobExecutor = class {
|
|
|
3512
4636
|
const actions = writes.map((write) => write.action);
|
|
3513
4637
|
const generatedOperations = [];
|
|
3514
4638
|
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
4639
|
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
3526
4640
|
success: false,
|
|
3527
4641
|
generatedOperations,
|
|
@@ -3619,7 +4733,7 @@ var SimpleJobExecutor = class {
|
|
|
3619
4733
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3620
4734
|
documentVersion = docMeta.state.version;
|
|
3621
4735
|
}
|
|
3622
|
-
if (isUndoRedo(action) ||
|
|
4736
|
+
if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3623
4737
|
let document;
|
|
3624
4738
|
if (baseDocument !== void 0) document = baseDocument;
|
|
3625
4739
|
else try {
|
|
@@ -3679,7 +4793,7 @@ var SimpleJobExecutor = class {
|
|
|
3679
4793
|
branch: job.branch,
|
|
3680
4794
|
protocolVersion
|
|
3681
4795
|
};
|
|
3682
|
-
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
4796
|
+
updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
|
|
3683
4797
|
} catch (error) {
|
|
3684
4798
|
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
4799
|
const enhancedError = new Error(contextMessage);
|
|
@@ -3691,6 +4805,15 @@ var SimpleJobExecutor = class {
|
|
|
3691
4805
|
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
3692
4806
|
const newOperation = operations[operations.length - 1];
|
|
3693
4807
|
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
4808
|
+
if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
|
|
4809
|
+
await signSynthesized(newOperation, action, {
|
|
4810
|
+
documentId: job.documentId,
|
|
4811
|
+
scope,
|
|
4812
|
+
branch: job.branch
|
|
4813
|
+
}, this.signer, signal);
|
|
4814
|
+
} catch (error) {
|
|
4815
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4816
|
+
}
|
|
3694
4817
|
const resultingState = JSON.stringify({
|
|
3695
4818
|
...updatedDocument.state,
|
|
3696
4819
|
header: updatedDocument.header
|
|
@@ -3791,7 +4914,7 @@ var SimpleJobExecutor = class {
|
|
|
3791
4914
|
* apply and its own reasons for it.
|
|
3792
4915
|
* - A positional or replayed run carries skips and re-appended operations,
|
|
3793
4916
|
* whose indices are not a simple ascending run from the head.
|
|
3794
|
-
* - UNDO, REDO
|
|
4917
|
+
* - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
|
|
3795
4918
|
* force a full-history rebuild, so they cannot be reduced against state
|
|
3796
4919
|
* threaded from the write before them.
|
|
3797
4920
|
* - The auth scope decides later writes against the policy earlier ones
|
|
@@ -3809,7 +4932,7 @@ var SimpleJobExecutor = class {
|
|
|
3809
4932
|
if (scope === "auth" || scope === "document") return false;
|
|
3810
4933
|
return writes.every((write) => {
|
|
3811
4934
|
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 !== "
|
|
4935
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
|
|
3813
4936
|
});
|
|
3814
4937
|
}
|
|
3815
4938
|
/**
|
|
@@ -4107,7 +5230,7 @@ var SimpleJobExecutor = class {
|
|
|
4107
5230
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
4108
5231
|
const result = await this.processActions(tail.map((operation, i) => ({
|
|
4109
5232
|
action: operation.action,
|
|
4110
|
-
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
|
|
5233
|
+
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
|
|
4111
5234
|
sourceRemote: "",
|
|
4112
5235
|
deniedReason: reevaluated[firstChange + i]
|
|
4113
5236
|
})), {
|
|
@@ -4196,36 +5319,32 @@ var SimpleJobExecutor = class {
|
|
|
4196
5319
|
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
4197
5320
|
duration: Date.now() - startTime
|
|
4198
5321
|
};
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
const
|
|
4204
|
-
if (
|
|
4205
|
-
|
|
4206
|
-
let conflictingOps;
|
|
4207
|
-
try {
|
|
4208
|
-
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
4209
|
-
} catch {
|
|
4210
|
-
conflictingOps = [];
|
|
4211
|
-
}
|
|
4212
|
-
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
4213
|
-
if (conflictingOps.length > 0) {
|
|
4214
|
-
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
5322
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
5323
|
+
const admitted = /* @__PURE__ */ new Set();
|
|
5324
|
+
let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
|
|
5325
|
+
for (;;) {
|
|
5326
|
+
const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
|
|
5327
|
+
if (unadmitted.length === 0) break;
|
|
5328
|
+
let refused;
|
|
4215
5329
|
try {
|
|
4216
|
-
|
|
4217
|
-
} catch {
|
|
4218
|
-
|
|
5330
|
+
refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
|
|
5331
|
+
} catch (error) {
|
|
5332
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4219
5333
|
}
|
|
5334
|
+
for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
|
|
5335
|
+
else admitted.add(operation);
|
|
5336
|
+
if (refused.size === 0) break;
|
|
5337
|
+
const remaining = job.operations.filter((operation) => !dropped.has(operation));
|
|
5338
|
+
if (remaining.length === 0) return {
|
|
5339
|
+
job,
|
|
5340
|
+
success: true,
|
|
5341
|
+
operations: [],
|
|
5342
|
+
operationsWithContext: [],
|
|
5343
|
+
duration: Date.now() - startTime
|
|
5344
|
+
};
|
|
5345
|
+
selection = await this.selectLoadWrites(job, remaining, stores, signal);
|
|
4220
5346
|
}
|
|
4221
|
-
const
|
|
4222
|
-
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
4223
|
-
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
4224
|
-
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
4225
|
-
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
4226
|
-
}
|
|
4227
|
-
return true;
|
|
4228
|
-
});
|
|
5347
|
+
const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
|
|
4229
5348
|
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
4230
5349
|
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
4231
5350
|
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
@@ -4246,14 +5365,6 @@ var SimpleJobExecutor = class {
|
|
|
4246
5365
|
const logicalSkip = latestRevision - minLogicalIndex;
|
|
4247
5366
|
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
4248
5367
|
}
|
|
4249
|
-
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
4250
|
-
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
4251
|
-
const incomingOpsToApply = job.operations.filter((op) => {
|
|
4252
|
-
if (existingActionIds.has(op.action.id)) return false;
|
|
4253
|
-
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
4254
|
-
seenIncomingActionIds.add(op.action.id);
|
|
4255
|
-
return true;
|
|
4256
|
-
});
|
|
4257
5368
|
if (incomingOpsToApply.length === 0) return {
|
|
4258
5369
|
job,
|
|
4259
5370
|
success: true,
|
|
@@ -4281,7 +5392,8 @@ var SimpleJobExecutor = class {
|
|
|
4281
5392
|
...operation,
|
|
4282
5393
|
id: operation.id
|
|
4283
5394
|
})));
|
|
4284
|
-
|
|
5395
|
+
const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
|
|
5396
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
|
|
4285
5397
|
let deniedReasons;
|
|
4286
5398
|
if (this.featureFlags.documentDecisions) try {
|
|
4287
5399
|
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
@@ -4333,6 +5445,71 @@ var SimpleJobExecutor = class {
|
|
|
4333
5445
|
duration: Date.now() - startTime
|
|
4334
5446
|
};
|
|
4335
5447
|
}
|
|
5448
|
+
/** The conflicting window a load opens, and which of its operations are new. */
|
|
5449
|
+
async selectLoadWrites(job, operations, stores, signal) {
|
|
5450
|
+
const scope = job.scope;
|
|
5451
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
5452
|
+
let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
|
|
5453
|
+
for (const operation of operations) {
|
|
5454
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
5455
|
+
const ts = operation.timestampUtcMs || "";
|
|
5456
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
5457
|
+
}
|
|
5458
|
+
let conflictingOps;
|
|
5459
|
+
try {
|
|
5460
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
5461
|
+
} catch {
|
|
5462
|
+
conflictingOps = [];
|
|
5463
|
+
}
|
|
5464
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
5465
|
+
if (conflictingOps.length > 0) {
|
|
5466
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
5467
|
+
try {
|
|
5468
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
5469
|
+
} catch {
|
|
5470
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
const incomingActionIds = new Set(operations.map((op) => op.action.id));
|
|
5474
|
+
const supersededBy = (op) => {
|
|
5475
|
+
let nearest;
|
|
5476
|
+
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;
|
|
5477
|
+
return nearest;
|
|
5478
|
+
};
|
|
5479
|
+
const undoneActionIds = /* @__PURE__ */ new Set();
|
|
5480
|
+
let predecessorBound = minIncomingIndex;
|
|
5481
|
+
const rowsByActionId = /* @__PURE__ */ new Map();
|
|
5482
|
+
for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
|
|
5483
|
+
const liveOps = /* @__PURE__ */ new Set();
|
|
5484
|
+
for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
|
|
5485
|
+
for (const [actionId, rows] of rowsByActionId) {
|
|
5486
|
+
if (rows.some((row) => liveOps.has(row))) continue;
|
|
5487
|
+
if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
|
|
5488
|
+
else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
|
|
5489
|
+
}
|
|
5490
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
5491
|
+
if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
|
|
5492
|
+
return liveOps.has(op);
|
|
5493
|
+
});
|
|
5494
|
+
let rewoundFrom = Number.POSITIVE_INFINITY;
|
|
5495
|
+
for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
|
|
5496
|
+
const reappended = new Set(nonSupersededOps.map((op) => op.index));
|
|
5497
|
+
for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
|
|
5498
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
5499
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
5500
|
+
const incomingOpsToApply = operations.filter((op) => {
|
|
5501
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
5502
|
+
if (undoneActionIds.has(op.action.id)) return false;
|
|
5503
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
5504
|
+
seenIncomingActionIds.add(op.action.id);
|
|
5505
|
+
return true;
|
|
5506
|
+
});
|
|
5507
|
+
return {
|
|
5508
|
+
nonSupersededOps,
|
|
5509
|
+
allOpsFromMinConflictingIndex,
|
|
5510
|
+
incomingOpsToApply
|
|
5511
|
+
};
|
|
5512
|
+
}
|
|
4336
5513
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
4337
5514
|
if (!result.success) return result;
|
|
4338
5515
|
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
@@ -4479,6 +5656,7 @@ var DocumentModelRegistry = class {
|
|
|
4479
5656
|
//#endregion
|
|
4480
5657
|
//#region src/storage/kysely/keyframe-store.ts
|
|
4481
5658
|
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
5659
|
+
db;
|
|
4482
5660
|
trx;
|
|
4483
5661
|
constructor(db) {
|
|
4484
5662
|
this.db = db;
|
|
@@ -4563,6 +5741,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
4563
5741
|
//#endregion
|
|
4564
5742
|
//#region src/storage/txn.ts
|
|
4565
5743
|
var AtomicTransaction = class {
|
|
5744
|
+
documentId;
|
|
5745
|
+
documentType;
|
|
5746
|
+
scope;
|
|
5747
|
+
branch;
|
|
5748
|
+
baseRevision;
|
|
4566
5749
|
operations = [];
|
|
4567
5750
|
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
4568
5751
|
this.documentId = documentId;
|
|
@@ -4596,6 +5779,11 @@ var AtomicTransaction = class {
|
|
|
4596
5779
|
//#endregion
|
|
4597
5780
|
//#region src/storage/kysely/store.ts
|
|
4598
5781
|
var _UniqueConstraintContext = class extends Error {
|
|
5782
|
+
documentId;
|
|
5783
|
+
scope;
|
|
5784
|
+
branch;
|
|
5785
|
+
revision;
|
|
5786
|
+
stagedOps;
|
|
4599
5787
|
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
4600
5788
|
super("unique constraint");
|
|
4601
5789
|
this.documentId = documentId;
|
|
@@ -4607,6 +5795,7 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
4607
5795
|
}
|
|
4608
5796
|
};
|
|
4609
5797
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
5798
|
+
db;
|
|
4610
5799
|
trx;
|
|
4611
5800
|
constructor(db) {
|
|
4612
5801
|
this.db = db;
|
|
@@ -4706,12 +5895,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4706
5895
|
* otherwise read a snapshot taken before the locks were held.
|
|
4707
5896
|
*/
|
|
4708
5897
|
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
4709
|
-
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
5898
|
+
const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
|
|
4710
5899
|
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
5900
|
+
const sortedKeys = sql.join([...keys].sort());
|
|
4711
5901
|
await sql`
|
|
4712
5902
|
with ordered as materialized (
|
|
4713
5903
|
select key
|
|
4714
|
-
from unnest(array[${
|
|
5904
|
+
from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
|
|
4715
5905
|
order by ord
|
|
4716
5906
|
)
|
|
4717
5907
|
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
@@ -4788,6 +5978,14 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4788
5978
|
}
|
|
4789
5979
|
return false;
|
|
4790
5980
|
}
|
|
5981
|
+
/**
|
|
5982
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
5983
|
+
* last row returned), not the last row's own index. This keeps "0" an
|
|
5984
|
+
* unambiguous start-of-stream sentinel even when a page ends at index 0
|
|
5985
|
+
* (e.g. `limit: 1` on a fresh stream), which would otherwise make
|
|
5986
|
+
* `nextCursor` equal the start cursor and loop a caller that walks pages
|
|
5987
|
+
* forever.
|
|
5988
|
+
*/
|
|
4791
5989
|
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
4792
5990
|
throwIfAborted(signal);
|
|
4793
5991
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
@@ -4802,10 +6000,10 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4802
6000
|
}
|
|
4803
6001
|
if (paging) {
|
|
4804
6002
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4805
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6003
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4806
6004
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4807
6005
|
}
|
|
4808
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
6006
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
4809
6007
|
cursor,
|
|
4810
6008
|
limit
|
|
4811
6009
|
}, signal));
|
|
@@ -4823,26 +6021,28 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4823
6021
|
limit
|
|
4824
6022
|
}, signal));
|
|
4825
6023
|
}
|
|
6024
|
+
/**
|
|
6025
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
6026
|
+
* last row returned), not the last row's own index, for the same reason as
|
|
6027
|
+
* `getSince`: a page ending at index 0 must not produce a cursor that is
|
|
6028
|
+
* indistinguishable from the start-of-stream sentinel.
|
|
6029
|
+
*/
|
|
4826
6030
|
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
4827
6031
|
throwIfAborted(signal);
|
|
4828
6032
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
|
|
4829
6033
|
if (paging) {
|
|
4830
6034
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4831
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6035
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4832
6036
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4833
6037
|
}
|
|
4834
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
6038
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
4835
6039
|
cursor,
|
|
4836
6040
|
limit
|
|
4837
6041
|
}, signal));
|
|
4838
6042
|
}
|
|
4839
6043
|
async getRevisions(documentId, branch, signal) {
|
|
4840
6044
|
throwIfAborted(signal);
|
|
4841
|
-
const scopeRevisions = await this.queryExecutor.selectFrom("Operation as
|
|
4842
|
-
"o1.scope",
|
|
4843
|
-
"o1.index",
|
|
4844
|
-
"o1.timestampUtcMs"
|
|
4845
|
-
]).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();
|
|
6045
|
+
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();
|
|
4846
6046
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
|
|
4847
6047
|
const revision = {};
|
|
4848
6048
|
for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
|
|
@@ -4855,6 +6055,20 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4855
6055
|
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();
|
|
4856
6056
|
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
4857
6057
|
}
|
|
6058
|
+
async findOperationIds(documentId, scope, branch, opIds, signal) {
|
|
6059
|
+
throwIfAborted(signal);
|
|
6060
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Set();
|
|
6061
|
+
const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
|
|
6062
|
+
return new Set(rows.map((row) => row.opId));
|
|
6063
|
+
}
|
|
6064
|
+
async getOperationsByIds(documentId, scope, branch, opIds, signal) {
|
|
6065
|
+
throwIfAborted(signal);
|
|
6066
|
+
if (opIds.length === 0) return [];
|
|
6067
|
+
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();
|
|
6068
|
+
const latest = /* @__PURE__ */ new Map();
|
|
6069
|
+
for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
|
|
6070
|
+
return [...latest.values()];
|
|
6071
|
+
}
|
|
4858
6072
|
rowToOperation(row) {
|
|
4859
6073
|
return {
|
|
4860
6074
|
index: row.index,
|
|
@@ -4948,8 +6162,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
4948
6162
|
}
|
|
4949
6163
|
//#endregion
|
|
4950
6164
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
4951
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4952
|
-
async function up$
|
|
6165
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
|
|
6166
|
+
async function up$22(db) {
|
|
4953
6167
|
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", [
|
|
4954
6168
|
"documentId",
|
|
4955
6169
|
"scope",
|
|
@@ -4974,8 +6188,8 @@ async function up$18(db) {
|
|
|
4974
6188
|
}
|
|
4975
6189
|
//#endregion
|
|
4976
6190
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
4977
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4978
|
-
async function up$
|
|
6191
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
|
|
6192
|
+
async function up$21(db) {
|
|
4979
6193
|
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", [
|
|
4980
6194
|
"documentId",
|
|
4981
6195
|
"scope",
|
|
@@ -4991,14 +6205,14 @@ async function up$17(db) {
|
|
|
4991
6205
|
}
|
|
4992
6206
|
//#endregion
|
|
4993
6207
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
4994
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4995
|
-
async function up$
|
|
6208
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
|
|
6209
|
+
async function up$20(db) {
|
|
4996
6210
|
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();
|
|
4997
6211
|
}
|
|
4998
6212
|
//#endregion
|
|
4999
6213
|
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
5000
|
-
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5001
|
-
async function up$
|
|
6214
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
|
|
6215
|
+
async function up$19(db) {
|
|
5002
6216
|
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", [
|
|
5003
6217
|
"sourceId",
|
|
5004
6218
|
"targetId",
|
|
@@ -5010,14 +6224,14 @@ async function up$15(db) {
|
|
|
5010
6224
|
}
|
|
5011
6225
|
//#endregion
|
|
5012
6226
|
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
5013
|
-
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5014
|
-
async function up$
|
|
6227
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
|
|
6228
|
+
async function up$18(db) {
|
|
5015
6229
|
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();
|
|
5016
6230
|
}
|
|
5017
6231
|
//#endregion
|
|
5018
6232
|
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
5019
|
-
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5020
|
-
async function up$
|
|
6233
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
|
|
6234
|
+
async function up$17(db) {
|
|
5021
6235
|
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", [
|
|
5022
6236
|
"documentId",
|
|
5023
6237
|
"scope",
|
|
@@ -5038,8 +6252,8 @@ async function up$13(db) {
|
|
|
5038
6252
|
}
|
|
5039
6253
|
//#endregion
|
|
5040
6254
|
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
5041
|
-
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5042
|
-
async function up$
|
|
6255
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
|
|
6256
|
+
async function up$16(db) {
|
|
5043
6257
|
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", [
|
|
5044
6258
|
"documentId",
|
|
5045
6259
|
"scope",
|
|
@@ -5049,14 +6263,14 @@ async function up$12(db) {
|
|
|
5049
6263
|
}
|
|
5050
6264
|
//#endregion
|
|
5051
6265
|
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
5052
|
-
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5053
|
-
async function up$
|
|
6266
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
6267
|
+
async function up$15(db) {
|
|
5054
6268
|
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();
|
|
5055
6269
|
}
|
|
5056
6270
|
//#endregion
|
|
5057
6271
|
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
5058
|
-
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5059
|
-
async function up$
|
|
6272
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
6273
|
+
async function up$14(db) {
|
|
5060
6274
|
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();
|
|
5061
6275
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
5062
6276
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -5070,8 +6284,8 @@ async function up$10(db) {
|
|
|
5070
6284
|
}
|
|
5071
6285
|
//#endregion
|
|
5072
6286
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
5073
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5074
|
-
async function up$
|
|
6287
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
6288
|
+
async function up$13(db) {
|
|
5075
6289
|
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();
|
|
5076
6290
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
5077
6291
|
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();
|
|
@@ -5079,8 +6293,8 @@ async function up$9(db) {
|
|
|
5079
6293
|
}
|
|
5080
6294
|
//#endregion
|
|
5081
6295
|
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
5082
|
-
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5083
|
-
async function up$
|
|
6296
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
6297
|
+
async function up$12(db) {
|
|
5084
6298
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
5085
6299
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
5086
6300
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -5089,64 +6303,64 @@ async function up$8(db) {
|
|
|
5089
6303
|
}
|
|
5090
6304
|
//#endregion
|
|
5091
6305
|
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
5092
|
-
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5093
|
-
async function up$
|
|
6306
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
|
|
6307
|
+
async function up$11(db) {
|
|
5094
6308
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
5095
6309
|
}
|
|
5096
6310
|
//#endregion
|
|
5097
6311
|
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
5098
|
-
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5099
|
-
async function up$
|
|
6312
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
|
|
6313
|
+
async function up$10(db) {
|
|
5100
6314
|
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();
|
|
5101
6315
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
5102
6316
|
}
|
|
5103
6317
|
//#endregion
|
|
5104
6318
|
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
5105
|
-
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5106
|
-
async function up$
|
|
6319
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
|
|
6320
|
+
async function up$9(db) {
|
|
5107
6321
|
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();
|
|
5108
6322
|
}
|
|
5109
6323
|
//#endregion
|
|
5110
6324
|
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
5111
6325
|
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
5112
|
-
down: () => down$
|
|
5113
|
-
up: () => up$
|
|
6326
|
+
down: () => down$8,
|
|
6327
|
+
up: () => up$8
|
|
5114
6328
|
});
|
|
5115
6329
|
/**
|
|
5116
6330
|
* Records why authorization refused an operation. Separate from `error` so a
|
|
5117
6331
|
* denial is distinguishable from a reducer failure without matching on a
|
|
5118
6332
|
* message. Null for every operation written before decisions were enforced.
|
|
5119
6333
|
*/
|
|
5120
|
-
async function up$
|
|
6334
|
+
async function up$8(db) {
|
|
5121
6335
|
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
5122
6336
|
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
5123
6337
|
}
|
|
5124
|
-
async function down$
|
|
6338
|
+
async function down$8(db) {
|
|
5125
6339
|
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
5126
6340
|
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
5127
6341
|
}
|
|
5128
6342
|
//#endregion
|
|
5129
6343
|
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
5130
6344
|
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
5131
|
-
down: () => down$
|
|
5132
|
-
up: () => up$
|
|
6345
|
+
down: () => down$7,
|
|
6346
|
+
up: () => up$7
|
|
5133
6347
|
});
|
|
5134
6348
|
/**
|
|
5135
6349
|
* The classification a dead letter falls into, stored because it decides whether
|
|
5136
6350
|
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
5137
6351
|
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
5138
6352
|
*/
|
|
5139
|
-
async function up$
|
|
6353
|
+
async function up$7(db) {
|
|
5140
6354
|
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
5141
6355
|
}
|
|
5142
|
-
async function down$
|
|
6356
|
+
async function down$7(db) {
|
|
5143
6357
|
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
5144
6358
|
}
|
|
5145
6359
|
//#endregion
|
|
5146
6360
|
//#region src/storage/migrations/017_create_group_references.ts
|
|
5147
6361
|
var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
5148
|
-
down: () => down$
|
|
5149
|
-
up: () => up$
|
|
6362
|
+
down: () => down$6,
|
|
6363
|
+
up: () => up$6
|
|
5150
6364
|
});
|
|
5151
6365
|
/**
|
|
5152
6366
|
* One row per (document, group) reference ever discovered from an auth
|
|
@@ -5157,18 +6371,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
|
5157
6371
|
* requires (sync), and by groupId for the documents a group change affects
|
|
5158
6372
|
* (re-evaluation).
|
|
5159
6373
|
*/
|
|
5160
|
-
async function up$
|
|
6374
|
+
async function up$6(db) {
|
|
5161
6375
|
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();
|
|
5162
6376
|
await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
|
|
5163
6377
|
}
|
|
5164
|
-
async function down$
|
|
6378
|
+
async function down$6(db) {
|
|
5165
6379
|
await db.schema.dropTable("group_references").execute();
|
|
5166
6380
|
}
|
|
5167
6381
|
//#endregion
|
|
5168
6382
|
//#region src/storage/migrations/018_add_sync_remote_bound_address.ts
|
|
5169
6383
|
var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
5170
|
-
down: () => down$
|
|
5171
|
-
up: () => up$
|
|
6384
|
+
down: () => down$5,
|
|
6385
|
+
up: () => up$5
|
|
5172
6386
|
});
|
|
5173
6387
|
/**
|
|
5174
6388
|
* The address a sync channel is bound to, so a channel created by one subject
|
|
@@ -5179,17 +6393,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
|
5179
6393
|
* until its first authenticated poll adopts it. A default would claim them all
|
|
5180
6394
|
* for one address.
|
|
5181
6395
|
*/
|
|
5182
|
-
async function up$
|
|
6396
|
+
async function up$5(db) {
|
|
5183
6397
|
await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
|
|
5184
6398
|
}
|
|
5185
|
-
async function down$
|
|
6399
|
+
async function down$5(db) {
|
|
5186
6400
|
await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
|
|
5187
6401
|
}
|
|
5188
6402
|
//#endregion
|
|
5189
6403
|
//#region src/storage/migrations/019_require_action_id.ts
|
|
5190
6404
|
var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
5191
|
-
down: () => down,
|
|
5192
|
-
up: () => up
|
|
6405
|
+
down: () => down$4,
|
|
6406
|
+
up: () => up$4
|
|
5193
6407
|
});
|
|
5194
6408
|
/**
|
|
5195
6409
|
* Makes an operation whose action carries no id physically unstorable.
|
|
@@ -5216,7 +6430,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
|
5216
6430
|
* The empty string is rejected alongside null. It derives the same colliding
|
|
5217
6431
|
* operation id as an absent id, so admitting it would leave the hole open.
|
|
5218
6432
|
*/
|
|
5219
|
-
async function up(db) {
|
|
6433
|
+
async function up$4(db) {
|
|
5220
6434
|
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();
|
|
5221
6435
|
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();
|
|
5222
6436
|
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();
|
|
@@ -5228,11 +6442,74 @@ async function up(db) {
|
|
|
5228
6442
|
* their operations are now known by, and reverting them would reintroduce the
|
|
5229
6443
|
* collision the migration removed.
|
|
5230
6444
|
*/
|
|
5231
|
-
async function down(db) {
|
|
6445
|
+
async function down$4(db) {
|
|
5232
6446
|
await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
|
|
5233
6447
|
await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
|
|
5234
6448
|
}
|
|
5235
6449
|
//#endregion
|
|
6450
|
+
//#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
|
|
6451
|
+
var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
|
|
6452
|
+
down: () => down$3,
|
|
6453
|
+
up: () => up$3
|
|
6454
|
+
});
|
|
6455
|
+
/** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
|
|
6456
|
+
async function up$3(db) {
|
|
6457
|
+
await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
6458
|
+
}
|
|
6459
|
+
async function down$3(db) {
|
|
6460
|
+
await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
|
|
6461
|
+
}
|
|
6462
|
+
//#endregion
|
|
6463
|
+
//#region src/storage/migrations/021_add_sync_remote_peer.ts
|
|
6464
|
+
var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
|
|
6465
|
+
down: () => down$2,
|
|
6466
|
+
up: () => up$2
|
|
6467
|
+
});
|
|
6468
|
+
/**
|
|
6469
|
+
* The peer's manifest as canonical JSON, and when it was heard. Both null: not
|
|
6470
|
+
* yet heard. A time with no manifest: a silent peer, which gets the baselines.
|
|
6471
|
+
*/
|
|
6472
|
+
async function up$2(db) {
|
|
6473
|
+
await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
|
|
6474
|
+
}
|
|
6475
|
+
async function down$2(db) {
|
|
6476
|
+
await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
|
|
6477
|
+
}
|
|
6478
|
+
//#endregion
|
|
6479
|
+
//#region src/storage/migrations/022_create_sync_holds.ts
|
|
6480
|
+
var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
|
|
6481
|
+
down: () => down$1,
|
|
6482
|
+
up: () => up$1
|
|
6483
|
+
});
|
|
6484
|
+
/** Documents held back from a remote whose peer cannot run their versions. */
|
|
6485
|
+
async function up$1(db) {
|
|
6486
|
+
await db.schema.createTable("sync_holds").addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("protocol", "text", (col) => col.notNull()).addColumn("version", "integer", (col) => col.notNull()).addColumn("held_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_holds_pkey", [
|
|
6487
|
+
"remote_name",
|
|
6488
|
+
"document_id",
|
|
6489
|
+
"branch"
|
|
6490
|
+
]).execute();
|
|
6491
|
+
}
|
|
6492
|
+
async function down$1(db) {
|
|
6493
|
+
await db.schema.dropTable("sync_holds").execute();
|
|
6494
|
+
}
|
|
6495
|
+
//#endregion
|
|
6496
|
+
//#region src/storage/migrations/023_index_created_protocol_versions.ts
|
|
6497
|
+
var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
|
|
6498
|
+
down: () => down,
|
|
6499
|
+
up: () => up
|
|
6500
|
+
});
|
|
6501
|
+
/**
|
|
6502
|
+
* One entry per document created with protocolVersions, keyed by a hash so an
|
|
6503
|
+
* oversized header cannot make its creation unstorable. Startup reads the
|
|
6504
|
+
* distinct values without visiting every document.
|
|
6505
|
+
*/
|
|
6506
|
+
async function up(db) {
|
|
6507
|
+
await db.schema.createIndex("idx_operation_created_protocol_versions").on("Operation").expression(sql`md5((action->'input'->'protocolVersions')::text)`).where(sql`scope = 'document' and "index" = 0 and (action->'input'->'protocolVersions') is not null`).execute();
|
|
6508
|
+
}
|
|
6509
|
+
async function down(db) {
|
|
6510
|
+
await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
|
|
6511
|
+
}
|
|
6512
|
+
//#endregion
|
|
5236
6513
|
//#region src/storage/migrations/migrator.ts
|
|
5237
6514
|
const REACTOR_SCHEMA = "reactor";
|
|
5238
6515
|
const migrations = {
|
|
@@ -5254,7 +6531,11 @@ const migrations = {
|
|
|
5254
6531
|
"016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
|
|
5255
6532
|
"017_create_group_references": _017_create_group_references_exports,
|
|
5256
6533
|
"018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
|
|
5257
|
-
"019_require_action_id": _019_require_action_id_exports
|
|
6534
|
+
"019_require_action_id": _019_require_action_id_exports,
|
|
6535
|
+
"020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
|
|
6536
|
+
"021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
|
|
6537
|
+
"022_create_sync_holds": _022_create_sync_holds_exports,
|
|
6538
|
+
"023_index_created_protocol_versions": _023_index_created_protocol_versions_exports
|
|
5258
6539
|
};
|
|
5259
6540
|
var ProgrammaticMigrationProvider = class {
|
|
5260
6541
|
getMigrations() {
|
|
@@ -5312,8 +6593,148 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
5312
6593
|
}
|
|
5313
6594
|
//#endregion
|
|
5314
6595
|
//#region src/core/drive-container-types.ts
|
|
5315
|
-
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6596
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
6597
|
+
//#endregion
|
|
6598
|
+
//#region src/executor/worker/error-info.ts
|
|
6599
|
+
const MAX_STACK_LENGTH = 8192;
|
|
6600
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
6601
|
+
function toErrorInfo(err, depth = 0) {
|
|
6602
|
+
if (depth > MAX_CAUSE_DEPTH) return {
|
|
6603
|
+
name: "Error",
|
|
6604
|
+
message: "cause chain truncated"
|
|
6605
|
+
};
|
|
6606
|
+
if (err instanceof Error) {
|
|
6607
|
+
const info = {
|
|
6608
|
+
name: err.name || "Error",
|
|
6609
|
+
message: err.message
|
|
6610
|
+
};
|
|
6611
|
+
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
6612
|
+
if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
|
|
6613
|
+
return info;
|
|
6614
|
+
}
|
|
6615
|
+
if (typeof err === "string") return {
|
|
6616
|
+
name: "Error",
|
|
6617
|
+
message: err
|
|
6618
|
+
};
|
|
6619
|
+
if (err && typeof err === "object") {
|
|
6620
|
+
const record = err;
|
|
6621
|
+
return {
|
|
6622
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
6623
|
+
message: typeof record.message === "string" ? record.message : safeStringify(err)
|
|
6624
|
+
};
|
|
6625
|
+
}
|
|
6626
|
+
return {
|
|
6627
|
+
name: "Error",
|
|
6628
|
+
message: safeStringify(err)
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
function safeStringify(value) {
|
|
6632
|
+
if (value === null || value === void 0) return String(value);
|
|
6633
|
+
if (typeof value === "string") return value;
|
|
6634
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
6635
|
+
try {
|
|
6636
|
+
return JSON.stringify(value);
|
|
6637
|
+
} catch {
|
|
6638
|
+
return "[unserializable]";
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
function fromErrorInfo(info) {
|
|
6642
|
+
const err = new Error(info.message);
|
|
6643
|
+
Object.defineProperty(err, "name", {
|
|
6644
|
+
value: info.name,
|
|
6645
|
+
configurable: true,
|
|
6646
|
+
writable: true
|
|
6647
|
+
});
|
|
6648
|
+
if (info.stack !== void 0) err.stack = info.stack;
|
|
6649
|
+
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
6650
|
+
value: fromErrorInfo(info.cause),
|
|
6651
|
+
configurable: true,
|
|
6652
|
+
writable: true
|
|
6653
|
+
});
|
|
6654
|
+
return err;
|
|
6655
|
+
}
|
|
6656
|
+
//#endregion
|
|
6657
|
+
//#region src/executor/worker/sanitize.ts
|
|
6658
|
+
const MAX_DEPTH = 8;
|
|
6659
|
+
function errorToInfo(err) {
|
|
6660
|
+
return toErrorInfo(err);
|
|
6661
|
+
}
|
|
6662
|
+
function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
6663
|
+
if (depth > MAX_DEPTH) return "[Truncated]";
|
|
6664
|
+
if (value === null || value === void 0) return null;
|
|
6665
|
+
if (typeof value === "boolean") return value;
|
|
6666
|
+
if (typeof value === "number") return value;
|
|
6667
|
+
if (typeof value === "string") return value;
|
|
6668
|
+
if (typeof value === "bigint") return value.toString();
|
|
6669
|
+
if (typeof value === "function" || typeof value === "symbol") return null;
|
|
6670
|
+
if (value instanceof Error) return toErrorInfo(value);
|
|
6671
|
+
if (value instanceof Date) return value.toISOString();
|
|
6672
|
+
if (value instanceof Map) {
|
|
6673
|
+
const obj = {};
|
|
6674
|
+
for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
|
|
6675
|
+
return obj;
|
|
6676
|
+
}
|
|
6677
|
+
if (value instanceof Set) {
|
|
6678
|
+
const arr = [];
|
|
6679
|
+
for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
|
|
6680
|
+
return arr;
|
|
6681
|
+
}
|
|
6682
|
+
if (Array.isArray(value)) {
|
|
6683
|
+
if (seen.has(value)) return "[Circular]";
|
|
6684
|
+
seen.add(value);
|
|
6685
|
+
const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
|
|
6686
|
+
seen.delete(value);
|
|
6687
|
+
return result;
|
|
6688
|
+
}
|
|
6689
|
+
if (typeof value === "object") {
|
|
6690
|
+
const proto = Object.getPrototypeOf(value);
|
|
6691
|
+
if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
|
|
6692
|
+
if (seen.has(value)) return "[Circular]";
|
|
6693
|
+
seen.add(value);
|
|
6694
|
+
const result = {};
|
|
6695
|
+
for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
|
|
6696
|
+
seen.delete(value);
|
|
6697
|
+
return result;
|
|
6698
|
+
}
|
|
6699
|
+
return null;
|
|
6700
|
+
}
|
|
6701
|
+
//#endregion
|
|
6702
|
+
//#region src/executor/worker/forwarding-logger.ts
|
|
6703
|
+
function createForwardingLogger(post, level = "info", tags = []) {
|
|
6704
|
+
function safePost(msg) {
|
|
6705
|
+
try {
|
|
6706
|
+
post(msg);
|
|
6707
|
+
} catch {
|
|
6708
|
+
try {
|
|
6709
|
+
process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
|
|
6710
|
+
} catch {}
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
function emit(wireLevel, message, args) {
|
|
6714
|
+
const sanitizedArgs = args.map((a) => sanitizeArg(a));
|
|
6715
|
+
safePost({
|
|
6716
|
+
type: "log",
|
|
6717
|
+
level: wireLevel,
|
|
6718
|
+
message,
|
|
6719
|
+
args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
|
|
6720
|
+
timestamp: Date.now()
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
return {
|
|
6724
|
+
level,
|
|
6725
|
+
verbose: (message, ...args) => emit("debug", message, args),
|
|
6726
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
6727
|
+
info: (message, ...args) => emit("info", message, args),
|
|
6728
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
6729
|
+
error: (message, ...args) => emit("error", message, args),
|
|
6730
|
+
errorHandler: ((...data) => {
|
|
6731
|
+
const first = data[0];
|
|
6732
|
+
emit("error", typeof first === "string" ? first : "", data.slice(1));
|
|
6733
|
+
}),
|
|
6734
|
+
child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
|
|
6735
|
+
};
|
|
6736
|
+
}
|
|
5316
6737
|
//#endregion
|
|
5317
|
-
export {
|
|
6738
|
+
export { OptimisticLockError as $, DEFAULT_DEFERRED_JOB_TTL_MS as A, summarizeSubmittedActions as B, SettledWatermark as C, snapshotFunctionsFor as D, parseSnapshot as E, ModuleNotFoundError as F, authDecisionModel as G, decideAtHead as H, GATED_DOCUMENT_ACTIONS as I, APPEND_CONDITION_FAILED_PREFIX as J, buildDecisionModel as K, createConsistencyToken as L, DuplicateManifestError as M, DuplicateModuleError as N, DocumentMetaCache as O, InvalidModuleError as P, DuplicateOperationError as Q, createEmptyConsistencyToken as R, ProbeSettler as S, describeWaitingSessions as T, selectDecisionModel as U, targetDocumentId as V, documentDecisionModel as W, DocumentAlreadyExistsError as X, AppendConditionFailedError as Y, DocumentExistence as Z, KyselyExecutionScope as _, throwIfAborted as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, DocumentNotFoundError as at, KyselyWriteCache as b, runMigrations as c, InvalidSignatureError as ct, KyselyOperationStore as d, UnsupportedStoredProtocolError as dt, RevisionMismatchError as et, KyselyKeyframeStore as f, UpgradePreconditionFailedError as ft, PassthroughSigner as g, parsePagingOptions as gt, verifyActionSignature as h, matchesScope as ht, fromErrorInfo as i, DocumentDeletedError as it, JobExecutorEventTypes as j, CollectionMembershipCache as k, createForwardingPoolInstrumentation as l, RelationshipNotFoundError as lt, SimpleJobExecutor as m, ReactorEventTypes as mt, errorToInfo as n, AuthTimestampNotMonotonicError as nt, REACTOR_SCHEMA as o, ExcessiveReshuffleError as ot, DocumentModelRegistry as p, EventBusAggregateError as pt, DriveCollectionId as q, sanitizeArg as r, AuthorizationDeniedError as rt, getMigrationStatus as s, InvalidOperationTimestampError as st, createForwardingLogger as t, AuthEnforcementDisabledError as tt, instrumentPgPool as u, UnsupportedProtocolVersionError as ut, resolveFeatureFlags as v, yieldToMain as vt, createKyselyWatermarkProbe as w, KyselyOperationIndex as x, EventBus as y, __exportAll as yt, submittedActionIds as z };
|
|
5318
6739
|
|
|
5319
|
-
//# sourceMappingURL=
|
|
6740
|
+
//# sourceMappingURL=forwarding-logger-BkwlvD9W.js.map
|