@powerhousedao/reactor 6.2.2-dev.4 → 6.2.2-dev.41
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--nhFRF47.js → build-worker-executor-CAEZD_yJ.js} +2 -2
- package/dist/{build-worker-executor--nhFRF47.js.map → build-worker-executor-CAEZD_yJ.js.map} +1 -1
- package/dist/{document-indexer-FGJmRAdX.js → document-indexer-C0GB0b8Q.js} +20 -7
- package/dist/document-indexer-C0GB0b8Q.js.map +1 -0
- package/dist/{drive-container-types-DpJp2AmE.js → drive-container-types-BJCKXJwH.js} +1498 -206
- package/dist/drive-container-types-BJCKXJwH.js.map +1 -0
- package/dist/entry.js +3 -2
- package/dist/entry.js.map +1 -1
- package/dist/index.d.ts +2045 -1637
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +319 -66
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +4 -4
- package/dist/projection-entry.js.map +1 -1
- package/dist/{worker-handle-B1w03nRA.js → worker-handle-CrERzl8s.js} +3 -2
- package/dist/worker-handle-CrERzl8s.js.map +1 -0
- package/dist/{worker-DBJOv8Gp.js → worker-jEJW6_j7.js} +2 -2
- package/dist/{worker-DBJOv8Gp.js.map → worker-jEJW6_j7.js.map} +1 -1
- package/package.json +5 -4
- package/dist/document-indexer-FGJmRAdX.js.map +0 -1
- package/dist/drive-container-types-DpJp2AmE.js.map +0 -1
- package/dist/worker-handle-B1w03nRA.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
|
|
2
|
-
import { DowngradeNotSupportedError, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, createPresignedHeader, defaultBaseState, deriveOperationId, isUndoRedo } from "@powerhousedao/shared/document-model";
|
|
2
|
+
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, hashDocumentStateForScope, isDenied, isUndoRedo, sortOperations } from "@powerhousedao/shared/document-model";
|
|
3
3
|
import { v4 } from "uuid";
|
|
4
4
|
import { Migrator, sql } from "kysely";
|
|
5
5
|
//#region \0rolldown/runtime.js
|
|
@@ -72,6 +72,102 @@ var DocumentDeletedError = class DocumentDeletedError extends Error {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
|
+
* Error thrown when the auth policy denies an action at the executor gate.
|
|
76
|
+
*/
|
|
77
|
+
var AuthorizationDeniedError = class AuthorizationDeniedError extends Error {
|
|
78
|
+
documentId;
|
|
79
|
+
scope;
|
|
80
|
+
operation;
|
|
81
|
+
subject;
|
|
82
|
+
constructor(documentId, scope, operation, subject) {
|
|
83
|
+
super(`Authorization denied: ${subject ?? "anonymous"} may not execute ${operation} in scope "${scope}" of document ${documentId}`);
|
|
84
|
+
this.name = "AuthorizationDeniedError";
|
|
85
|
+
this.documentId = documentId;
|
|
86
|
+
this.scope = scope;
|
|
87
|
+
this.operation = operation;
|
|
88
|
+
this.subject = subject;
|
|
89
|
+
Error.captureStackTrace(this, AuthorizationDeniedError);
|
|
90
|
+
}
|
|
91
|
+
static isError(error) {
|
|
92
|
+
return Error.isError(error) && error.name === "AuthorizationDeniedError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* An auth operation did not strictly exceed the newest timestamp in its stream.
|
|
97
|
+
*
|
|
98
|
+
* Terminal and asymmetric by design: no ordering rule can reconcile two replicas
|
|
99
|
+
* that each accepted an auth operation offline, because either order hands one
|
|
100
|
+
* authority the other never granted, so the replica ahead holds the arrival.
|
|
101
|
+
*/
|
|
102
|
+
var AuthTimestampNotMonotonicError = class AuthTimestampNotMonotonicError extends Error {
|
|
103
|
+
documentId;
|
|
104
|
+
branch;
|
|
105
|
+
timestampUtcMs;
|
|
106
|
+
newestTimestampUtcMs;
|
|
107
|
+
constructor(documentId, branch, timestampUtcMs, newestTimestampUtcMs) {
|
|
108
|
+
super(`Auth timestamp not monotonic: ${timestampUtcMs} does not exceed ${newestTimestampUtcMs} in the auth stream of document ${documentId} on branch ${branch}`);
|
|
109
|
+
this.name = "AuthTimestampNotMonotonicError";
|
|
110
|
+
this.documentId = documentId;
|
|
111
|
+
this.branch = branch;
|
|
112
|
+
this.timestampUtcMs = timestampUtcMs;
|
|
113
|
+
this.newestTimestampUtcMs = newestTimestampUtcMs;
|
|
114
|
+
Error.captureStackTrace(this, AuthTimestampNotMonotonicError);
|
|
115
|
+
}
|
|
116
|
+
static isError(error) {
|
|
117
|
+
return Error.isError(error) && error.name === "AuthTimestampNotMonotonicError";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* An operation or action carried a timestamp that is not an ISO-8601 UTC
|
|
122
|
+
* instant.
|
|
123
|
+
*
|
|
124
|
+
* Terminal rather than retryable: the value does not change between attempts,
|
|
125
|
+
* so a retry re-runs the whole job to fail identically. Quarantining, unlike a
|
|
126
|
+
* held auth operation — this is malformed data rather than two replicas
|
|
127
|
+
* disagreeing, and nothing further from that source should be trusted until it
|
|
128
|
+
* is looked at.
|
|
129
|
+
*/
|
|
130
|
+
var InvalidOperationTimestampError = class InvalidOperationTimestampError extends Error {
|
|
131
|
+
documentId;
|
|
132
|
+
scope;
|
|
133
|
+
timestampUtcMs;
|
|
134
|
+
constructor(documentId, scope, timestampUtcMs, context) {
|
|
135
|
+
super(`Invalid timestamp "${timestampUtcMs}" on ${context} in scope "${scope}" of document ${documentId}`);
|
|
136
|
+
this.name = "InvalidOperationTimestampError";
|
|
137
|
+
this.documentId = documentId;
|
|
138
|
+
this.scope = scope;
|
|
139
|
+
this.timestampUtcMs = timestampUtcMs;
|
|
140
|
+
Error.captureStackTrace(this, InvalidOperationTimestampError);
|
|
141
|
+
}
|
|
142
|
+
static isError(error) {
|
|
143
|
+
return Error.isError(error) && error.name === "InvalidOperationTimestampError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* A load would move more operations than the bound allows, indicating a real
|
|
148
|
+
* divergence between local and incoming history. Counts only first-time moves,
|
|
149
|
+
* so a re-evaluation pass's re-appends do not make busy documents
|
|
150
|
+
* revocation-proof. Terminal: the condition is deterministic.
|
|
151
|
+
*/
|
|
152
|
+
var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
|
|
153
|
+
documentId;
|
|
154
|
+
scope;
|
|
155
|
+
count;
|
|
156
|
+
threshold;
|
|
157
|
+
constructor(documentId, scope, count, threshold) {
|
|
158
|
+
super(`Excessive reshuffle detected: ${count} operations in scope "${scope}" of document ${documentId} exceeds the threshold of ${threshold}. This indicates a significant divergence between local and incoming operations.`);
|
|
159
|
+
this.name = "ExcessiveReshuffleError";
|
|
160
|
+
this.documentId = documentId;
|
|
161
|
+
this.scope = scope;
|
|
162
|
+
this.count = count;
|
|
163
|
+
this.threshold = threshold;
|
|
164
|
+
Error.captureStackTrace(this, ExcessiveReshuffleError);
|
|
165
|
+
}
|
|
166
|
+
static isError(error) {
|
|
167
|
+
return Error.isError(error) && error.name === "ExcessiveReshuffleError";
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
75
171
|
* Error thrown when an operation has an invalid signature.
|
|
76
172
|
*/
|
|
77
173
|
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
@@ -181,6 +277,57 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
181
277
|
}
|
|
182
278
|
};
|
|
183
279
|
//#endregion
|
|
280
|
+
//#region src/storage/interfaces.ts
|
|
281
|
+
/**
|
|
282
|
+
* Thrown when an operation with the same identity already exists in the store.
|
|
283
|
+
*/
|
|
284
|
+
var DuplicateOperationError = class extends Error {
|
|
285
|
+
constructor(description) {
|
|
286
|
+
super(`Duplicate operation: ${description}`);
|
|
287
|
+
this.name = "DuplicateOperationError";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Thrown when a concurrent write conflict is detected during an atomic apply.
|
|
292
|
+
*/
|
|
293
|
+
var OptimisticLockError = class extends Error {
|
|
294
|
+
constructor(message) {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "OptimisticLockError";
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Thrown when the caller-provided revision does not match the current
|
|
301
|
+
* stored revision, indicating a stale read.
|
|
302
|
+
*/
|
|
303
|
+
var RevisionMismatchError = class extends Error {
|
|
304
|
+
constructor(expected, actual) {
|
|
305
|
+
super(`Revision mismatch: expected ${expected}, got ${actual}`);
|
|
306
|
+
this.name = "RevisionMismatchError";
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
/** Error history keeps messages, not classes, so failures match by prefix. */
|
|
310
|
+
const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
311
|
+
/**
|
|
312
|
+
* A read-set stream grew before the append committed. A concurrency
|
|
313
|
+
* conflict, not a fault: the caller retries against the new stream heads.
|
|
314
|
+
*/
|
|
315
|
+
var AppendConditionFailedError = class extends Error {
|
|
316
|
+
constructor(condition) {
|
|
317
|
+
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
318
|
+
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
319
|
+
this.condition = condition;
|
|
320
|
+
this.name = "AppendConditionFailedError";
|
|
321
|
+
}
|
|
322
|
+
static isError(error) {
|
|
323
|
+
return Error.isError(error) && error.name === "AppendConditionFailedError";
|
|
324
|
+
}
|
|
325
|
+
/** True when a recorded error message is an append-condition failure. */
|
|
326
|
+
static isFailureMessage(message) {
|
|
327
|
+
return message.startsWith(APPEND_CONDITION_FAILED_PREFIX);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
//#endregion
|
|
184
331
|
//#region src/cache/collection-membership-cache.ts
|
|
185
332
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
186
333
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -216,6 +363,34 @@ var CollectionMembershipCache = class CollectionMembershipCache {
|
|
|
216
363
|
};
|
|
217
364
|
//#endregion
|
|
218
365
|
//#region src/executor/util.ts
|
|
366
|
+
/** Actions the reactor reduces itself, onto the document scope. */
|
|
367
|
+
const DOCUMENT_SCOPE_ACTIONS = new Set([
|
|
368
|
+
"CREATE_DOCUMENT",
|
|
369
|
+
"DELETE_DOCUMENT",
|
|
370
|
+
"UPGRADE_DOCUMENT",
|
|
371
|
+
"ADD_RELATIONSHIP",
|
|
372
|
+
"REMOVE_RELATIONSHIP",
|
|
373
|
+
"UPDATE_RELATIONSHIP"
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
|
|
377
|
+
* so building a decision model would throw and defer the job forever.
|
|
378
|
+
*/
|
|
379
|
+
const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
|
|
380
|
+
/**
|
|
381
|
+
* The document a document-scope action writes to, which is not always the job's
|
|
382
|
+
* own document: delete and upgrade name it in `input.documentId`, and the
|
|
383
|
+
* relationship actions in `input.sourceId`. `execute` only checks that a batch
|
|
384
|
+
* shares one scope, so a caller can submit an action whose target is a document
|
|
385
|
+
* other than the one the job is keyed by. The policy gate has to follow the
|
|
386
|
+
* action rather than the job, or it decides against a policy the caller may
|
|
387
|
+
* control instead of the one guarding the write.
|
|
388
|
+
*/
|
|
389
|
+
function targetDocumentId(action, fallback) {
|
|
390
|
+
const input = action.input;
|
|
391
|
+
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;
|
|
392
|
+
return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
|
|
393
|
+
}
|
|
219
394
|
/**
|
|
220
395
|
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
221
396
|
* Reconstructs the document header and initializes the base state.
|
|
@@ -353,6 +528,24 @@ function buildErrorResult(job, error, startTime) {
|
|
|
353
528
|
duration: Date.now() - startTime
|
|
354
529
|
};
|
|
355
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* The error a refusal surfaces as. Both classes are already terminal in the job
|
|
533
|
+
* result handler, so a refusal never burns a retry.
|
|
534
|
+
*/
|
|
535
|
+
function refusalError(reason, documentId, deletedAtUtcIso, action) {
|
|
536
|
+
if (reason === DOCUMENT_DELETED_REASON) return new DocumentDeletedError(documentId, deletedAtUtcIso);
|
|
537
|
+
return new AuthorizationDeniedError(documentId, action.scope, action.type, action.context?.signer?.user.address);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Whether this operation is part of the document's creation. The create and the
|
|
541
|
+
* upgrade from version zero hold the first two indexes for the life of the
|
|
542
|
+
* document, so a reshuffle has to leave them where they are.
|
|
543
|
+
*/
|
|
544
|
+
function isGenesisOperation(operation) {
|
|
545
|
+
if (operation.action.type === "CREATE_DOCUMENT") return true;
|
|
546
|
+
if (operation.action.type !== "UPGRADE_DOCUMENT") return false;
|
|
547
|
+
return operation.action.input.fromVersion === 0;
|
|
548
|
+
}
|
|
356
549
|
//#endregion
|
|
357
550
|
//#region src/cache/lru/lru-tracker.ts
|
|
358
551
|
var LRUNode = class {
|
|
@@ -634,6 +827,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
634
827
|
skip: op.skip,
|
|
635
828
|
hash: op.hash,
|
|
636
829
|
action: op.action,
|
|
830
|
+
deniedReason: op.deniedReason ?? null,
|
|
637
831
|
sourceRemote: op.sourceRemote
|
|
638
832
|
}));
|
|
639
833
|
operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
|
|
@@ -765,6 +959,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
765
959
|
hash: row.hash,
|
|
766
960
|
skip: row.skip,
|
|
767
961
|
action: row.action,
|
|
962
|
+
deniedReason: row.deniedReason ?? void 0,
|
|
768
963
|
id: row.opId
|
|
769
964
|
},
|
|
770
965
|
context: {
|
|
@@ -788,6 +983,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
788
983
|
hash: row.hash,
|
|
789
984
|
skip: row.skip,
|
|
790
985
|
action: row.action,
|
|
986
|
+
deniedReason: row.deniedReason ?? void 0,
|
|
791
987
|
id: row.opId,
|
|
792
988
|
sourceRemote: row.sourceRemote
|
|
793
989
|
};
|
|
@@ -873,11 +1069,55 @@ var RingBuffer = class {
|
|
|
873
1069
|
}
|
|
874
1070
|
};
|
|
875
1071
|
//#endregion
|
|
1072
|
+
//#region src/cache/write-cache-types.ts
|
|
1073
|
+
/**
|
|
1074
|
+
* Where a snapshot sits in its stream.
|
|
1075
|
+
*
|
|
1076
|
+
* - `Head`: the newest revision of the stream when it was stored. Only these
|
|
1077
|
+
* can answer a read that asks for the head.
|
|
1078
|
+
* - `Historical`: state at an earlier revision. Usable as a starting point to
|
|
1079
|
+
* replay forward from, and as an answer to a read for that same revision.
|
|
1080
|
+
*/
|
|
1081
|
+
let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
|
|
1082
|
+
SnapshotPosition["Head"] = "head";
|
|
1083
|
+
SnapshotPosition["Historical"] = "historical";
|
|
1084
|
+
return SnapshotPosition;
|
|
1085
|
+
}({});
|
|
1086
|
+
//#endregion
|
|
876
1087
|
//#region src/cache/kysely-write-cache.ts
|
|
1088
|
+
/**
|
|
1089
|
+
* The last operation index a keyframe's document reflects for the scope. A
|
|
1090
|
+
* keyframe only exists for a scope that has operations, so a missing entry
|
|
1091
|
+
* means the stored row is corrupt.
|
|
1092
|
+
*/
|
|
1093
|
+
function keyframeRevision(keyframe, documentId, scope) {
|
|
1094
|
+
const nextIndex = keyframe.document.header.revision[scope];
|
|
1095
|
+
if (typeof nextIndex !== "number") throw new Error(`Corrupt keyframe for document ${documentId} at revision ${keyframe.revision}: header carries no ${scope} revision`);
|
|
1096
|
+
return nextIndex - 1;
|
|
1097
|
+
}
|
|
877
1098
|
function extractModuleVersion(doc) {
|
|
878
1099
|
const v = doc.state.document.version;
|
|
879
1100
|
return v === 0 ? void 0 : v;
|
|
880
1101
|
}
|
|
1102
|
+
/** The highest revision held, latest push winning a tie. */
|
|
1103
|
+
function highestRevision(snapshots) {
|
|
1104
|
+
let newest = void 0;
|
|
1105
|
+
for (const snapshot of snapshots) if (!newest || snapshot.revision >= newest.revision) newest = snapshot;
|
|
1106
|
+
return newest;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Copies a document far enough that the caller cannot write through it. Inside
|
|
1110
|
+
* this class, callers only ever replace whole fields on these four, so one
|
|
1111
|
+
* level each is enough.
|
|
1112
|
+
*/
|
|
1113
|
+
function copyDocument(document) {
|
|
1114
|
+
return {
|
|
1115
|
+
...document,
|
|
1116
|
+
header: { ...document.header },
|
|
1117
|
+
state: { ...document.state },
|
|
1118
|
+
operations: { ...document.operations }
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
881
1121
|
/**
|
|
882
1122
|
* In-memory write cache with keyframe persistence for PHDocuments.
|
|
883
1123
|
*
|
|
@@ -956,6 +1196,8 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
956
1196
|
/**
|
|
957
1197
|
* Retrieves document state at a specific revision from cache or rebuilds it.
|
|
958
1198
|
*
|
|
1199
|
+
* Note: this returns a _shallow_ copy of the document.
|
|
1200
|
+
*
|
|
959
1201
|
* Cache hit path: Returns cached snapshot if available (O(1))
|
|
960
1202
|
* Warm miss path: Rebuilds from cached base revision + incremental ops
|
|
961
1203
|
* Cold miss path: Rebuilds from keyframe or from scratch using all operations
|
|
@@ -979,30 +1221,35 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
979
1221
|
if (stream) {
|
|
980
1222
|
const snapshots = stream.ringBuffer.getAll();
|
|
981
1223
|
if (targetRevision === void 0) {
|
|
982
|
-
|
|
983
|
-
|
|
1224
|
+
const newest = highestRevision(snapshots);
|
|
1225
|
+
if (newest?.position === SnapshotPosition.Head) {
|
|
984
1226
|
this.lruTracker.touch(streamKey);
|
|
985
|
-
return newest.document;
|
|
1227
|
+
return copyDocument(newest.document);
|
|
1228
|
+
}
|
|
1229
|
+
if (newest) {
|
|
1230
|
+
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1231
|
+
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, SnapshotPosition.Head);
|
|
1232
|
+
this.lruTracker.touch(streamKey);
|
|
1233
|
+
return document;
|
|
986
1234
|
}
|
|
987
1235
|
} else {
|
|
988
|
-
const exactMatch = snapshots.
|
|
1236
|
+
const exactMatch = snapshots.findLast((s) => s.revision === targetRevision);
|
|
989
1237
|
if (exactMatch) {
|
|
990
1238
|
this.lruTracker.touch(streamKey);
|
|
991
|
-
return exactMatch.document;
|
|
1239
|
+
return copyDocument(exactMatch.document);
|
|
992
1240
|
}
|
|
993
1241
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
994
1242
|
if (newestOlder) {
|
|
995
1243
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
996
|
-
this.
|
|
1244
|
+
this.store(documentId, scope, branch, targetRevision, document, SnapshotPosition.Historical);
|
|
997
1245
|
this.lruTracker.touch(streamKey);
|
|
998
1246
|
return document;
|
|
999
1247
|
}
|
|
1000
1248
|
}
|
|
1001
1249
|
}
|
|
1002
1250
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
this.putState(documentId, scope, branch, revision, document);
|
|
1251
|
+
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1252
|
+
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? SnapshotPosition.Head : SnapshotPosition.Historical);
|
|
1006
1253
|
return document;
|
|
1007
1254
|
}
|
|
1008
1255
|
/**
|
|
@@ -1025,16 +1272,20 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1025
1272
|
* @param document - The document to cache
|
|
1026
1273
|
* @throws {Error} If document serialization fails
|
|
1027
1274
|
*/
|
|
1028
|
-
putState(documentId, scope, branch, revision, document) {
|
|
1275
|
+
putState(documentId, scope, branch, revision, document, position) {
|
|
1276
|
+
this.store(documentId, scope, branch, revision, document, position);
|
|
1277
|
+
}
|
|
1278
|
+
store(documentId, scope, branch, revision, document, position) {
|
|
1029
1279
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1030
1280
|
const stream = this.getOrCreateStream(streamKey);
|
|
1031
1281
|
const snapshot = {
|
|
1032
1282
|
revision,
|
|
1033
1283
|
document: {
|
|
1034
|
-
...document,
|
|
1284
|
+
...copyDocument(document),
|
|
1035
1285
|
operations: Object.fromEntries(Object.entries(document.operations).map(([k, ops]) => [k, ops.length ? [ops.at(-1)] : []])),
|
|
1036
1286
|
clipboard: []
|
|
1037
|
-
}
|
|
1287
|
+
},
|
|
1288
|
+
position
|
|
1038
1289
|
};
|
|
1039
1290
|
stream.ringBuffer.push(snapshot);
|
|
1040
1291
|
if (this.isKeyframeRevision(revision)) this.keyframeStore.putKeyframe(documentId, scope, branch, revision, {
|
|
@@ -1102,58 +1353,75 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1102
1353
|
}
|
|
1103
1354
|
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
1104
1355
|
if (targetRevision === Number.MAX_SAFE_INTEGER || targetRevision <= 0) return;
|
|
1105
|
-
|
|
1356
|
+
const keyframe = await this.keyframeStore.findNearestKeyframe(documentId, scope, branch, targetRevision, signal);
|
|
1357
|
+
if (!keyframe) return;
|
|
1358
|
+
return {
|
|
1359
|
+
revision: Math.min(keyframeRevision(keyframe, documentId, scope), keyframe.revision),
|
|
1360
|
+
document: keyframe.document
|
|
1361
|
+
};
|
|
1106
1362
|
}
|
|
1107
1363
|
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
1108
1364
|
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1109
1365
|
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
1366
|
+
const documentScopeBound = scope === "document" ? targetRevision : void 0;
|
|
1110
1367
|
let document;
|
|
1111
1368
|
let startRevision;
|
|
1112
1369
|
let documentType;
|
|
1113
1370
|
const validatedUpgrades = [];
|
|
1371
|
+
let lastDocumentScopeOperation;
|
|
1114
1372
|
if (keyframe) {
|
|
1115
1373
|
document = keyframe.document;
|
|
1116
1374
|
startRevision = keyframe.revision;
|
|
1117
1375
|
documentType = keyframe.document.header.documentType;
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1376
|
+
const documentScopeResume = scope === "document" ? keyframe.revision : keyframeRevision(keyframe, documentId, "document");
|
|
1377
|
+
const docScopeOpsAfterKeyframe = await this.operationStore.getSince(documentId, "document", branch, documentScopeResume, void 0, void 0, signal);
|
|
1378
|
+
for (const operation of docScopeOpsAfterKeyframe.results) {
|
|
1379
|
+
if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
|
|
1380
|
+
lastDocumentScopeOperation = operation;
|
|
1381
|
+
if (operation.error || isDenied(operation)) continue;
|
|
1382
|
+
if (operation.action.type === "UPGRADE_DOCUMENT") {
|
|
1383
|
+
const upgradeAction = operation.action;
|
|
1384
|
+
const fromVersion = upgradeAction.input.fromVersion;
|
|
1385
|
+
const toVersion = upgradeAction.input.toVersion;
|
|
1386
|
+
if (fromVersion > 0 && fromVersion < toVersion) {
|
|
1387
|
+
let upgradePath;
|
|
1388
|
+
try {
|
|
1389
|
+
upgradePath = this.registry.computeUpgradePath(documentType, fromVersion, toVersion);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
if (upgradeAction.input.initialState !== void 0) upgradePath = void 0;
|
|
1392
|
+
else throw new Error(`Failed to rebuild document ${documentId}: no upgrade manifest for ${documentType} v${fromVersion}→v${toVersion} and no initialState snapshot. ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1393
|
+
}
|
|
1394
|
+
validatedUpgrades.push({
|
|
1395
|
+
fromVersion,
|
|
1396
|
+
toVersion,
|
|
1397
|
+
revision: upgradeAction.input.revision,
|
|
1398
|
+
timestampUtcMs: operation.timestampUtcMs
|
|
1399
|
+
});
|
|
1400
|
+
document = applyUpgradeDocumentAction(document, upgradeAction, upgradePath);
|
|
1130
1401
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
toVersion,
|
|
1134
|
-
revision: upgradeAction.input.revision,
|
|
1135
|
-
timestampUtcMs: operation.timestampUtcMs
|
|
1136
|
-
});
|
|
1137
|
-
document = applyUpgradeDocumentAction(document, upgradeAction, upgradePath);
|
|
1138
|
-
}
|
|
1139
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1402
|
+
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1403
|
+
}
|
|
1140
1404
|
} else {
|
|
1141
1405
|
startRevision = -1;
|
|
1142
1406
|
const createOpResult = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, {
|
|
1143
1407
|
cursor: "0",
|
|
1144
1408
|
limit: 1
|
|
1145
1409
|
}, signal);
|
|
1146
|
-
if (createOpResult.results.length === 0) throw new
|
|
1410
|
+
if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1147
1411
|
const createOp = createOpResult.results[0];
|
|
1148
1412
|
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Failed to rebuild document ${documentId}: first operation in document scope must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
1149
1413
|
const documentCreateAction = createOp.action;
|
|
1150
1414
|
documentType = documentCreateAction.input.model;
|
|
1151
1415
|
if (!documentType) throw new Error(`Failed to rebuild document ${documentId}: CREATE_DOCUMENT action missing model in input`);
|
|
1152
1416
|
document = createDocumentFromAction(documentCreateAction);
|
|
1417
|
+
lastDocumentScopeOperation = createOp;
|
|
1153
1418
|
let docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1154
1419
|
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, 0, void 0, void 0, signal);
|
|
1155
1420
|
for (const operation of docScopeOps.results) {
|
|
1421
|
+
if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
|
|
1422
|
+
lastDocumentScopeOperation = operation;
|
|
1156
1423
|
if (operation.index === 0) continue;
|
|
1424
|
+
if (operation.error || isDenied(operation)) continue;
|
|
1157
1425
|
if (operation.action.type === "UPGRADE_DOCUMENT") {
|
|
1158
1426
|
const upgradeAction = operation.action;
|
|
1159
1427
|
const fromVersion = upgradeAction.input.fromVersion;
|
|
@@ -1177,7 +1445,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1177
1445
|
docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1178
1446
|
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1179
1447
|
else {
|
|
1180
|
-
const protocolVersion = document.header
|
|
1448
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1181
1449
|
document = docModule.reducer(document, operation.action, void 0, {
|
|
1182
1450
|
skip: operation.skip,
|
|
1183
1451
|
protocolVersion
|
|
@@ -1185,6 +1453,21 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1185
1453
|
}
|
|
1186
1454
|
}
|
|
1187
1455
|
}
|
|
1456
|
+
if (scope === "document") {
|
|
1457
|
+
const last = lastDocumentScopeOperation ?? await this.operationAt(documentId, "document", branch, startRevision, signal);
|
|
1458
|
+
document.operations = {
|
|
1459
|
+
...document.operations,
|
|
1460
|
+
document: last ? [last] : []
|
|
1461
|
+
};
|
|
1462
|
+
return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
1463
|
+
}
|
|
1464
|
+
if (keyframe) {
|
|
1465
|
+
const resumeOperation = await this.operationAt(documentId, scope, branch, startRevision, signal);
|
|
1466
|
+
if (resumeOperation) document.operations = {
|
|
1467
|
+
...document.operations,
|
|
1468
|
+
[scope]: [resumeOperation]
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1188
1471
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
1189
1472
|
const getModuleCached = (version) => {
|
|
1190
1473
|
const key = version ?? 0;
|
|
@@ -1209,11 +1492,14 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1209
1492
|
for (const operation of result.results) {
|
|
1210
1493
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1211
1494
|
const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, extractModuleVersion(document));
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1495
|
+
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
1496
|
+
else {
|
|
1497
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1498
|
+
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
1499
|
+
skip: operation.skip,
|
|
1500
|
+
protocolVersion
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1217
1503
|
}
|
|
1218
1504
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
1219
1505
|
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
@@ -1222,11 +1508,31 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1222
1508
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1223
1509
|
}
|
|
1224
1510
|
} while (hasMorePages);
|
|
1511
|
+
return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Copies the current document revisions onto the document. Overwrites the
|
|
1515
|
+
* requested scope revision with the target revision, if provided.
|
|
1516
|
+
*/
|
|
1517
|
+
async stampRevisions(document, documentId, scope, branch, targetRevision, signal) {
|
|
1225
1518
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1226
1519
|
document.header.revision = revisions.revision;
|
|
1520
|
+
if (targetRevision !== void 0) document.header.revision = {
|
|
1521
|
+
...document.header.revision,
|
|
1522
|
+
[scope]: targetRevision + 1
|
|
1523
|
+
};
|
|
1227
1524
|
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1228
1525
|
return document;
|
|
1229
1526
|
}
|
|
1527
|
+
/** The stored operation at `index`, or undefined if it is no longer there. */
|
|
1528
|
+
async operationAt(documentId, scope, branch, index, signal) {
|
|
1529
|
+
if (index < 0) return;
|
|
1530
|
+
const operation = (await this.operationStore.getSince(documentId, scope, branch, index - 1, void 0, {
|
|
1531
|
+
cursor: "0",
|
|
1532
|
+
limit: 1
|
|
1533
|
+
}, signal)).results[0];
|
|
1534
|
+
return operation && operation.index === index ? operation : void 0;
|
|
1535
|
+
}
|
|
1230
1536
|
/**
|
|
1231
1537
|
* Resolves which module version to use for a given operation in phase 2.
|
|
1232
1538
|
*
|
|
@@ -1250,19 +1556,22 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1250
1556
|
async warmMissRebuild(baseDocument, baseRevision, documentId, scope, branch, targetRevision, signal) {
|
|
1251
1557
|
const documentType = baseDocument.header.documentType;
|
|
1252
1558
|
const docScopeNextIndex = baseDocument.header.revision["document"] ?? 0;
|
|
1253
|
-
if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.
|
|
1559
|
+
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);
|
|
1254
1560
|
const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
|
|
1255
|
-
let document = baseDocument;
|
|
1561
|
+
let document = copyDocument(baseDocument);
|
|
1256
1562
|
try {
|
|
1257
1563
|
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
1258
1564
|
for (const operation of pagedResults.results) {
|
|
1259
1565
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1260
1566
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1567
|
+
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
1568
|
+
else {
|
|
1569
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1570
|
+
document = module.reducer(document, operation.action, void 0, {
|
|
1571
|
+
skip: operation.skip,
|
|
1572
|
+
protocolVersion
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1266
1575
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
1267
1576
|
}
|
|
1268
1577
|
} catch (err) {
|
|
@@ -1270,6 +1579,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1270
1579
|
}
|
|
1271
1580
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1272
1581
|
document.header.revision = revisions.revision;
|
|
1582
|
+
if (targetRevision !== void 0) document.header.revision = {
|
|
1583
|
+
...document.header.revision,
|
|
1584
|
+
[scope]: targetRevision + 1
|
|
1585
|
+
};
|
|
1273
1586
|
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1274
1587
|
return document;
|
|
1275
1588
|
}
|
|
@@ -1339,6 +1652,32 @@ var EventBus = class {
|
|
|
1339
1652
|
}
|
|
1340
1653
|
};
|
|
1341
1654
|
//#endregion
|
|
1655
|
+
//#region src/core/feature-flags.ts
|
|
1656
|
+
/**
|
|
1657
|
+
* Every flag this reactor knows, with the flags it requires. A stage adds its
|
|
1658
|
+
* flag here when it ships, so asking an older reactor for a later stage's flag
|
|
1659
|
+
* is an unrecognized name rather than a flag that quietly does nothing.
|
|
1660
|
+
*/
|
|
1661
|
+
const FLAG_PREREQUISITES = {
|
|
1662
|
+
documentDecisions: [],
|
|
1663
|
+
authEnforcement: ["documentDecisions"]
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Throws when the flags ask for enforcement the reactor cannot deliver. Either
|
|
1667
|
+
* failure would otherwise read as enforcement being on while the reactor
|
|
1668
|
+
* applies less than the caller asked for.
|
|
1669
|
+
*/
|
|
1670
|
+
function validateFeatureFlags(flags, prerequisites) {
|
|
1671
|
+
const known = Object.keys(prerequisites);
|
|
1672
|
+
const unrecognized = Object.keys(flags).filter((name) => !known.includes(name));
|
|
1673
|
+
if (unrecognized.length > 0) throw new Error(`Unrecognized reactor feature flag: ${unrecognized.join(", ")}. This reactor knows: ${known.join(", ")}.`);
|
|
1674
|
+
for (const name of known) {
|
|
1675
|
+
if (flags[name] !== true) continue;
|
|
1676
|
+
const missing = prerequisites[name].filter((required) => flags[required] !== true);
|
|
1677
|
+
if (missing.length > 0) throw new Error(`Reactor feature flag ${name} requires ${missing.join(", ")}.`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1342
1681
|
//#region src/executor/execution-scope.ts
|
|
1343
1682
|
var DefaultExecutionScope = class {
|
|
1344
1683
|
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
@@ -1425,15 +1764,402 @@ function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
|
1425
1764
|
if (shouldPrioritizeLogicalIndex) {
|
|
1426
1765
|
if (logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1427
1766
|
}
|
|
1428
|
-
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
1429
|
-
if (actionIdDiff !== 0) return actionIdDiff;
|
|
1430
|
-
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1431
|
-
return a.id.localeCompare(b.id);
|
|
1432
|
-
}).map((op, i) => ({
|
|
1433
|
-
...op,
|
|
1434
|
-
index: startIndex.index + i,
|
|
1435
|
-
skip: i === 0 ? startIndex.skip : 0
|
|
1436
|
-
}));
|
|
1767
|
+
const actionIdDiff = (a.action?.id ?? "").localeCompare(b.action?.id ?? "");
|
|
1768
|
+
if (actionIdDiff !== 0) return actionIdDiff;
|
|
1769
|
+
if (!shouldPrioritizeLogicalIndex && logicalIndexDiff !== 0) return logicalIndexDiff;
|
|
1770
|
+
return a.id.localeCompare(b.id);
|
|
1771
|
+
}).map((op, i) => ({
|
|
1772
|
+
...op,
|
|
1773
|
+
index: startIndex.index + i,
|
|
1774
|
+
skip: i === 0 ? startIndex.skip : 0
|
|
1775
|
+
}));
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/decision/auth-decision-model.ts
|
|
1779
|
+
function refusalReason(refusal) {
|
|
1780
|
+
switch (refusal) {
|
|
1781
|
+
case "version-unsupported": return AUTH_VERSION_UNSUPPORTED_REASON;
|
|
1782
|
+
case "denied-by-grant": return AUTH_DENIED_BY_GRANT_REASON;
|
|
1783
|
+
case "no-applicable-grant": return AUTH_NO_GRANT_REASON;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/** This decision model uses both the document and the auth streams. */
|
|
1787
|
+
function authDecisionModel(target) {
|
|
1788
|
+
return {
|
|
1789
|
+
projections: {
|
|
1790
|
+
document: {
|
|
1791
|
+
decidingActions: ["DELETE_DOCUMENT"],
|
|
1792
|
+
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
1793
|
+
...document,
|
|
1794
|
+
state: { ...document.state }
|
|
1795
|
+
}, operation.action) : document,
|
|
1796
|
+
query: {
|
|
1797
|
+
documentId: target.documentId,
|
|
1798
|
+
branch: target.branch,
|
|
1799
|
+
scope: "document"
|
|
1800
|
+
}
|
|
1801
|
+
},
|
|
1802
|
+
auth: {
|
|
1803
|
+
decidingActions: [...AUTH_ACTION_TYPES],
|
|
1804
|
+
apply: (document, operation) => applyAuthAction(document, operation.action),
|
|
1805
|
+
query: {
|
|
1806
|
+
documentId: target.documentId,
|
|
1807
|
+
branch: target.branch,
|
|
1808
|
+
scope: "auth"
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
evaluatesScope() {
|
|
1813
|
+
return true;
|
|
1814
|
+
},
|
|
1815
|
+
decide(model, subject, request) {
|
|
1816
|
+
if (request.verb === "execute" && model.document.isDeleted) return {
|
|
1817
|
+
decision: "deny",
|
|
1818
|
+
reason: DOCUMENT_DELETED_REASON
|
|
1819
|
+
};
|
|
1820
|
+
const evaluation = evaluate(model.auth, subject, request);
|
|
1821
|
+
if (evaluation.decision === "allow") return { decision: "allow" };
|
|
1822
|
+
return {
|
|
1823
|
+
decision: "deny",
|
|
1824
|
+
reason: refusalReason(evaluation.refusal)
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
//#endregion
|
|
1830
|
+
//#region src/decision/build-decision-model.ts
|
|
1831
|
+
/**
|
|
1832
|
+
* Reads each projection's stream through the write cache, recording the
|
|
1833
|
+
* revision observed. Static projections resolve first; derived projections
|
|
1834
|
+
* see only those and contribute a map from document id to state. Each
|
|
1835
|
+
* distinct stream is read once and yields one append condition entry.
|
|
1836
|
+
*/
|
|
1837
|
+
async function buildDecisionModel(cache, definition, target, signal) {
|
|
1838
|
+
const decisionModel = definition(target);
|
|
1839
|
+
const projections = Object.entries(decisionModel.projections);
|
|
1840
|
+
const reads = /* @__PURE__ */ new Map();
|
|
1841
|
+
const model = {};
|
|
1842
|
+
for (const [key, projection] of projections) {
|
|
1843
|
+
if (typeof projection.query === "function") continue;
|
|
1844
|
+
model[key] = (await readStream(cache, projection.query, reads, signal)).state;
|
|
1845
|
+
}
|
|
1846
|
+
const staticModel = { ...model };
|
|
1847
|
+
for (const [key, projection] of projections) {
|
|
1848
|
+
if (typeof projection.query !== "function") continue;
|
|
1849
|
+
const queries = projection.query(staticModel);
|
|
1850
|
+
const value = {};
|
|
1851
|
+
for (const query of queries) {
|
|
1852
|
+
const read = await readStream(cache, query, reads, signal);
|
|
1853
|
+
value[query.documentId] = read.state;
|
|
1854
|
+
}
|
|
1855
|
+
model[key] = value;
|
|
1856
|
+
}
|
|
1857
|
+
return {
|
|
1858
|
+
model,
|
|
1859
|
+
appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async function readStream(cache, query, reads, signal) {
|
|
1863
|
+
const key = `${query.documentId}:${query.scope}:${query.branch}`;
|
|
1864
|
+
const existing = reads.get(key);
|
|
1865
|
+
if (existing) return existing;
|
|
1866
|
+
const document = await cache.getState(query.documentId, query.scope, query.branch, void 0, signal);
|
|
1867
|
+
const read = {
|
|
1868
|
+
state: document.state[query.scope],
|
|
1869
|
+
stream: {
|
|
1870
|
+
documentId: query.documentId,
|
|
1871
|
+
scope: query.scope,
|
|
1872
|
+
branch: query.branch,
|
|
1873
|
+
revision: observedRevision(document, query.scope)
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
reads.set(key, read);
|
|
1877
|
+
return read;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* The highest operation index the document reflects for the scope, or -1 if
|
|
1881
|
+
* empty. `header.revision` is authoritative, not the rebuilt operation list.
|
|
1882
|
+
*/
|
|
1883
|
+
function observedRevision(document, scope) {
|
|
1884
|
+
if (scope in document.header.revision) return document.header.revision[scope] - 1;
|
|
1885
|
+
if (scope in document.operations) {
|
|
1886
|
+
const operations = document.operations[scope];
|
|
1887
|
+
if (operations.length > 0) return operations[operations.length - 1].index;
|
|
1888
|
+
}
|
|
1889
|
+
if (!(scope in document.header.revision)) return -1;
|
|
1890
|
+
return document.header.revision[scope] - 1;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* The streams a model reads whose queries are known before it is built. A
|
|
1894
|
+
* derived query needs the statically-queried projections first, so it is not
|
|
1895
|
+
* included here.
|
|
1896
|
+
*/
|
|
1897
|
+
function staticReadSet(definition) {
|
|
1898
|
+
const streams = [];
|
|
1899
|
+
for (const [name, projection] of Object.entries(definition.projections)) {
|
|
1900
|
+
if (typeof projection.query === "function") continue;
|
|
1901
|
+
streams.push({
|
|
1902
|
+
name,
|
|
1903
|
+
query: projection.query,
|
|
1904
|
+
decidingActions: projection.decidingActions,
|
|
1905
|
+
apply: projection.apply
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
return streams;
|
|
1909
|
+
}
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region src/decision/document-decision-model.ts
|
|
1912
|
+
/**
|
|
1913
|
+
* The simplest decision model: one projection over the document scope, which
|
|
1914
|
+
* rejects on a deleted document.
|
|
1915
|
+
*/
|
|
1916
|
+
function documentDecisionModel(target) {
|
|
1917
|
+
return {
|
|
1918
|
+
projections: { document: {
|
|
1919
|
+
decidingActions: ["DELETE_DOCUMENT"],
|
|
1920
|
+
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
1921
|
+
...document,
|
|
1922
|
+
state: { ...document.state }
|
|
1923
|
+
}, operation.action) : document,
|
|
1924
|
+
query: {
|
|
1925
|
+
documentId: target.documentId,
|
|
1926
|
+
branch: target.branch,
|
|
1927
|
+
scope: "document"
|
|
1928
|
+
}
|
|
1929
|
+
} },
|
|
1930
|
+
evaluatesScope() {
|
|
1931
|
+
return true;
|
|
1932
|
+
},
|
|
1933
|
+
decide(model) {
|
|
1934
|
+
return model.document.isDeleted ? {
|
|
1935
|
+
decision: "deny",
|
|
1936
|
+
reason: DOCUMENT_DELETED_REASON
|
|
1937
|
+
} : { decision: "allow" };
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/decision/registered-model.ts
|
|
1943
|
+
/**
|
|
1944
|
+
* Builds the model at the stream heads and decides one request against it. The
|
|
1945
|
+
* append condition it returns is the read-set the store enforces at write time.
|
|
1946
|
+
*/
|
|
1947
|
+
async function decideAtHead(model, cache, target, subject, request, signal) {
|
|
1948
|
+
const built = await buildDecisionModel(cache, model, target, signal);
|
|
1949
|
+
return {
|
|
1950
|
+
evaluation: model(target).decide(built.model, subject, request, { scopeState: void 0 }),
|
|
1951
|
+
appendCondition: built.appendCondition,
|
|
1952
|
+
documentVersion: built.model.document.version,
|
|
1953
|
+
deletedAtUtcIso: built.model.document.deletedAtUtcIso ?? null
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* The model this reactor enforces. With `authEnforcement` off the auth scope is
|
|
1958
|
+
* absent from every append condition and no load walks it.
|
|
1959
|
+
*/
|
|
1960
|
+
function selectDecisionModel(flags) {
|
|
1961
|
+
return flags.authEnforcement ? authDecisionModel : documentDecisionModel;
|
|
1962
|
+
}
|
|
1963
|
+
//#endregion
|
|
1964
|
+
//#region src/decision/merged-order.ts
|
|
1965
|
+
/** Identifies a stream within a walk. */
|
|
1966
|
+
function streamKey(query) {
|
|
1967
|
+
return `${query.documentId}:${query.scope}:${query.branch}`;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Orders two operations from different streams by position. Timestamp decides;
|
|
1971
|
+
* an equal timestamp puts an auth operation first, and otherwise falls to the
|
|
1972
|
+
* action id and then the operation id, so that two replicas holding the same
|
|
1973
|
+
* operations agree on the order whatever order they happen to store them in.
|
|
1974
|
+
*/
|
|
1975
|
+
function comparePositions(a, b) {
|
|
1976
|
+
const aTime = Date.parse(a.operation.timestampUtcMs);
|
|
1977
|
+
const bTime = Date.parse(b.operation.timestampUtcMs);
|
|
1978
|
+
if (aTime !== bTime) return aTime - bTime;
|
|
1979
|
+
if (a.streamKey === b.streamKey) return a.operation.index - b.operation.index;
|
|
1980
|
+
const aAuth = a.scope === "auth";
|
|
1981
|
+
if (aAuth !== (b.scope === "auth")) return aAuth ? -1 : 1;
|
|
1982
|
+
const actionIds = (a.operation.action.id ?? "").localeCompare(b.operation.action.id ?? "");
|
|
1983
|
+
if (actionIds !== 0) return actionIds;
|
|
1984
|
+
return (a.operation.id ?? "").localeCompare(b.operation.id ?? "");
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Merges the read-set streams into one sequence by position. An operation's
|
|
1988
|
+
* place in the result is the bound a decision at that operation reads to: every
|
|
1989
|
+
* operation before it has been applied, and it has not.
|
|
1990
|
+
*/
|
|
1991
|
+
function mergeByPosition(streams) {
|
|
1992
|
+
const merged = [];
|
|
1993
|
+
for (const stream of streams) for (const operation of stream.operations) merged.push({
|
|
1994
|
+
streamKey: stream.streamKey,
|
|
1995
|
+
scope: stream.scope,
|
|
1996
|
+
operation
|
|
1997
|
+
});
|
|
1998
|
+
return merged.sort(comparePositions);
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* The skip that retracts everything from `firstRetractedIndex` up to where the
|
|
2002
|
+
* re-appended operation lands. It spans the indexes rather than counting the
|
|
2003
|
+
* operations, because a stream with a gap in it makes those differ.
|
|
2004
|
+
*/
|
|
2005
|
+
function retractionSkip(nextIndex, firstRetractedIndex) {
|
|
2006
|
+
return nextIndex - firstRetractedIndex;
|
|
2007
|
+
}
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/decision/walk.ts
|
|
2010
|
+
/**
|
|
2011
|
+
* A single forward pass is only correct while a stream's effective operations
|
|
2012
|
+
* are ordered.
|
|
2013
|
+
*/
|
|
2014
|
+
function assertPositionOrder(streamKey, scope, operations) {
|
|
2015
|
+
for (let i = 1; i < operations.length; i++) {
|
|
2016
|
+
const previous = operations[i - 1];
|
|
2017
|
+
const current = operations[i];
|
|
2018
|
+
if (comparePositions({
|
|
2019
|
+
streamKey,
|
|
2020
|
+
scope,
|
|
2021
|
+
operation: previous
|
|
2022
|
+
}, {
|
|
2023
|
+
streamKey,
|
|
2024
|
+
scope,
|
|
2025
|
+
operation: current
|
|
2026
|
+
}) > 0) throw new Error(`Stream ${streamKey} is out of position order: index ${previous.index} at ${previous.timestampUtcMs} precedes index ${current.index} at ${current.timestampUtcMs}`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Visits every operation in the read-set once, in the order their positions
|
|
2031
|
+
* fall, and hands back the state each stream held just before it. That state is
|
|
2032
|
+
* what a decision at that operation reads.
|
|
2033
|
+
*
|
|
2034
|
+
* Skips are resolved first (i.e. this is performed on a garbage collected
|
|
2035
|
+
* stream), which means we can do a single forward pass.
|
|
2036
|
+
*
|
|
2037
|
+
* An operation that contributes no state, whether denied or holding a reducer
|
|
2038
|
+
* error, is visited but not applied (this matches the write cache's rebuild).
|
|
2039
|
+
*
|
|
2040
|
+
* The consumer sends back whether it refused the operation it was handed: a
|
|
2041
|
+
* refusal this pass produced must suppress it the same way a stored one does.
|
|
2042
|
+
*/
|
|
2043
|
+
function* walkByPosition(streams) {
|
|
2044
|
+
const merged = mergeByPosition(streams.map((stream) => {
|
|
2045
|
+
const operations = garbageCollect(sortOperations([...stream.operations]));
|
|
2046
|
+
assertPositionOrder(stream.streamKey, stream.scope, operations);
|
|
2047
|
+
return {
|
|
2048
|
+
streamKey: stream.streamKey,
|
|
2049
|
+
scope: stream.scope,
|
|
2050
|
+
operations
|
|
2051
|
+
};
|
|
2052
|
+
}));
|
|
2053
|
+
const byKey = new Map(streams.map((stream) => [stream.streamKey, stream]));
|
|
2054
|
+
const states = new Map(streams.map((stream) => [stream.streamKey, stream.document]));
|
|
2055
|
+
for (const { streamKey, operation } of merged) {
|
|
2056
|
+
if ((yield {
|
|
2057
|
+
streamKey,
|
|
2058
|
+
operation,
|
|
2059
|
+
states: new Map(states)
|
|
2060
|
+
}) || operation.error !== void 0 || isDenied(operation)) continue;
|
|
2061
|
+
const stream = byKey.get(streamKey);
|
|
2062
|
+
const before = states.get(streamKey);
|
|
2063
|
+
if (before === void 0 || stream === void 0) throw new Error(`No state for stream ${streamKey}`);
|
|
2064
|
+
states.set(streamKey, stream.apply(before, operation));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
//#endregion
|
|
2068
|
+
//#region src/decision/evaluation.ts
|
|
2069
|
+
/** The stream key for evaluated operations whose scope no projection reads. */
|
|
2070
|
+
const EVALUATED_ONLY = "evaluated";
|
|
2071
|
+
/**
|
|
2072
|
+
* Whether any stream the model reads declares this operation's action type as
|
|
2073
|
+
* one that can change an evaluation.
|
|
2074
|
+
*/
|
|
2075
|
+
function isDecidingAction(operation, readSet) {
|
|
2076
|
+
return readSet.some((stream) => stream.decidingActions.includes(operation.action.type));
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Who an operation acts as. A replayed operation is evaluated as its own signer,
|
|
2080
|
+
* so an address-scoped policy does not deny its own author's history.
|
|
2081
|
+
*/
|
|
2082
|
+
function subjectOf(operation) {
|
|
2083
|
+
const signer = operation.action.context?.signer;
|
|
2084
|
+
return {
|
|
2085
|
+
address: signer?.user.address,
|
|
2086
|
+
key: signer?.app.key
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* The model as the walk reached this operation: each projection's value is its
|
|
2091
|
+
* own scope's state, taken from the stream that projection reads.
|
|
2092
|
+
*/
|
|
2093
|
+
function modelAt(readSet, states) {
|
|
2094
|
+
const model = {};
|
|
2095
|
+
for (const stream of readSet) {
|
|
2096
|
+
const document = states.get(streamKey(stream.query));
|
|
2097
|
+
if (document === void 0) throw new Error(`No state walked for projection ${stream.name}`);
|
|
2098
|
+
model[stream.name] = document.state[stream.query.scope];
|
|
2099
|
+
}
|
|
2100
|
+
return model;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Evaluates each operation at its own position and returns the refusals in an
|
|
2104
|
+
* array parallel to the operations, where undefined means allowed.
|
|
2105
|
+
*
|
|
2106
|
+
* A position is a timestamp, so an operation refused by a delete is one that
|
|
2107
|
+
* sorts after it, and the operations before it are left alone. That holds
|
|
2108
|
+
* whether the delete is already stored or is among the operations passed in.
|
|
2109
|
+
*/
|
|
2110
|
+
async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
2111
|
+
const { scope, operations } = subject;
|
|
2112
|
+
const { writeCache, operationStore } = stores;
|
|
2113
|
+
const definition = model(target);
|
|
2114
|
+
const readSet = staticReadSet(definition);
|
|
2115
|
+
if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
|
|
2116
|
+
const evaluating = new Set(operations.map((operation) => operation.id));
|
|
2117
|
+
const readStreams = await Promise.all(readSet.map(async (stream) => ({
|
|
2118
|
+
stream,
|
|
2119
|
+
operations: (await operationStore.getSince(stream.query.documentId, stream.query.scope, stream.query.branch, -1, { actionTypes: stream.decidingActions }, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id))
|
|
2120
|
+
})));
|
|
2121
|
+
const decidingOperations = operations.filter((operation) => isDecidingAction(operation, readSet));
|
|
2122
|
+
if (readStreams.every((read) => read.operations.length === 0) && decidingOperations.length === 0) return operations.map(() => void 0);
|
|
2123
|
+
if (readStreams.length === 0) throw new Error(`Decision model for ${target.documentId} reads no stream whose query is known before it is built`);
|
|
2124
|
+
const writtenProjection = readSet.find((stream) => stream.query.scope === scope);
|
|
2125
|
+
const walked = [];
|
|
2126
|
+
for (const read of readStreams) {
|
|
2127
|
+
const isWritten = read.stream === writtenProjection;
|
|
2128
|
+
const before = await writeCache.getState(read.stream.query.documentId, read.stream.query.scope, read.stream.query.branch, -1, signal);
|
|
2129
|
+
walked.push({
|
|
2130
|
+
streamKey: streamKey(read.stream.query),
|
|
2131
|
+
scope: read.stream.query.scope,
|
|
2132
|
+
document: before,
|
|
2133
|
+
operations: isWritten ? [...read.operations, ...operations] : read.operations,
|
|
2134
|
+
apply: read.stream.apply
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
if (writtenProjection === void 0) walked.push({
|
|
2138
|
+
streamKey: EVALUATED_ONLY,
|
|
2139
|
+
scope,
|
|
2140
|
+
document: walked[0].document,
|
|
2141
|
+
operations,
|
|
2142
|
+
apply: (document) => document
|
|
2143
|
+
});
|
|
2144
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
2145
|
+
const walk = walkByPosition(walked);
|
|
2146
|
+
let step = walk.next(false);
|
|
2147
|
+
while (!step.done) {
|
|
2148
|
+
const position = step.value;
|
|
2149
|
+
if (!evaluating.has(position.operation.id)) {
|
|
2150
|
+
step = walk.next(false);
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const evaluation = definition.decide(modelAt(readSet, position.states), subjectOf(position.operation), {
|
|
2154
|
+
verb: "execute",
|
|
2155
|
+
scope: position.operation.action.scope,
|
|
2156
|
+
operation: position.operation.action.type
|
|
2157
|
+
}, { scopeState: void 0 });
|
|
2158
|
+
const denied = evaluation.decision === "deny";
|
|
2159
|
+
reasons.set(position.operation.id, denied ? evaluation.reason : void 0);
|
|
2160
|
+
step = walk.next(denied);
|
|
2161
|
+
}
|
|
2162
|
+
return operations.map((operation) => reasons.get(operation.id));
|
|
1437
2163
|
}
|
|
1438
2164
|
//#endregion
|
|
1439
2165
|
//#region src/cache/operation-index-types.ts
|
|
@@ -1482,23 +2208,119 @@ var DriveCollectionId = class DriveCollectionId {
|
|
|
1482
2208
|
//#endregion
|
|
1483
2209
|
//#region src/executor/document-action-handler.ts
|
|
1484
2210
|
var DocumentActionHandler = class {
|
|
1485
|
-
constructor(registry, logger, driveContainerTypes) {
|
|
2211
|
+
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel) {
|
|
1486
2212
|
this.registry = registry;
|
|
1487
2213
|
this.logger = logger;
|
|
1488
2214
|
this.driveContainerTypes = driveContainerTypes;
|
|
1489
|
-
|
|
1490
|
-
|
|
2215
|
+
this.featureFlags = featureFlags;
|
|
2216
|
+
this.decisionModel = decisionModel;
|
|
2217
|
+
}
|
|
2218
|
+
/** Whether the write arrives with its evaluation already decided. */
|
|
2219
|
+
alreadyEvaluated(executing) {
|
|
2220
|
+
return this.featureFlags.documentDecisions && (executing.replayingAcceptedHistory || executing.evaluatedByPosition);
|
|
2221
|
+
}
|
|
2222
|
+
async execute(write, executing) {
|
|
2223
|
+
const { action } = write;
|
|
2224
|
+
if (write.deniedReason !== void 0) return this.writeDenied(write, executing);
|
|
2225
|
+
const refusal = await this.refuseIfPolicyDenies(write, executing);
|
|
2226
|
+
if (refusal) return refusal;
|
|
1491
2227
|
switch (action.type) {
|
|
1492
|
-
case "CREATE_DOCUMENT": return this.executeCreate(
|
|
1493
|
-
case "DELETE_DOCUMENT": return this.executeDelete(
|
|
1494
|
-
case "UPGRADE_DOCUMENT": return this.executeUpgrade(
|
|
1495
|
-
case "ADD_RELATIONSHIP": return this.executeAddRelationship(
|
|
1496
|
-
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(
|
|
1497
|
-
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(
|
|
1498
|
-
default: return buildErrorResult(job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), startTime);
|
|
2228
|
+
case "CREATE_DOCUMENT": return this.executeCreate(write, executing);
|
|
2229
|
+
case "DELETE_DOCUMENT": return this.executeDelete(write, executing);
|
|
2230
|
+
case "UPGRADE_DOCUMENT": return this.executeUpgrade(write, executing);
|
|
2231
|
+
case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
|
|
2232
|
+
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
|
|
2233
|
+
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(write, executing);
|
|
2234
|
+
default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Refuses a document-scope write the policy denies, or undefined to proceed.
|
|
2239
|
+
* Without this an `execute`-on-`document` grant is unenforceable.
|
|
2240
|
+
*/
|
|
2241
|
+
async refuseIfPolicyDenies(write, executing) {
|
|
2242
|
+
const { action } = write;
|
|
2243
|
+
const { job, startTime, stores, signal } = executing;
|
|
2244
|
+
if (!this.featureFlags.documentDecisions || !this.featureFlags.authEnforcement || this.alreadyEvaluated(executing) || !GATED_DOCUMENT_ACTIONS.has(action.type)) return;
|
|
2245
|
+
const documentId = targetDocumentId(action, job.documentId);
|
|
2246
|
+
let admission;
|
|
2247
|
+
try {
|
|
2248
|
+
admission = await decideAtHead(this.decisionModel, stores.writeCache, {
|
|
2249
|
+
documentId,
|
|
2250
|
+
branch: job.branch
|
|
2251
|
+
}, {
|
|
2252
|
+
address: action.context?.signer?.user.address,
|
|
2253
|
+
key: action.context?.signer?.app.key
|
|
2254
|
+
}, {
|
|
2255
|
+
verb: "execute",
|
|
2256
|
+
scope: action.scope,
|
|
2257
|
+
operation: action.type
|
|
2258
|
+
}, signal);
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1499
2261
|
}
|
|
2262
|
+
if (admission.evaluation.decision === "allow") return;
|
|
2263
|
+
return buildErrorResult(job, refusalError(admission.evaluation.reason, documentId, admission.deletedAtUtcIso, action), startTime);
|
|
1500
2264
|
}
|
|
1501
|
-
|
|
2265
|
+
/** A refused operation holds a position in the stream but changes nothing. */
|
|
2266
|
+
async writeDenied(write, executing) {
|
|
2267
|
+
const { action, skip, sourceRemote, deniedReason } = write;
|
|
2268
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
2269
|
+
let document;
|
|
2270
|
+
try {
|
|
2271
|
+
document = await stores.writeCache.getState(job.documentId, job.scope, job.branch, void 0, signal);
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2274
|
+
}
|
|
2275
|
+
const index = getNextIndexForScope(document, job.scope);
|
|
2276
|
+
let standing = document;
|
|
2277
|
+
if (skip > 0) try {
|
|
2278
|
+
standing = await stores.writeCache.getState(job.documentId, job.scope, job.branch, index - skip - 1, signal);
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2281
|
+
}
|
|
2282
|
+
let operation = createOperation(action, index, skip, {
|
|
2283
|
+
documentId: job.documentId,
|
|
2284
|
+
scope: job.scope,
|
|
2285
|
+
branch: job.branch
|
|
2286
|
+
});
|
|
2287
|
+
operation.deniedReason = deniedReason;
|
|
2288
|
+
operation.hash = hashDocumentStateForScope(standing, job.scope);
|
|
2289
|
+
const writeResult = await this.writeOperationToStore({
|
|
2290
|
+
documentId: job.documentId,
|
|
2291
|
+
documentType: document.header.documentType,
|
|
2292
|
+
scope: job.scope,
|
|
2293
|
+
branch: job.branch
|
|
2294
|
+
}, operation, executing);
|
|
2295
|
+
if (!Array.isArray(writeResult)) return writeResult;
|
|
2296
|
+
operation = writeResult[0];
|
|
2297
|
+
updateDocumentRevision(standing, job.scope, operation.index);
|
|
2298
|
+
standing.operations = {
|
|
2299
|
+
...standing.operations,
|
|
2300
|
+
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2301
|
+
};
|
|
2302
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, SnapshotPosition.Head);
|
|
2303
|
+
indexTxn.write([{
|
|
2304
|
+
...operation,
|
|
2305
|
+
documentId: job.documentId,
|
|
2306
|
+
documentType: document.header.documentType,
|
|
2307
|
+
branch: job.branch,
|
|
2308
|
+
scope: job.scope,
|
|
2309
|
+
sourceRemote
|
|
2310
|
+
}]);
|
|
2311
|
+
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2312
|
+
state: standing.state.document,
|
|
2313
|
+
documentType: standing.header.documentType,
|
|
2314
|
+
documentScopeRevision: operation.index + 1
|
|
2315
|
+
});
|
|
2316
|
+
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
2317
|
+
header: standing.header,
|
|
2318
|
+
document: standing.state.document
|
|
2319
|
+
}), startTime);
|
|
2320
|
+
}
|
|
2321
|
+
async executeCreate(write, executing) {
|
|
2322
|
+
const { action, skip, sourceRemote } = write;
|
|
2323
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
1502
2324
|
if (job.scope !== "document") return {
|
|
1503
2325
|
job,
|
|
1504
2326
|
success: false,
|
|
@@ -1516,7 +2338,12 @@ var DocumentActionHandler = class {
|
|
|
1516
2338
|
...document.state
|
|
1517
2339
|
};
|
|
1518
2340
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
1519
|
-
const writeResult = await this.writeOperationToStore(
|
|
2341
|
+
const writeResult = await this.writeOperationToStore({
|
|
2342
|
+
documentId: document.header.id,
|
|
2343
|
+
documentType: document.header.documentType,
|
|
2344
|
+
scope: job.scope,
|
|
2345
|
+
branch: job.branch
|
|
2346
|
+
}, operation, executing);
|
|
1520
2347
|
if (!Array.isArray(writeResult)) return writeResult;
|
|
1521
2348
|
operation = writeResult[0];
|
|
1522
2349
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
@@ -1524,7 +2351,7 @@ var DocumentActionHandler = class {
|
|
|
1524
2351
|
...document.operations,
|
|
1525
2352
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1526
2353
|
};
|
|
1527
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
2354
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1528
2355
|
indexTxn.write([{
|
|
1529
2356
|
...operation,
|
|
1530
2357
|
documentId: document.header.id,
|
|
@@ -1545,7 +2372,9 @@ var DocumentActionHandler = class {
|
|
|
1545
2372
|
});
|
|
1546
2373
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
1547
2374
|
}
|
|
1548
|
-
async executeDelete(
|
|
2375
|
+
async executeDelete(write, executing) {
|
|
2376
|
+
const { action, skip, sourceRemote } = write;
|
|
2377
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
1549
2378
|
const input = action.input;
|
|
1550
2379
|
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1551
2380
|
const documentId = input.documentId;
|
|
@@ -1556,8 +2385,8 @@ var DocumentActionHandler = class {
|
|
|
1556
2385
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1557
2386
|
}
|
|
1558
2387
|
const documentState = document.state.document;
|
|
1559
|
-
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1560
|
-
let operation = createOperation(action, getNextIndexForScope(document, job.scope),
|
|
2388
|
+
if (documentState.isDeleted && !this.alreadyEvaluated(executing)) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
2389
|
+
let operation = createOperation(action, getNextIndexForScope(document, job.scope), skip, {
|
|
1561
2390
|
documentId,
|
|
1562
2391
|
scope: job.scope,
|
|
1563
2392
|
branch: job.branch
|
|
@@ -1568,7 +2397,12 @@ var DocumentActionHandler = class {
|
|
|
1568
2397
|
document: document.state.document
|
|
1569
2398
|
};
|
|
1570
2399
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
1571
|
-
const writeResult = await this.writeOperationToStore(
|
|
2400
|
+
const writeResult = await this.writeOperationToStore({
|
|
2401
|
+
documentId,
|
|
2402
|
+
documentType: document.header.documentType,
|
|
2403
|
+
scope: job.scope,
|
|
2404
|
+
branch: job.branch
|
|
2405
|
+
}, operation, executing);
|
|
1572
2406
|
if (!Array.isArray(writeResult)) return writeResult;
|
|
1573
2407
|
operation = writeResult[0];
|
|
1574
2408
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
@@ -1576,7 +2410,7 @@ var DocumentActionHandler = class {
|
|
|
1576
2410
|
...document.operations,
|
|
1577
2411
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1578
2412
|
};
|
|
1579
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
2413
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1580
2414
|
indexTxn.write([{
|
|
1581
2415
|
...operation,
|
|
1582
2416
|
documentId,
|
|
@@ -1592,7 +2426,9 @@ var DocumentActionHandler = class {
|
|
|
1592
2426
|
});
|
|
1593
2427
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
1594
2428
|
}
|
|
1595
|
-
async executeUpgrade(
|
|
2429
|
+
async executeUpgrade(write, executing) {
|
|
2430
|
+
const { action, skip, sourceRemote } = write;
|
|
2431
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
1596
2432
|
const input = action.input;
|
|
1597
2433
|
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1598
2434
|
const documentId = input.documentId;
|
|
@@ -1605,7 +2441,7 @@ var DocumentActionHandler = class {
|
|
|
1605
2441
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1606
2442
|
}
|
|
1607
2443
|
const documentState = document.state.document;
|
|
1608
|
-
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
2444
|
+
if (documentState.isDeleted && !this.alreadyEvaluated(executing)) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1609
2445
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
1610
2446
|
let upgradePath;
|
|
1611
2447
|
if (fromVersion > 0 && fromVersion < toVersion) try {
|
|
@@ -1635,7 +2471,12 @@ var DocumentActionHandler = class {
|
|
|
1635
2471
|
...document.state
|
|
1636
2472
|
};
|
|
1637
2473
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
1638
|
-
const writeResult = await this.writeOperationToStore(
|
|
2474
|
+
const writeResult = await this.writeOperationToStore({
|
|
2475
|
+
documentId,
|
|
2476
|
+
documentType: document.header.documentType,
|
|
2477
|
+
scope: job.scope,
|
|
2478
|
+
branch: job.branch
|
|
2479
|
+
}, operation, executing);
|
|
1639
2480
|
if (!Array.isArray(writeResult)) return writeResult;
|
|
1640
2481
|
operation = writeResult[0];
|
|
1641
2482
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
@@ -1643,7 +2484,7 @@ var DocumentActionHandler = class {
|
|
|
1643
2484
|
...document.operations,
|
|
1644
2485
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1645
2486
|
};
|
|
1646
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
2487
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1647
2488
|
indexTxn.write([{
|
|
1648
2489
|
...operation,
|
|
1649
2490
|
documentId,
|
|
@@ -1659,8 +2500,8 @@ var DocumentActionHandler = class {
|
|
|
1659
2500
|
});
|
|
1660
2501
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
1661
2502
|
}
|
|
1662
|
-
executeAddRelationship(
|
|
1663
|
-
return this.withRelationshipAction("ADD_RELATIONSHIP",
|
|
2503
|
+
executeAddRelationship(write, executing) {
|
|
2504
|
+
return this.withRelationshipAction("ADD_RELATIONSHIP", write, executing, (input) => input.sourceId === input.targetId ? /* @__PURE__ */ new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)") : null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
1664
2505
|
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
1665
2506
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
1666
2507
|
txn.addToCollection(collectionId, input.targetId);
|
|
@@ -1668,8 +2509,8 @@ var DocumentActionHandler = class {
|
|
|
1668
2509
|
}
|
|
1669
2510
|
});
|
|
1670
2511
|
}
|
|
1671
|
-
executeRemoveRelationship(
|
|
1672
|
-
return this.withRelationshipAction("REMOVE_RELATIONSHIP",
|
|
2512
|
+
executeRemoveRelationship(write, executing) {
|
|
2513
|
+
return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
1673
2514
|
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
1674
2515
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
1675
2516
|
txn.removeFromCollection(collectionId, input.targetId);
|
|
@@ -1677,10 +2518,12 @@ var DocumentActionHandler = class {
|
|
|
1677
2518
|
}
|
|
1678
2519
|
});
|
|
1679
2520
|
}
|
|
1680
|
-
executeUpdateRelationship(
|
|
1681
|
-
return this.withRelationshipAction("UPDATE_RELATIONSHIP",
|
|
2521
|
+
executeUpdateRelationship(write, executing) {
|
|
2522
|
+
return this.withRelationshipAction("UPDATE_RELATIONSHIP", write, executing, null, null);
|
|
1682
2523
|
}
|
|
1683
|
-
async withRelationshipAction(actionTypeName,
|
|
2524
|
+
async withRelationshipAction(actionTypeName, write, executing, preValidate, postWrite) {
|
|
2525
|
+
const { action, skip, sourceRemote } = write;
|
|
2526
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
1684
2527
|
if (job.scope !== "document") return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} must be in "document" scope, got "${job.scope}"`), startTime);
|
|
1685
2528
|
const input = action.input;
|
|
1686
2529
|
if (!input.sourceId || !input.targetId || !input.relationshipType) return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} action requires sourceId, targetId, and relationshipType in input`), startTime);
|
|
@@ -1694,12 +2537,17 @@ var DocumentActionHandler = class {
|
|
|
1694
2537
|
} catch (error) {
|
|
1695
2538
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1696
2539
|
}
|
|
1697
|
-
let operation = createOperation(action, getNextIndexForScope(sourceDoc, job.scope),
|
|
2540
|
+
let operation = createOperation(action, getNextIndexForScope(sourceDoc, job.scope), skip, {
|
|
1698
2541
|
documentId: input.sourceId,
|
|
1699
2542
|
scope: job.scope,
|
|
1700
2543
|
branch: job.branch
|
|
1701
2544
|
});
|
|
1702
|
-
const writeResult = await this.writeOperationToStore(
|
|
2545
|
+
const writeResult = await this.writeOperationToStore({
|
|
2546
|
+
documentId: input.sourceId,
|
|
2547
|
+
documentType: sourceDoc.header.documentType,
|
|
2548
|
+
scope: job.scope,
|
|
2549
|
+
branch: job.branch
|
|
2550
|
+
}, operation, executing);
|
|
1703
2551
|
if (!Array.isArray(writeResult)) return writeResult;
|
|
1704
2552
|
operation = writeResult[0];
|
|
1705
2553
|
sourceDoc.header.lastModifiedAtUtcIso = operation.timestampUtcMs || (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1714,7 +2562,7 @@ var DocumentActionHandler = class {
|
|
|
1714
2562
|
[job.scope]: scopeState === void 0 ? {} : structuredClone(scopeState)
|
|
1715
2563
|
};
|
|
1716
2564
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
1717
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
2565
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, SnapshotPosition.Head);
|
|
1718
2566
|
indexTxn.write([{
|
|
1719
2567
|
...operation,
|
|
1720
2568
|
documentId: input.sourceId,
|
|
@@ -1737,7 +2585,9 @@ var DocumentActionHandler = class {
|
|
|
1737
2585
|
});
|
|
1738
2586
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
1739
2587
|
}
|
|
1740
|
-
async writeOperationToStore(
|
|
2588
|
+
async writeOperationToStore(target, operation, executing) {
|
|
2589
|
+
const { documentId, documentType, scope, branch } = target;
|
|
2590
|
+
const { job, startTime, stores, signal } = executing;
|
|
1741
2591
|
let storedOperations;
|
|
1742
2592
|
try {
|
|
1743
2593
|
storedOperations = await stores.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
|
|
@@ -1746,10 +2596,11 @@ var DocumentActionHandler = class {
|
|
|
1746
2596
|
} catch (error) {
|
|
1747
2597
|
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
1748
2598
|
stores.writeCache.invalidate(documentId, scope, branch);
|
|
2599
|
+
if (AppendConditionFailedError.isError(error)) for (const stream of error.condition.streams) stores.writeCache.invalidate(stream.documentId, stream.scope, stream.branch);
|
|
1749
2600
|
return {
|
|
1750
2601
|
job,
|
|
1751
2602
|
success: false,
|
|
1752
|
-
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2603
|
+
error: AppendConditionFailedError.isError(error) ? error : /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
1753
2604
|
duration: Date.now() - startTime
|
|
1754
2605
|
};
|
|
1755
2606
|
}
|
|
@@ -1814,19 +2665,13 @@ function isValidISOTimestamp(value) {
|
|
|
1814
2665
|
if (!ISO_TIMESTAMP_REGEX.test(value)) return false;
|
|
1815
2666
|
return !isNaN(new Date(value).getTime());
|
|
1816
2667
|
}
|
|
1817
|
-
const documentScopeActions = [
|
|
1818
|
-
"CREATE_DOCUMENT",
|
|
1819
|
-
"DELETE_DOCUMENT",
|
|
1820
|
-
"UPGRADE_DOCUMENT",
|
|
1821
|
-
"ADD_RELATIONSHIP",
|
|
1822
|
-
"REMOVE_RELATIONSHIP",
|
|
1823
|
-
"UPDATE_RELATIONSHIP"
|
|
1824
|
-
];
|
|
1825
2668
|
/**
|
|
1826
2669
|
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
1827
2670
|
*/
|
|
1828
2671
|
var SimpleJobExecutor = class {
|
|
1829
2672
|
config;
|
|
2673
|
+
featureFlags;
|
|
2674
|
+
decisionModel;
|
|
1830
2675
|
signatureVerifierModule;
|
|
1831
2676
|
documentActionHandler;
|
|
1832
2677
|
executionScope;
|
|
@@ -1841,6 +2686,7 @@ var SimpleJobExecutor = class {
|
|
|
1841
2686
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
1842
2687
|
this.driveContainerTypes = driveContainerTypes;
|
|
1843
2688
|
this.config = {
|
|
2689
|
+
featureFlags: config.featureFlags ?? {},
|
|
1844
2690
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
1845
2691
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
1846
2692
|
jobTimeoutMs: config.jobTimeoutMs ?? 3e4,
|
|
@@ -1848,8 +2694,14 @@ var SimpleJobExecutor = class {
|
|
|
1848
2694
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
1849
2695
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
1850
2696
|
};
|
|
2697
|
+
this.featureFlags = {
|
|
2698
|
+
documentDecisions: config.featureFlags?.documentDecisions ?? false,
|
|
2699
|
+
authEnforcement: config.featureFlags?.authEnforcement ?? false
|
|
2700
|
+
};
|
|
2701
|
+
validateFeatureFlags(this.featureFlags, FLAG_PREREQUISITES);
|
|
2702
|
+
this.decisionModel = selectDecisionModel(this.featureFlags);
|
|
1851
2703
|
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
1852
|
-
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes);
|
|
2704
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel);
|
|
1853
2705
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
1854
2706
|
}
|
|
1855
2707
|
/**
|
|
@@ -1865,7 +2717,15 @@ var SimpleJobExecutor = class {
|
|
|
1865
2717
|
result = await this.executionScope.run(async (stores) => {
|
|
1866
2718
|
const indexTxn = stores.operationIndex.start();
|
|
1867
2719
|
if (job.kind === "load") {
|
|
1868
|
-
const loadResult = await this.executeLoadJob(
|
|
2720
|
+
const loadResult = await this.executeLoadJob({
|
|
2721
|
+
job,
|
|
2722
|
+
startTime,
|
|
2723
|
+
indexTxn,
|
|
2724
|
+
stores,
|
|
2725
|
+
signal,
|
|
2726
|
+
replayingAcceptedHistory: true,
|
|
2727
|
+
evaluatedByPosition: false
|
|
2728
|
+
});
|
|
1869
2729
|
if (loadResult.success && loadResult.operationsWithContext) {
|
|
1870
2730
|
for (const owc of loadResult.operationsWithContext) touchedCacheEntries.push({
|
|
1871
2731
|
documentId: owc.context.documentId,
|
|
@@ -1884,7 +2744,18 @@ var SimpleJobExecutor = class {
|
|
|
1884
2744
|
}
|
|
1885
2745
|
return loadResult;
|
|
1886
2746
|
}
|
|
1887
|
-
const
|
|
2747
|
+
const positioned = await this.positionByTimestamp(job, stores, signal);
|
|
2748
|
+
if (positioned.error) return buildErrorResult(job, positioned.error, startTime);
|
|
2749
|
+
const executing = {
|
|
2750
|
+
job,
|
|
2751
|
+
startTime,
|
|
2752
|
+
indexTxn,
|
|
2753
|
+
stores,
|
|
2754
|
+
signal,
|
|
2755
|
+
replayingAcceptedHistory: false,
|
|
2756
|
+
evaluatedByPosition: positioned.evaluatedByPosition
|
|
2757
|
+
};
|
|
2758
|
+
const actionResult = await this.processActions(positioned.writes, executing);
|
|
1888
2759
|
if (!actionResult.success) return {
|
|
1889
2760
|
job,
|
|
1890
2761
|
success: false,
|
|
@@ -1896,6 +2767,16 @@ var SimpleJobExecutor = class {
|
|
|
1896
2767
|
scope: owc.context.scope,
|
|
1897
2768
|
branch: owc.context.branch
|
|
1898
2769
|
});
|
|
2770
|
+
const reevaluationError = await this.reevaluateIfCriteriaMet({
|
|
2771
|
+
scope: job.scope,
|
|
2772
|
+
operations: actionResult.generatedOperations
|
|
2773
|
+
}, executing);
|
|
2774
|
+
if (reevaluationError) return {
|
|
2775
|
+
job,
|
|
2776
|
+
success: false,
|
|
2777
|
+
error: reevaluationError,
|
|
2778
|
+
duration: Date.now() - startTime
|
|
2779
|
+
};
|
|
1899
2780
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
1900
2781
|
if (actionResult.operationsWithContext.length > 0) {
|
|
1901
2782
|
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
@@ -1931,7 +2812,9 @@ var SimpleJobExecutor = class {
|
|
|
1931
2812
|
const documentIds = [...new Set(operations.map((op) => op.context.documentId))];
|
|
1932
2813
|
return stores.collectionMembershipCache.getCollectionsForDocuments(documentIds);
|
|
1933
2814
|
}
|
|
1934
|
-
async processActions(
|
|
2815
|
+
async processActions(writes, executing) {
|
|
2816
|
+
const { job, signal } = executing;
|
|
2817
|
+
const actions = writes.map((write) => write.action);
|
|
1935
2818
|
const generatedOperations = [];
|
|
1936
2819
|
const operationsWithContext = [];
|
|
1937
2820
|
try {
|
|
@@ -1948,14 +2831,11 @@ var SimpleJobExecutor = class {
|
|
|
1948
2831
|
success: false,
|
|
1949
2832
|
generatedOperations,
|
|
1950
2833
|
operationsWithContext,
|
|
1951
|
-
error:
|
|
2834
|
+
error: new InvalidOperationTimestampError(job.documentId, action.scope, action.timestampUtcMs, `action ${action.type} (id: ${action.id})`)
|
|
1952
2835
|
};
|
|
1953
2836
|
let lastYield = performance.now();
|
|
1954
|
-
for (
|
|
1955
|
-
const
|
|
1956
|
-
const skip = skipValues?.[actionIndex] ?? 0;
|
|
1957
|
-
const sourceOperation = sourceOperations?.[actionIndex];
|
|
1958
|
-
const result = documentScopeActions.includes(action.type) ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal) : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote, signal);
|
|
2837
|
+
for (const write of writes) {
|
|
2838
|
+
const result = DOCUMENT_SCOPE_ACTIONS.has(write.action.type) ? await this.documentActionHandler.execute(write, executing) : await this.executeRegularAction(write, executing);
|
|
1959
2839
|
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
1960
2840
|
if (error !== null) return {
|
|
1961
2841
|
success: false,
|
|
@@ -1980,14 +2860,44 @@ var SimpleJobExecutor = class {
|
|
|
1980
2860
|
operationsWithContext
|
|
1981
2861
|
};
|
|
1982
2862
|
}
|
|
1983
|
-
async executeRegularAction(
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2863
|
+
async executeRegularAction(write, executing) {
|
|
2864
|
+
const { action, skip, sourceOperation, sourceRemote, deniedReason } = write;
|
|
2865
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
2866
|
+
let appendCondition;
|
|
2867
|
+
let documentVersion;
|
|
2868
|
+
const alreadyEvaluated = this.featureFlags.documentDecisions && (executing.replayingAcceptedHistory || executing.evaluatedByPosition);
|
|
2869
|
+
if (this.featureFlags.documentDecisions && !alreadyEvaluated) {
|
|
2870
|
+
const target = {
|
|
2871
|
+
documentId: job.documentId,
|
|
2872
|
+
branch: job.branch
|
|
2873
|
+
};
|
|
2874
|
+
let admission;
|
|
2875
|
+
try {
|
|
2876
|
+
admission = await decideAtHead(this.decisionModel, stores.writeCache, target, {
|
|
2877
|
+
address: action.context?.signer?.user.address,
|
|
2878
|
+
key: action.context?.signer?.app.key
|
|
2879
|
+
}, {
|
|
2880
|
+
verb: "execute",
|
|
2881
|
+
scope: action.scope,
|
|
2882
|
+
operation: action.type
|
|
2883
|
+
}, signal);
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2886
|
+
}
|
|
2887
|
+
if (admission.evaluation.decision === "deny") return buildErrorResult(job, refusalError(admission.evaluation.reason, job.documentId, admission.deletedAtUtcIso, action), startTime);
|
|
2888
|
+
appendCondition = admission.appendCondition;
|
|
2889
|
+
documentVersion = admission.documentVersion;
|
|
2890
|
+
} else if (alreadyEvaluated) documentVersion = (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).state.document.version;
|
|
2891
|
+
else {
|
|
2892
|
+
let docMeta;
|
|
2893
|
+
try {
|
|
2894
|
+
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2897
|
+
}
|
|
2898
|
+
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2899
|
+
documentVersion = docMeta.state.version;
|
|
1989
2900
|
}
|
|
1990
|
-
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
1991
2901
|
if (isUndoRedo(action) || action.type === "PRUNE" || action.type === "NOOP" && skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
1992
2902
|
let document;
|
|
1993
2903
|
try {
|
|
@@ -1995,16 +2905,49 @@ var SimpleJobExecutor = class {
|
|
|
1995
2905
|
} catch (error) {
|
|
1996
2906
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1997
2907
|
}
|
|
2908
|
+
if (!this.featureFlags.authEnforcement && !executing.replayingAcceptedHistory) {
|
|
2909
|
+
const subject = {
|
|
2910
|
+
address: write.action.context?.signer?.user.address,
|
|
2911
|
+
key: write.action.context?.signer?.app.key
|
|
2912
|
+
};
|
|
2913
|
+
if (decide(document.state.auth, subject, {
|
|
2914
|
+
verb: "execute",
|
|
2915
|
+
scope: action.scope,
|
|
2916
|
+
operation: action.type
|
|
2917
|
+
}) === "deny") return buildErrorResult(job, new AuthorizationDeniedError(job.documentId, action.scope, action.type, subject.address), startTime);
|
|
2918
|
+
}
|
|
1998
2919
|
let module;
|
|
1999
2920
|
try {
|
|
2000
|
-
const moduleVersion =
|
|
2921
|
+
const moduleVersion = documentVersion === 0 ? void 0 : documentVersion;
|
|
2001
2922
|
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
2002
2923
|
} catch (error) {
|
|
2003
2924
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2004
2925
|
}
|
|
2005
2926
|
let updatedDocument;
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2927
|
+
if (deniedReason !== void 0) {
|
|
2928
|
+
const index = getNextIndexForScope(document, job.scope);
|
|
2929
|
+
const denied = createOperation(action, index, skip, {
|
|
2930
|
+
documentId: job.documentId,
|
|
2931
|
+
scope: job.scope,
|
|
2932
|
+
branch: job.branch
|
|
2933
|
+
});
|
|
2934
|
+
denied.deniedReason = deniedReason;
|
|
2935
|
+
let standing = document;
|
|
2936
|
+
if (skip > 0) try {
|
|
2937
|
+
standing = await stores.writeCache.getState(job.documentId, job.scope, job.branch, index - skip - 1, signal);
|
|
2938
|
+
} catch (error) {
|
|
2939
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2940
|
+
}
|
|
2941
|
+
denied.hash = hashDocumentStateForScope(standing, job.scope);
|
|
2942
|
+
updatedDocument = {
|
|
2943
|
+
...standing,
|
|
2944
|
+
operations: {
|
|
2945
|
+
...standing.operations,
|
|
2946
|
+
[job.scope]: [...standing.operations[job.scope] ?? [], denied]
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
} else try {
|
|
2950
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
2008
2951
|
const reducerOptions = sourceOperation ? {
|
|
2009
2952
|
skip,
|
|
2010
2953
|
branch: job.branch,
|
|
@@ -2035,14 +2978,15 @@ var SimpleJobExecutor = class {
|
|
|
2035
2978
|
try {
|
|
2036
2979
|
storedOperations = await stores.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
2037
2980
|
txn.addOperations(newOperation);
|
|
2038
|
-
}, signal);
|
|
2981
|
+
}, signal, appendCondition);
|
|
2039
2982
|
} catch (error) {
|
|
2040
2983
|
this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
|
|
2041
2984
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2985
|
+
if (AppendConditionFailedError.isError(error)) for (const stream of error.condition.streams) stores.writeCache.invalidate(stream.documentId, stream.scope, stream.branch);
|
|
2042
2986
|
return {
|
|
2043
2987
|
job,
|
|
2044
2988
|
success: false,
|
|
2045
|
-
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2989
|
+
error: AppendConditionFailedError.isError(error) ? error : /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2046
2990
|
duration: Date.now() - startTime
|
|
2047
2991
|
};
|
|
2048
2992
|
}
|
|
@@ -2051,7 +2995,7 @@ var SimpleJobExecutor = class {
|
|
|
2051
2995
|
...updatedDocument.header.revision,
|
|
2052
2996
|
[scope]: storedOperation.index + 1
|
|
2053
2997
|
};
|
|
2054
|
-
stores.writeCache.putState(job.documentId, scope, job.branch, storedOperation.index, updatedDocument);
|
|
2998
|
+
stores.writeCache.putState(job.documentId, scope, job.branch, storedOperation.index, updatedDocument, SnapshotPosition.Head);
|
|
2055
2999
|
indexTxn.write([{
|
|
2056
3000
|
...storedOperation,
|
|
2057
3001
|
documentId: job.documentId,
|
|
@@ -2078,14 +3022,238 @@ var SimpleJobExecutor = class {
|
|
|
2078
3022
|
duration: Date.now() - startTime
|
|
2079
3023
|
};
|
|
2080
3024
|
}
|
|
2081
|
-
|
|
3025
|
+
/**
|
|
3026
|
+
* Orders a write by timestamp and decides it where it lands. The caller
|
|
3027
|
+
* supplies the timestamp, so a write can belong before operations already
|
|
3028
|
+
* stored; those are re-appended alongside it, the way a load reshuffles.
|
|
3029
|
+
*
|
|
3030
|
+
* Deciding a backdated write at the stream heads instead of at its position
|
|
3031
|
+
* would overwrite the verdict every other replica computes for it.
|
|
3032
|
+
*/
|
|
3033
|
+
async positionByTimestamp(job, stores, signal) {
|
|
3034
|
+
const plain = () => ({
|
|
3035
|
+
writes: job.actions.map((action) => ({
|
|
3036
|
+
action,
|
|
3037
|
+
skip: 0,
|
|
3038
|
+
sourceRemote: ""
|
|
3039
|
+
})),
|
|
3040
|
+
evaluatedByPosition: false
|
|
3041
|
+
});
|
|
3042
|
+
if (!this.featureFlags.documentDecisions || job.actions.length === 0) return plain();
|
|
3043
|
+
let earliest = job.actions[0].timestampUtcMs;
|
|
3044
|
+
let earliestAt = Date.parse(earliest);
|
|
3045
|
+
for (const action of job.actions) {
|
|
3046
|
+
const at = Date.parse(action.timestampUtcMs);
|
|
3047
|
+
if (at < earliestAt) {
|
|
3048
|
+
earliest = action.timestampUtcMs;
|
|
3049
|
+
earliestAt = at;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
|
|
3053
|
+
const backdated = earliestAt < Date.parse(revisions.latestTimestamp);
|
|
3054
|
+
if (this.featureFlags.authEnforcement && job.scope === "auth") {
|
|
3055
|
+
const newest = await stores.operationStore.getStreamLatestTimestamp(job.documentId, "auth", job.branch, signal);
|
|
3056
|
+
const violation = this.firstNonMonotonicTimestamp(job.actions, newest, job.documentId, job.branch);
|
|
3057
|
+
if (violation) return {
|
|
3058
|
+
writes: [],
|
|
3059
|
+
evaluatedByPosition: false,
|
|
3060
|
+
error: violation
|
|
3061
|
+
};
|
|
3062
|
+
if (!backdated) return plain();
|
|
3063
|
+
return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
|
|
3064
|
+
}
|
|
3065
|
+
if (!backdated) return plain();
|
|
3066
|
+
const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
|
|
3067
|
+
if (conflicting.length === 0) {
|
|
3068
|
+
if (!this.featureFlags.authEnforcement) return plain();
|
|
3069
|
+
return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
|
|
3070
|
+
}
|
|
3071
|
+
const nextIndex = revisions.revision[job.scope] ?? 0;
|
|
3072
|
+
let firstConflicting = conflicting[0].index;
|
|
3073
|
+
for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
|
|
3074
|
+
const incoming = job.actions.map((action, i) => ({
|
|
3075
|
+
id: action.id,
|
|
3076
|
+
index: nextIndex + i,
|
|
3077
|
+
skip: 0,
|
|
3078
|
+
hash: "",
|
|
3079
|
+
timestampUtcMs: action.timestampUtcMs,
|
|
3080
|
+
action
|
|
3081
|
+
}));
|
|
3082
|
+
const merged = reshuffleByTimestamp({
|
|
3083
|
+
index: nextIndex,
|
|
3084
|
+
skip: retractionSkip(nextIndex, firstConflicting)
|
|
3085
|
+
}, conflicting, incoming);
|
|
3086
|
+
stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3087
|
+
if (!this.featureFlags.authEnforcement) return {
|
|
3088
|
+
writes: merged.map((operation) => ({
|
|
3089
|
+
action: operation.action,
|
|
3090
|
+
skip: operation.skip,
|
|
3091
|
+
sourceRemote: ""
|
|
3092
|
+
})),
|
|
3093
|
+
evaluatedByPosition: false
|
|
3094
|
+
};
|
|
3095
|
+
return this.evaluatePositioned(job, stores, merged, signal);
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Decides each operation where it lands and carries the verdict on it. A
|
|
3099
|
+
* refused submitted action is reported to the caller and nothing is stored; a
|
|
3100
|
+
* refused operation the reshuffle merely moved keeps its verdict, because it
|
|
3101
|
+
* already holds a position.
|
|
3102
|
+
*
|
|
3103
|
+
* The operations carry the indexes and skips they will be stored at, because
|
|
3104
|
+
* the walk resolves skips before it orders them.
|
|
3105
|
+
*/
|
|
3106
|
+
async evaluatePositioned(job, stores, operations, signal) {
|
|
3107
|
+
const reasons = await evaluateByPosition(this.decisionModel, {
|
|
3108
|
+
documentId: job.documentId,
|
|
3109
|
+
branch: job.branch
|
|
3110
|
+
}, {
|
|
3111
|
+
scope: job.scope,
|
|
3112
|
+
operations
|
|
3113
|
+
}, stores, signal);
|
|
3114
|
+
const submitted = new Set(job.actions.map((action) => action.id));
|
|
3115
|
+
for (let i = 0; i < operations.length; i++) {
|
|
3116
|
+
const reason = reasons[i];
|
|
3117
|
+
if (reason !== void 0 && submitted.has(operations[i].action.id)) return {
|
|
3118
|
+
writes: [],
|
|
3119
|
+
evaluatedByPosition: false,
|
|
3120
|
+
error: refusalError(reason, job.documentId, null, operations[i].action)
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
return {
|
|
3124
|
+
writes: operations.map((operation, i) => ({
|
|
3125
|
+
action: operation.action,
|
|
3126
|
+
skip: operation.skip,
|
|
3127
|
+
sourceRemote: "",
|
|
3128
|
+
deniedReason: reasons[i]
|
|
3129
|
+
})),
|
|
3130
|
+
evaluatedByPosition: true
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* The scopes a re-evaluation pass visits, in a fixed order.
|
|
3135
|
+
*
|
|
3136
|
+
* The revisions map comes from a query with no ORDER BY, and the order is
|
|
3137
|
+
* load-bearing: each scope's pass re-reads the auth stream, and the walk skips
|
|
3138
|
+
* an operation by its stored denial, so a denial this pass just wrote is
|
|
3139
|
+
* visible to a later-visited scope and invisible to an earlier one. The model's
|
|
3140
|
+
* own projection order leads, then the rest sorted, so the pass is reproducible
|
|
3141
|
+
* across replicas and across runs.
|
|
3142
|
+
*/
|
|
3143
|
+
evaluationOrder(target, revision) {
|
|
3144
|
+
const definition = this.decisionModel(target);
|
|
3145
|
+
const evaluated = Object.keys(revision).filter((scope) => definition.evaluatesScope(scope));
|
|
3146
|
+
const leading = [];
|
|
3147
|
+
for (const stream of staticReadSet(definition)) {
|
|
3148
|
+
const scope = stream.query.scope;
|
|
3149
|
+
if (evaluated.includes(scope) && !leading.includes(scope)) leading.push(scope);
|
|
3150
|
+
}
|
|
3151
|
+
const rest = evaluated.filter((scope) => !leading.includes(scope)).sort((a, b) => a.localeCompare(b));
|
|
3152
|
+
return [...leading, ...rest];
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* The first timestamp in the batch that does not strictly exceed everything
|
|
3156
|
+
* ahead of it, or undefined when the whole batch is monotonic.
|
|
3157
|
+
*
|
|
3158
|
+
* The bound is carried forward rather than compared against one stored maximum,
|
|
3159
|
+
* because a single execute can carry several auth actions stamped in the same
|
|
3160
|
+
* millisecond. Letting a tie through would store a stream the position walk
|
|
3161
|
+
* then refuses to read, with no repair path.
|
|
3162
|
+
*/
|
|
3163
|
+
firstNonMonotonicTimestamp(entries, newest, documentId, branch) {
|
|
3164
|
+
let boundIso = newest;
|
|
3165
|
+
let bound = newest === void 0 ? Number.NEGATIVE_INFINITY : Date.parse(newest);
|
|
3166
|
+
for (const entry of entries) {
|
|
3167
|
+
if (!isValidISOTimestamp(entry.timestampUtcMs)) return new InvalidOperationTimestampError(documentId, "auth", entry.timestampUtcMs, "auth operation");
|
|
3168
|
+
const at = Date.parse(entry.timestampUtcMs);
|
|
3169
|
+
if (boundIso !== void 0 && at <= bound) return new AuthTimestampNotMonotonicError(documentId, branch, entry.timestampUtcMs, boundIso);
|
|
3170
|
+
bound = at;
|
|
3171
|
+
boundIso = entry.timestampUtcMs;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
/** The operations a batch of submitted actions appends at the scope's tail. */
|
|
3175
|
+
appendedOperations(job, nextIndex) {
|
|
3176
|
+
return job.actions.map((action, i) => ({
|
|
3177
|
+
id: action.id,
|
|
3178
|
+
index: nextIndex + i,
|
|
3179
|
+
skip: 0,
|
|
3180
|
+
hash: "",
|
|
3181
|
+
timestampUtcMs: action.timestampUtcMs,
|
|
3182
|
+
action
|
|
3183
|
+
}));
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Re-evaluates the document when a write meets both criteria: it was written
|
|
3187
|
+
* to a stream the model reads, and it is timestamped before an operation
|
|
3188
|
+
* already stored. The caller supplies the timestamp and the reactor does not replace
|
|
3189
|
+
* it, so a mutation job can write such an operation just as a load job can,
|
|
3190
|
+
* which is why both executeJob and executeLoadJob call this.
|
|
3191
|
+
*/
|
|
3192
|
+
async reevaluateIfCriteriaMet(criteria, executing) {
|
|
3193
|
+
if (!this.featureFlags.documentDecisions) return;
|
|
3194
|
+
const { job, stores, signal } = executing;
|
|
3195
|
+
const target = {
|
|
3196
|
+
documentId: job.documentId,
|
|
3197
|
+
branch: job.branch
|
|
3198
|
+
};
|
|
3199
|
+
if (!staticReadSet(this.decisionModel(target)).some((stream) => stream.query.documentId === job.documentId && stream.query.scope === criteria.scope && stream.query.branch === job.branch)) return;
|
|
3200
|
+
const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
|
|
3201
|
+
const latest = Date.parse(revisions.latestTimestamp);
|
|
3202
|
+
if (!criteria.operations.some((operation) => Date.parse(operation.timestampUtcMs) < latest)) return;
|
|
3203
|
+
return this.reevaluateDocument(executing);
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Re-evaluates every scope the model evaluates. Where an operation's
|
|
3207
|
+
* evaluation differs from what is stored, the tail from that operation is
|
|
3208
|
+
* re-appended, carrying a skip that spans the indices it supersedes.
|
|
3209
|
+
*/
|
|
3210
|
+
async reevaluateDocument(executing) {
|
|
3211
|
+
const { job, stores, signal } = executing;
|
|
3212
|
+
const target = {
|
|
3213
|
+
documentId: job.documentId,
|
|
3214
|
+
branch: job.branch
|
|
3215
|
+
};
|
|
3216
|
+
const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
|
|
3217
|
+
for (const scope of this.evaluationOrder(target, revisions.revision)) {
|
|
3218
|
+
const stored = (await stores.operationStore.getSince(job.documentId, scope, job.branch, -1, void 0, void 0, signal)).results;
|
|
3219
|
+
const effective = garbageCollect(sortOperations([...stored]));
|
|
3220
|
+
if (effective.length === 0) continue;
|
|
3221
|
+
const reevaluated = await evaluateByPosition(this.decisionModel, target, {
|
|
3222
|
+
scope,
|
|
3223
|
+
operations: effective
|
|
3224
|
+
}, stores, signal);
|
|
3225
|
+
const firstChange = effective.findIndex((operation, i) => operation.deniedReason !== reevaluated[i]);
|
|
3226
|
+
if (firstChange === -1) continue;
|
|
3227
|
+
const tail = effective.slice(firstChange);
|
|
3228
|
+
const nextIndex = revisions.revision[scope];
|
|
3229
|
+
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
3230
|
+
const result = await this.processActions(tail.map((operation, i) => ({
|
|
3231
|
+
action: operation.action,
|
|
3232
|
+
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
|
|
3233
|
+
sourceRemote: "",
|
|
3234
|
+
deniedReason: reevaluated[firstChange + i]
|
|
3235
|
+
})), {
|
|
3236
|
+
...executing,
|
|
3237
|
+
job: {
|
|
3238
|
+
...job,
|
|
3239
|
+
scope
|
|
3240
|
+
},
|
|
3241
|
+
replayingAcceptedHistory: true,
|
|
3242
|
+
evaluatedByPosition: true
|
|
3243
|
+
});
|
|
3244
|
+
if (!result.success) return result.error ?? /* @__PURE__ */ new Error(`Re-evaluation of ${job.documentId} ${scope} failed`);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
async executeLoadJob(executing) {
|
|
3248
|
+
const { job, startTime, indexTxn, stores, signal } = executing;
|
|
2082
3249
|
if (job.operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("Load job must include at least one operation"), startTime);
|
|
2083
3250
|
let docMeta;
|
|
2084
3251
|
try {
|
|
2085
3252
|
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
2086
3253
|
} catch {}
|
|
2087
|
-
if (docMeta?.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3254
|
+
if (docMeta?.state.isDeleted && !this.featureFlags.documentDecisions) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2088
3255
|
const scope = job.scope;
|
|
3256
|
+
const monotonicAuthStream = this.featureFlags.authEnforcement && scope === "auth";
|
|
2089
3257
|
let latestRevision;
|
|
2090
3258
|
try {
|
|
2091
3259
|
latestRevision = (await stores.operationStore.getRevisions(job.documentId, job.branch, signal)).revision[scope] ?? 0;
|
|
@@ -2095,7 +3263,7 @@ var SimpleJobExecutor = class {
|
|
|
2095
3263
|
for (const operation of job.operations) if (operation.timestampUtcMs && !isValidISOTimestamp(operation.timestampUtcMs)) return {
|
|
2096
3264
|
job,
|
|
2097
3265
|
success: false,
|
|
2098
|
-
error:
|
|
3266
|
+
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
2099
3267
|
duration: Date.now() - startTime
|
|
2100
3268
|
};
|
|
2101
3269
|
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
@@ -2103,7 +3271,7 @@ var SimpleJobExecutor = class {
|
|
|
2103
3271
|
for (const operation of job.operations) {
|
|
2104
3272
|
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
2105
3273
|
const ts = operation.timestampUtcMs || "";
|
|
2106
|
-
if (ts < minIncomingTimestamp) minIncomingTimestamp = ts;
|
|
3274
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
2107
3275
|
}
|
|
2108
3276
|
let conflictingOps;
|
|
2109
3277
|
try {
|
|
@@ -2128,11 +3296,14 @@ var SimpleJobExecutor = class {
|
|
|
2128
3296
|
}
|
|
2129
3297
|
return true;
|
|
2130
3298
|
});
|
|
2131
|
-
const existingOpsToReshuffle = nonSupersededOps;
|
|
2132
|
-
|
|
3299
|
+
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
3300
|
+
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
3301
|
+
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
3302
|
+
const reshuffleCost = existingOpsToReshuffle.filter((operation) => (actionIdCounts.get(operation.action.id) ?? 0) < 2).length;
|
|
3303
|
+
if (reshuffleCost > this.config.maxSkipThreshold) return {
|
|
2133
3304
|
job,
|
|
2134
3305
|
success: false,
|
|
2135
|
-
error:
|
|
3306
|
+
error: new ExcessiveReshuffleError(job.documentId, scope, reshuffleCost, this.config.maxSkipThreshold),
|
|
2136
3307
|
duration: Date.now() - startTime
|
|
2137
3308
|
};
|
|
2138
3309
|
let skipCount = existingOpsToReshuffle.length;
|
|
@@ -2160,6 +3331,16 @@ var SimpleJobExecutor = class {
|
|
|
2160
3331
|
operationsWithContext: [],
|
|
2161
3332
|
duration: Date.now() - startTime
|
|
2162
3333
|
};
|
|
3334
|
+
if (monotonicAuthStream) {
|
|
3335
|
+
const newest = await stores.operationStore.getStreamLatestTimestamp(job.documentId, "auth", job.branch, signal);
|
|
3336
|
+
const violation = this.firstNonMonotonicTimestamp([...incomingOpsToApply].sort((a, b) => a.index - b.index), newest, job.documentId, job.branch);
|
|
3337
|
+
if (violation) return {
|
|
3338
|
+
job,
|
|
3339
|
+
success: false,
|
|
3340
|
+
error: violation,
|
|
3341
|
+
duration: Date.now() - startTime
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
2163
3344
|
const reshuffledOperations = existingOpsToReshuffle.length === 0 && skipCount === 0 ? incomingOpsToApply.slice().sort((a, b) => a.index - b.index).map((operation, i) => ({
|
|
2164
3345
|
...operation,
|
|
2165
3346
|
index: latestRevision + i
|
|
@@ -2171,10 +3352,31 @@ var SimpleJobExecutor = class {
|
|
|
2171
3352
|
id: operation.id
|
|
2172
3353
|
})));
|
|
2173
3354
|
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP") operation.skip = 1;
|
|
2174
|
-
|
|
2175
|
-
|
|
3355
|
+
let deniedReasons;
|
|
3356
|
+
if (this.featureFlags.documentDecisions) try {
|
|
3357
|
+
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
3358
|
+
documentId: job.documentId,
|
|
3359
|
+
branch: job.branch
|
|
3360
|
+
}, {
|
|
3361
|
+
scope,
|
|
3362
|
+
operations: reshuffledOperations
|
|
3363
|
+
}, stores, signal);
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
return {
|
|
3366
|
+
job,
|
|
3367
|
+
success: false,
|
|
3368
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
3369
|
+
duration: Date.now() - startTime
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
2176
3372
|
const effectiveSourceRemote = skipCount > 0 ? "" : job.meta.sourceRemote || "";
|
|
2177
|
-
const result = await this.processActions(
|
|
3373
|
+
const result = await this.processActions(reshuffledOperations.map((operation, i) => ({
|
|
3374
|
+
action: operation.action,
|
|
3375
|
+
skip: operation.skip,
|
|
3376
|
+
sourceOperation: operation,
|
|
3377
|
+
sourceRemote: effectiveSourceRemote,
|
|
3378
|
+
deniedReason: deniedReasons?.[i]
|
|
3379
|
+
})), executing);
|
|
2178
3380
|
if (!result.success) return {
|
|
2179
3381
|
job,
|
|
2180
3382
|
success: false,
|
|
@@ -2183,6 +3385,16 @@ var SimpleJobExecutor = class {
|
|
|
2183
3385
|
};
|
|
2184
3386
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2185
3387
|
if (scope === "document") stores.documentMetaCache.invalidate(job.documentId, job.branch);
|
|
3388
|
+
const reevaluationError = await this.reevaluateIfCriteriaMet({
|
|
3389
|
+
scope,
|
|
3390
|
+
operations: result.generatedOperations
|
|
3391
|
+
}, executing);
|
|
3392
|
+
if (reevaluationError) return {
|
|
3393
|
+
job,
|
|
3394
|
+
success: false,
|
|
3395
|
+
error: reevaluationError,
|
|
3396
|
+
duration: Date.now() - startTime
|
|
3397
|
+
};
|
|
2186
3398
|
return {
|
|
2187
3399
|
job,
|
|
2188
3400
|
success: true,
|
|
@@ -2419,36 +3631,6 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
2419
3631
|
};
|
|
2420
3632
|
}
|
|
2421
3633
|
//#endregion
|
|
2422
|
-
//#region src/storage/interfaces.ts
|
|
2423
|
-
/**
|
|
2424
|
-
* Thrown when an operation with the same identity already exists in the store.
|
|
2425
|
-
*/
|
|
2426
|
-
var DuplicateOperationError = class extends Error {
|
|
2427
|
-
constructor(description) {
|
|
2428
|
-
super(`Duplicate operation: ${description}`);
|
|
2429
|
-
this.name = "DuplicateOperationError";
|
|
2430
|
-
}
|
|
2431
|
-
};
|
|
2432
|
-
/**
|
|
2433
|
-
* Thrown when a concurrent write conflict is detected during an atomic apply.
|
|
2434
|
-
*/
|
|
2435
|
-
var OptimisticLockError = class extends Error {
|
|
2436
|
-
constructor(message) {
|
|
2437
|
-
super(message);
|
|
2438
|
-
this.name = "OptimisticLockError";
|
|
2439
|
-
}
|
|
2440
|
-
};
|
|
2441
|
-
/**
|
|
2442
|
-
* Thrown when the caller-provided revision does not match the current
|
|
2443
|
-
* stored revision, indicating a stale read.
|
|
2444
|
-
*/
|
|
2445
|
-
var RevisionMismatchError = class extends Error {
|
|
2446
|
-
constructor(expected, actual) {
|
|
2447
|
-
super(`Revision mismatch: expected ${expected}, got ${actual}`);
|
|
2448
|
-
this.name = "RevisionMismatchError";
|
|
2449
|
-
}
|
|
2450
|
-
};
|
|
2451
|
-
//#endregion
|
|
2452
3634
|
//#region src/storage/txn.ts
|
|
2453
3635
|
var AtomicTransaction = class {
|
|
2454
3636
|
operations = [];
|
|
@@ -2473,6 +3655,7 @@ var AtomicTransaction = class {
|
|
|
2473
3655
|
action: JSON.stringify(op.action),
|
|
2474
3656
|
skip: op.skip,
|
|
2475
3657
|
error: op.error || null,
|
|
3658
|
+
deniedReason: op.deniedReason || null,
|
|
2476
3659
|
hash: op.hash
|
|
2477
3660
|
});
|
|
2478
3661
|
}
|
|
@@ -2506,12 +3689,12 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2506
3689
|
instance.trx = trx;
|
|
2507
3690
|
return instance;
|
|
2508
3691
|
}
|
|
2509
|
-
async apply(documentId, documentType, scope, branch, revision, fn, signal) {
|
|
3692
|
+
async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
2510
3693
|
if (this.trx) {
|
|
2511
3694
|
let executeResult = null;
|
|
2512
3695
|
let uniqueCtx = null;
|
|
2513
3696
|
try {
|
|
2514
|
-
executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
3697
|
+
executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
|
|
2515
3698
|
} catch (error) {
|
|
2516
3699
|
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
2517
3700
|
else throw error;
|
|
@@ -2523,7 +3706,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2523
3706
|
let uniqueCtx = null;
|
|
2524
3707
|
try {
|
|
2525
3708
|
transactionResult = await this.db.transaction().execute(async (trx) => {
|
|
2526
|
-
return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
3709
|
+
return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
|
|
2527
3710
|
});
|
|
2528
3711
|
} catch (error) {
|
|
2529
3712
|
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
@@ -2542,12 +3725,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2542
3725
|
const op = ctx.stagedOps[0];
|
|
2543
3726
|
throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
|
|
2544
3727
|
}
|
|
2545
|
-
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
|
|
3728
|
+
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
2546
3729
|
throwIfAborted(signal);
|
|
2547
3730
|
const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
|
|
2548
3731
|
await fn(atomicTxn);
|
|
2549
3732
|
const operations = atomicTxn.getOperations();
|
|
2550
3733
|
if (operations.length === 0) return [];
|
|
3734
|
+
if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
|
|
2551
3735
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
2552
3736
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
2553
3737
|
if (currentRevision !== revision - 1) {
|
|
@@ -2563,22 +3747,91 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2563
3747
|
op.prevOpId = prevOpId;
|
|
2564
3748
|
prevOpId = op.opId;
|
|
2565
3749
|
}
|
|
3750
|
+
let insertedCount = operations.length;
|
|
2566
3751
|
try {
|
|
2567
|
-
|
|
3752
|
+
if (condition && condition.streams.length > 0) insertedCount = await this.insertGuarded(trx, operations, condition);
|
|
3753
|
+
else await trx.insertInto("Operation").values(operations).execute();
|
|
2568
3754
|
} catch (error) {
|
|
2569
3755
|
if (error instanceof Error && error.message.includes("unique constraint")) throw new _UniqueConstraintContext(documentId, scope, branch, revision, operations);
|
|
2570
3756
|
throw error;
|
|
2571
3757
|
}
|
|
3758
|
+
if (insertedCount !== operations.length) throw new AppendConditionFailedError(condition);
|
|
2572
3759
|
return operations.map((op) => ({
|
|
2573
3760
|
index: op.index,
|
|
2574
3761
|
timestampUtcMs: op.timestampUtcMs.toISOString(),
|
|
2575
3762
|
hash: op.hash,
|
|
2576
3763
|
skip: op.skip,
|
|
2577
3764
|
error: op.error || void 0,
|
|
3765
|
+
deniedReason: op.deniedReason || void 0,
|
|
2578
3766
|
id: op.opId,
|
|
2579
3767
|
action: JSON.parse(op.action)
|
|
2580
3768
|
}));
|
|
2581
3769
|
}
|
|
3770
|
+
/**
|
|
3771
|
+
* Locks the written stream and every read-set stream, in sorted key order
|
|
3772
|
+
* so that overlapping concurrent appends serialize rather than deadlock.
|
|
3773
|
+
* The locks are still taken one row at a time, so the query preserves that
|
|
3774
|
+
* order. It must stay separate from the guarded insert, which would
|
|
3775
|
+
* otherwise read a snapshot taken before the locks were held.
|
|
3776
|
+
*/
|
|
3777
|
+
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
3778
|
+
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
3779
|
+
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
3780
|
+
await sql`
|
|
3781
|
+
with ordered as materialized (
|
|
3782
|
+
select key
|
|
3783
|
+
from unnest(array[${sql.join([...keys].sort())}]::text[]) with ordinality as t(key, ord)
|
|
3784
|
+
order by ord
|
|
3785
|
+
)
|
|
3786
|
+
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
3787
|
+
`.execute(trx);
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Inserts the staged operations with the condition compiled in as a WHERE
|
|
3791
|
+
* NOT EXISTS guard, making the check and the append one statement. Returns
|
|
3792
|
+
* the rows inserted; zero means the guard failed and nothing was written.
|
|
3793
|
+
*/
|
|
3794
|
+
async insertGuarded(trx, operations, condition) {
|
|
3795
|
+
const branches = operations.map((op) => trx.selectNoFrom([
|
|
3796
|
+
sql`${op.jobId}::text`.as("jobId"),
|
|
3797
|
+
sql`${op.opId}::text`.as("opId"),
|
|
3798
|
+
sql`${op.prevOpId}::text`.as("prevOpId"),
|
|
3799
|
+
sql`${op.documentId}::text`.as("documentId"),
|
|
3800
|
+
sql`${op.documentType}::text`.as("documentType"),
|
|
3801
|
+
sql`${op.scope}::text`.as("scope"),
|
|
3802
|
+
sql`${op.branch}::text`.as("branch"),
|
|
3803
|
+
sql`${op.timestampUtcMs}::timestamptz`.as("timestampUtcMs"),
|
|
3804
|
+
sql`${op.index}::integer`.as("index"),
|
|
3805
|
+
sql`${op.action}::jsonb`.as("action"),
|
|
3806
|
+
sql`${op.skip}::integer`.as("skip"),
|
|
3807
|
+
sql`${op.error ?? null}::text`.as("error"),
|
|
3808
|
+
sql`${op.deniedReason ?? null}::text`.as("deniedReason"),
|
|
3809
|
+
sql`${op.hash}::text`.as("hash")
|
|
3810
|
+
]).where((eb) => eb.not(eb.exists(eb.selectFrom("Operation").select("Operation.id").where((web) => web.or(condition.streams.map((s) => web.and([
|
|
3811
|
+
web("Operation.documentId", "=", s.documentId),
|
|
3812
|
+
web("Operation.scope", "=", s.scope),
|
|
3813
|
+
web("Operation.branch", "=", s.branch),
|
|
3814
|
+
web("Operation.index", ">", s.revision)
|
|
3815
|
+
]))))))));
|
|
3816
|
+
let expression = branches[0];
|
|
3817
|
+
for (let i = 1; i < branches.length; i++) expression = expression.unionAll(branches[i]);
|
|
3818
|
+
return (await trx.insertInto("Operation").columns([
|
|
3819
|
+
"jobId",
|
|
3820
|
+
"opId",
|
|
3821
|
+
"prevOpId",
|
|
3822
|
+
"documentId",
|
|
3823
|
+
"documentType",
|
|
3824
|
+
"scope",
|
|
3825
|
+
"branch",
|
|
3826
|
+
"timestampUtcMs",
|
|
3827
|
+
"index",
|
|
3828
|
+
"action",
|
|
3829
|
+
"skip",
|
|
3830
|
+
"error",
|
|
3831
|
+
"deniedReason",
|
|
3832
|
+
"hash"
|
|
3833
|
+
]).expression(expression).returning("id").execute()).length;
|
|
3834
|
+
}
|
|
2582
3835
|
async findIdempotentReplay(executor, documentId, scope, branch, revision, stagedOps) {
|
|
2583
3836
|
const minIndex = revision;
|
|
2584
3837
|
const maxIndex = revision + stagedOps.length - 1;
|
|
@@ -2646,18 +3899,18 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2646
3899
|
"o1.index",
|
|
2647
3900
|
"o1.timestampUtcMs"
|
|
2648
3901
|
]).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();
|
|
3902
|
+
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
|
|
2649
3903
|
const revision = {};
|
|
2650
|
-
|
|
2651
|
-
for (const row of scopeRevisions) {
|
|
2652
|
-
revision[row.scope] = row.index + 1;
|
|
2653
|
-
const timestamp = row.timestampUtcMs.toISOString();
|
|
2654
|
-
if (timestamp > latestTimestamp) latestTimestamp = timestamp;
|
|
2655
|
-
}
|
|
3904
|
+
for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
|
|
2656
3905
|
return {
|
|
2657
3906
|
revision,
|
|
2658
|
-
latestTimestamp
|
|
3907
|
+
latestTimestamp: latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
2659
3908
|
};
|
|
2660
3909
|
}
|
|
3910
|
+
async getStreamLatestTimestamp(documentId, scope, branch, signal) {
|
|
3911
|
+
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();
|
|
3912
|
+
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
3913
|
+
}
|
|
2661
3914
|
rowToOperation(row) {
|
|
2662
3915
|
return {
|
|
2663
3916
|
index: row.index,
|
|
@@ -2665,6 +3918,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2665
3918
|
hash: row.hash,
|
|
2666
3919
|
skip: row.skip,
|
|
2667
3920
|
error: row.error || void 0,
|
|
3921
|
+
deniedReason: row.deniedReason || void 0,
|
|
2668
3922
|
id: row.opId,
|
|
2669
3923
|
action: row.action
|
|
2670
3924
|
};
|
|
@@ -2750,8 +4004,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
2750
4004
|
}
|
|
2751
4005
|
//#endregion
|
|
2752
4006
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
2753
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2754
|
-
async function up$
|
|
4007
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
4008
|
+
async function up$15(db) {
|
|
2755
4009
|
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", [
|
|
2756
4010
|
"documentId",
|
|
2757
4011
|
"scope",
|
|
@@ -2776,8 +4030,8 @@ async function up$13(db) {
|
|
|
2776
4030
|
}
|
|
2777
4031
|
//#endregion
|
|
2778
4032
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
2779
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2780
|
-
async function up$
|
|
4033
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
4034
|
+
async function up$14(db) {
|
|
2781
4035
|
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", [
|
|
2782
4036
|
"documentId",
|
|
2783
4037
|
"scope",
|
|
@@ -2793,14 +4047,14 @@ async function up$12(db) {
|
|
|
2793
4047
|
}
|
|
2794
4048
|
//#endregion
|
|
2795
4049
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
2796
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2797
|
-
async function up$
|
|
4050
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
4051
|
+
async function up$13(db) {
|
|
2798
4052
|
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();
|
|
2799
4053
|
}
|
|
2800
4054
|
//#endregion
|
|
2801
4055
|
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
2802
|
-
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2803
|
-
async function up$
|
|
4056
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
4057
|
+
async function up$12(db) {
|
|
2804
4058
|
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", [
|
|
2805
4059
|
"sourceId",
|
|
2806
4060
|
"targetId",
|
|
@@ -2812,14 +4066,14 @@ async function up$10(db) {
|
|
|
2812
4066
|
}
|
|
2813
4067
|
//#endregion
|
|
2814
4068
|
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
2815
|
-
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2816
|
-
async function up$
|
|
4069
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
|
|
4070
|
+
async function up$11(db) {
|
|
2817
4071
|
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();
|
|
2818
4072
|
}
|
|
2819
4073
|
//#endregion
|
|
2820
4074
|
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
2821
|
-
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2822
|
-
async function up$
|
|
4075
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
|
|
4076
|
+
async function up$10(db) {
|
|
2823
4077
|
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", [
|
|
2824
4078
|
"documentId",
|
|
2825
4079
|
"scope",
|
|
@@ -2840,8 +4094,8 @@ async function up$8(db) {
|
|
|
2840
4094
|
}
|
|
2841
4095
|
//#endregion
|
|
2842
4096
|
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
2843
|
-
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2844
|
-
async function up$
|
|
4097
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
|
|
4098
|
+
async function up$9(db) {
|
|
2845
4099
|
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", [
|
|
2846
4100
|
"documentId",
|
|
2847
4101
|
"scope",
|
|
@@ -2851,14 +4105,14 @@ async function up$7(db) {
|
|
|
2851
4105
|
}
|
|
2852
4106
|
//#endregion
|
|
2853
4107
|
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
2854
|
-
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2855
|
-
async function up$
|
|
4108
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
|
|
4109
|
+
async function up$8(db) {
|
|
2856
4110
|
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();
|
|
2857
4111
|
}
|
|
2858
4112
|
//#endregion
|
|
2859
4113
|
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
2860
|
-
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2861
|
-
async function up$
|
|
4114
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
|
|
4115
|
+
async function up$7(db) {
|
|
2862
4116
|
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();
|
|
2863
4117
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
2864
4118
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -2872,8 +4126,8 @@ async function up$5(db) {
|
|
|
2872
4126
|
}
|
|
2873
4127
|
//#endregion
|
|
2874
4128
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
2875
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2876
|
-
async function up$
|
|
4129
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
|
|
4130
|
+
async function up$6(db) {
|
|
2877
4131
|
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();
|
|
2878
4132
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
2879
4133
|
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();
|
|
@@ -2881,8 +4135,8 @@ async function up$4(db) {
|
|
|
2881
4135
|
}
|
|
2882
4136
|
//#endregion
|
|
2883
4137
|
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
2884
|
-
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2885
|
-
async function up$
|
|
4138
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
|
|
4139
|
+
async function up$5(db) {
|
|
2886
4140
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
2887
4141
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
2888
4142
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -2891,24 +4145,60 @@ async function up$3(db) {
|
|
|
2891
4145
|
}
|
|
2892
4146
|
//#endregion
|
|
2893
4147
|
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
2894
|
-
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2895
|
-
async function up$
|
|
4148
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
|
|
4149
|
+
async function up$4(db) {
|
|
2896
4150
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
2897
4151
|
}
|
|
2898
4152
|
//#endregion
|
|
2899
4153
|
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
2900
|
-
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2901
|
-
async function up$
|
|
4154
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
|
|
4155
|
+
async function up$3(db) {
|
|
2902
4156
|
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();
|
|
2903
4157
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
2904
4158
|
}
|
|
2905
4159
|
//#endregion
|
|
2906
4160
|
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
2907
|
-
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up });
|
|
2908
|
-
async function up(db) {
|
|
4161
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
|
|
4162
|
+
async function up$2(db) {
|
|
2909
4163
|
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();
|
|
2910
4164
|
}
|
|
2911
4165
|
//#endregion
|
|
4166
|
+
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
4167
|
+
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
4168
|
+
down: () => down$1,
|
|
4169
|
+
up: () => up$1
|
|
4170
|
+
});
|
|
4171
|
+
/**
|
|
4172
|
+
* Records why authorization refused an operation. Separate from `error` so a
|
|
4173
|
+
* denial is distinguishable from a reducer failure without matching on a
|
|
4174
|
+
* message. Null for every operation written before decisions were enforced.
|
|
4175
|
+
*/
|
|
4176
|
+
async function up$1(db) {
|
|
4177
|
+
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
4178
|
+
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
4179
|
+
}
|
|
4180
|
+
async function down$1(db) {
|
|
4181
|
+
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
4182
|
+
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
4183
|
+
}
|
|
4184
|
+
//#endregion
|
|
4185
|
+
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
4186
|
+
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
4187
|
+
down: () => down,
|
|
4188
|
+
up: () => up
|
|
4189
|
+
});
|
|
4190
|
+
/**
|
|
4191
|
+
* The classification a dead letter falls into, stored because it decides whether
|
|
4192
|
+
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
4193
|
+
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
4194
|
+
*/
|
|
4195
|
+
async function up(db) {
|
|
4196
|
+
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
4197
|
+
}
|
|
4198
|
+
async function down(db) {
|
|
4199
|
+
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
4200
|
+
}
|
|
4201
|
+
//#endregion
|
|
2912
4202
|
//#region src/storage/migrations/migrator.ts
|
|
2913
4203
|
const REACTOR_SCHEMA = "reactor";
|
|
2914
4204
|
const migrations = {
|
|
@@ -2925,7 +4215,9 @@ const migrations = {
|
|
|
2925
4215
|
"011_add_cursor_type_column": _011_add_cursor_type_column_exports,
|
|
2926
4216
|
"012_add_source_remote_column": _012_add_source_remote_column_exports,
|
|
2927
4217
|
"013_create_sync_dead_letters_table": _013_create_sync_dead_letters_table_exports,
|
|
2928
|
-
"014_create_processor_cursor_table": _014_create_processor_cursor_table_exports
|
|
4218
|
+
"014_create_processor_cursor_table": _014_create_processor_cursor_table_exports,
|
|
4219
|
+
"015_add_operation_denied_reason": _015_add_operation_denied_reason_exports,
|
|
4220
|
+
"016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports
|
|
2929
4221
|
};
|
|
2930
4222
|
var ProgrammaticMigrationProvider = class {
|
|
2931
4223
|
getMigrations() {
|
|
@@ -2979,6 +4271,6 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
2979
4271
|
//#region src/core/drive-container-types.ts
|
|
2980
4272
|
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
2981
4273
|
//#endregion
|
|
2982
|
-
export {
|
|
4274
|
+
export { OptimisticLockError as A, ExcessiveReshuffleError as B, DocumentMetaCache as C, APPEND_CONDITION_FAILED_PREFIX as D, CollectionMembershipCache as E, ModuleNotFoundError as F, __exportAll as G, matchesScope as H, AuthTimestampNotMonotonicError as I, AuthorizationDeniedError as L, DuplicateManifestError as M, DuplicateModuleError as N, AppendConditionFailedError as O, InvalidModuleError as P, DocumentDeletedError as R, KyselyOperationIndex as S, createEmptyConsistencyToken as T, parsePagingOptions as U, InvalidOperationTimestampError as V, throwIfAborted as W, KyselyExecutionScope as _, createForwardingPoolInstrumentation as a, EventBus as b, KyselyKeyframeStore as c, DriveCollectionId as d, decideAtHead as f, authDecisionModel as g, buildDecisionModel as h, runMigrations as i, RevisionMismatchError as j, DuplicateOperationError as k, DocumentModelRegistry as l, documentDecisionModel as m, REACTOR_SCHEMA as n, instrumentPgPool as o, selectDecisionModel as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, FLAG_PREREQUISITES as v, createConsistencyToken as w, KyselyWriteCache as x, validateFeatureFlags as y, DocumentNotFoundError as z };
|
|
2983
4275
|
|
|
2984
|
-
//# sourceMappingURL=drive-container-types-
|
|
4276
|
+
//# sourceMappingURL=drive-container-types-BJCKXJwH.js.map
|