@powerhousedao/reactor 6.2.2-dev.3 → 6.2.2-dev.31
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-BDrPlCJl.js → build-worker-executor-CO2_-cnA.js} +3 -3
- package/dist/{build-worker-executor-BDrPlCJl.js.map → build-worker-executor-CO2_-cnA.js.map} +1 -1
- package/dist/{document-indexer-FGJmRAdX.js → document-indexer-BY-mPXJF.js} +10 -3
- package/dist/document-indexer-BY-mPXJF.js.map +1 -0
- package/dist/{drive-container-types-DpJp2AmE.js → drive-container-types-BNwEHEXP.js} +785 -144
- package/dist/drive-container-types-BNwEHEXP.js.map +1 -0
- package/dist/entry.js +3 -2
- package/dist/entry.js.map +1 -1
- package/dist/index.d.ts +331 -107
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -119
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +3 -3
- package/dist/{worker-DBJOv8Gp.js → worker-DNfxv5Ir.js} +2 -2
- package/dist/{worker-DBJOv8Gp.js.map → worker-DNfxv5Ir.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/package.json +4 -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 { DowngradeNotSupportedError, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, 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,27 @@ 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
|
+
/**
|
|
75
96
|
* Error thrown when an operation has an invalid signature.
|
|
76
97
|
*/
|
|
77
98
|
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
@@ -181,6 +202,57 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
181
202
|
}
|
|
182
203
|
};
|
|
183
204
|
//#endregion
|
|
205
|
+
//#region src/storage/interfaces.ts
|
|
206
|
+
/**
|
|
207
|
+
* Thrown when an operation with the same identity already exists in the store.
|
|
208
|
+
*/
|
|
209
|
+
var DuplicateOperationError = class extends Error {
|
|
210
|
+
constructor(description) {
|
|
211
|
+
super(`Duplicate operation: ${description}`);
|
|
212
|
+
this.name = "DuplicateOperationError";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Thrown when a concurrent write conflict is detected during an atomic apply.
|
|
217
|
+
*/
|
|
218
|
+
var OptimisticLockError = class extends Error {
|
|
219
|
+
constructor(message) {
|
|
220
|
+
super(message);
|
|
221
|
+
this.name = "OptimisticLockError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Thrown when the caller-provided revision does not match the current
|
|
226
|
+
* stored revision, indicating a stale read.
|
|
227
|
+
*/
|
|
228
|
+
var RevisionMismatchError = class extends Error {
|
|
229
|
+
constructor(expected, actual) {
|
|
230
|
+
super(`Revision mismatch: expected ${expected}, got ${actual}`);
|
|
231
|
+
this.name = "RevisionMismatchError";
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
/** Error history keeps messages, not classes, so failures match by prefix. */
|
|
235
|
+
const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
236
|
+
/**
|
|
237
|
+
* A read-set stream grew before the append committed. A concurrency
|
|
238
|
+
* conflict, not a fault: the caller retries against the new stream heads.
|
|
239
|
+
*/
|
|
240
|
+
var AppendConditionFailedError = class extends Error {
|
|
241
|
+
constructor(condition) {
|
|
242
|
+
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
243
|
+
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
244
|
+
this.condition = condition;
|
|
245
|
+
this.name = "AppendConditionFailedError";
|
|
246
|
+
}
|
|
247
|
+
static isError(error) {
|
|
248
|
+
return Error.isError(error) && error.name === "AppendConditionFailedError";
|
|
249
|
+
}
|
|
250
|
+
/** True when a recorded error message is an append-condition failure. */
|
|
251
|
+
static isFailureMessage(message) {
|
|
252
|
+
return message.startsWith(APPEND_CONDITION_FAILED_PREFIX);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
184
256
|
//#region src/cache/collection-membership-cache.ts
|
|
185
257
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
186
258
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -634,6 +706,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
634
706
|
skip: op.skip,
|
|
635
707
|
hash: op.hash,
|
|
636
708
|
action: op.action,
|
|
709
|
+
deniedReason: op.deniedReason ?? null,
|
|
637
710
|
sourceRemote: op.sourceRemote
|
|
638
711
|
}));
|
|
639
712
|
operationOrdinals = (await trx.insertInto("operation_index_operations").values(operationRows).returning("ordinal").execute()).map((row) => row.ordinal);
|
|
@@ -765,6 +838,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
765
838
|
hash: row.hash,
|
|
766
839
|
skip: row.skip,
|
|
767
840
|
action: row.action,
|
|
841
|
+
deniedReason: row.deniedReason ?? void 0,
|
|
768
842
|
id: row.opId
|
|
769
843
|
},
|
|
770
844
|
context: {
|
|
@@ -788,6 +862,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
788
862
|
hash: row.hash,
|
|
789
863
|
skip: row.skip,
|
|
790
864
|
action: row.action,
|
|
865
|
+
deniedReason: row.deniedReason ?? void 0,
|
|
791
866
|
id: row.opId,
|
|
792
867
|
sourceRemote: row.sourceRemote
|
|
793
868
|
};
|
|
@@ -873,11 +948,69 @@ var RingBuffer = class {
|
|
|
873
948
|
}
|
|
874
949
|
};
|
|
875
950
|
//#endregion
|
|
951
|
+
//#region src/cache/write-cache-types.ts
|
|
952
|
+
/**
|
|
953
|
+
* Where a snapshot sits in its stream.
|
|
954
|
+
*
|
|
955
|
+
* - `Head`: the newest revision of the stream when it was stored. Only these
|
|
956
|
+
* can answer a read that asks for the head.
|
|
957
|
+
* - `Historical`: state at an earlier revision. Usable as a starting point to
|
|
958
|
+
* replay forward from, and as an answer to a read for that same revision.
|
|
959
|
+
*/
|
|
960
|
+
let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
|
|
961
|
+
SnapshotPosition["Head"] = "head";
|
|
962
|
+
SnapshotPosition["Historical"] = "historical";
|
|
963
|
+
return SnapshotPosition;
|
|
964
|
+
}({});
|
|
965
|
+
//#endregion
|
|
876
966
|
//#region src/cache/kysely-write-cache.ts
|
|
967
|
+
/**
|
|
968
|
+
* The last operation index a keyframe's document reflects for the scope. A
|
|
969
|
+
* keyframe only exists for a scope that has operations, so a missing entry
|
|
970
|
+
* means the stored row is corrupt.
|
|
971
|
+
*/
|
|
972
|
+
function keyframeRevision(keyframe, documentId, scope) {
|
|
973
|
+
const nextIndex = keyframe.document.header.revision[scope];
|
|
974
|
+
if (typeof nextIndex !== "number") throw new Error(`Corrupt keyframe for document ${documentId} at revision ${keyframe.revision}: header carries no ${scope} revision`);
|
|
975
|
+
return nextIndex - 1;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Records a denied operation in the history without applying it. This is used
|
|
979
|
+
* for auth-rejected operations, as these need to be recorded without being
|
|
980
|
+
* applied.
|
|
981
|
+
*/
|
|
982
|
+
function appendWithoutApplying(document, scope, operation) {
|
|
983
|
+
return {
|
|
984
|
+
...document,
|
|
985
|
+
operations: {
|
|
986
|
+
...document.operations,
|
|
987
|
+
[scope]: [...document.operations[scope] ?? [], operation]
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
877
991
|
function extractModuleVersion(doc) {
|
|
878
992
|
const v = doc.state.document.version;
|
|
879
993
|
return v === 0 ? void 0 : v;
|
|
880
994
|
}
|
|
995
|
+
/** The highest revision held, latest push winning a tie. */
|
|
996
|
+
function highestRevision(snapshots) {
|
|
997
|
+
let newest = void 0;
|
|
998
|
+
for (const snapshot of snapshots) if (!newest || snapshot.revision >= newest.revision) newest = snapshot;
|
|
999
|
+
return newest;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Copies a document far enough that the caller cannot write through it. Inside
|
|
1003
|
+
* this class, callers only ever replace whole fields on these four, so one
|
|
1004
|
+
* level each is enough.
|
|
1005
|
+
*/
|
|
1006
|
+
function copyDocument(document) {
|
|
1007
|
+
return {
|
|
1008
|
+
...document,
|
|
1009
|
+
header: { ...document.header },
|
|
1010
|
+
state: { ...document.state },
|
|
1011
|
+
operations: { ...document.operations }
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
881
1014
|
/**
|
|
882
1015
|
* In-memory write cache with keyframe persistence for PHDocuments.
|
|
883
1016
|
*
|
|
@@ -956,6 +1089,8 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
956
1089
|
/**
|
|
957
1090
|
* Retrieves document state at a specific revision from cache or rebuilds it.
|
|
958
1091
|
*
|
|
1092
|
+
* Note: this returns a _shallow_ copy of the document.
|
|
1093
|
+
*
|
|
959
1094
|
* Cache hit path: Returns cached snapshot if available (O(1))
|
|
960
1095
|
* Warm miss path: Rebuilds from cached base revision + incremental ops
|
|
961
1096
|
* Cold miss path: Rebuilds from keyframe or from scratch using all operations
|
|
@@ -979,30 +1114,35 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
979
1114
|
if (stream) {
|
|
980
1115
|
const snapshots = stream.ringBuffer.getAll();
|
|
981
1116
|
if (targetRevision === void 0) {
|
|
982
|
-
|
|
983
|
-
|
|
1117
|
+
const newest = highestRevision(snapshots);
|
|
1118
|
+
if (newest?.position === SnapshotPosition.Head) {
|
|
984
1119
|
this.lruTracker.touch(streamKey);
|
|
985
|
-
return newest.document;
|
|
1120
|
+
return copyDocument(newest.document);
|
|
1121
|
+
}
|
|
1122
|
+
if (newest) {
|
|
1123
|
+
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1124
|
+
this.store(documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, SnapshotPosition.Head);
|
|
1125
|
+
this.lruTracker.touch(streamKey);
|
|
1126
|
+
return document;
|
|
986
1127
|
}
|
|
987
1128
|
} else {
|
|
988
|
-
const exactMatch = snapshots.
|
|
1129
|
+
const exactMatch = snapshots.findLast((s) => s.revision === targetRevision);
|
|
989
1130
|
if (exactMatch) {
|
|
990
1131
|
this.lruTracker.touch(streamKey);
|
|
991
|
-
return exactMatch.document;
|
|
1132
|
+
return copyDocument(exactMatch.document);
|
|
992
1133
|
}
|
|
993
1134
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
994
1135
|
if (newestOlder) {
|
|
995
1136
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
996
|
-
this.
|
|
1137
|
+
this.store(documentId, scope, branch, targetRevision, document, SnapshotPosition.Historical);
|
|
997
1138
|
this.lruTracker.touch(streamKey);
|
|
998
1139
|
return document;
|
|
999
1140
|
}
|
|
1000
1141
|
}
|
|
1001
1142
|
}
|
|
1002
1143
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
this.putState(documentId, scope, branch, revision, document);
|
|
1144
|
+
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1145
|
+
this.store(documentId, scope, branch, revision, document, targetRevision === void 0 ? SnapshotPosition.Head : SnapshotPosition.Historical);
|
|
1006
1146
|
return document;
|
|
1007
1147
|
}
|
|
1008
1148
|
/**
|
|
@@ -1025,16 +1165,20 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1025
1165
|
* @param document - The document to cache
|
|
1026
1166
|
* @throws {Error} If document serialization fails
|
|
1027
1167
|
*/
|
|
1028
|
-
putState(documentId, scope, branch, revision, document) {
|
|
1168
|
+
putState(documentId, scope, branch, revision, document, position) {
|
|
1169
|
+
this.store(documentId, scope, branch, revision, document, position);
|
|
1170
|
+
}
|
|
1171
|
+
store(documentId, scope, branch, revision, document, position) {
|
|
1029
1172
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1030
1173
|
const stream = this.getOrCreateStream(streamKey);
|
|
1031
1174
|
const snapshot = {
|
|
1032
1175
|
revision,
|
|
1033
1176
|
document: {
|
|
1034
|
-
...document,
|
|
1177
|
+
...copyDocument(document),
|
|
1035
1178
|
operations: Object.fromEntries(Object.entries(document.operations).map(([k, ops]) => [k, ops.length ? [ops.at(-1)] : []])),
|
|
1036
1179
|
clipboard: []
|
|
1037
|
-
}
|
|
1180
|
+
},
|
|
1181
|
+
position
|
|
1038
1182
|
};
|
|
1039
1183
|
stream.ringBuffer.push(snapshot);
|
|
1040
1184
|
if (this.isKeyframeRevision(revision)) this.keyframeStore.putKeyframe(documentId, scope, branch, revision, {
|
|
@@ -1102,58 +1246,75 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1102
1246
|
}
|
|
1103
1247
|
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
1104
1248
|
if (targetRevision === Number.MAX_SAFE_INTEGER || targetRevision <= 0) return;
|
|
1105
|
-
|
|
1249
|
+
const keyframe = await this.keyframeStore.findNearestKeyframe(documentId, scope, branch, targetRevision, signal);
|
|
1250
|
+
if (!keyframe) return;
|
|
1251
|
+
return {
|
|
1252
|
+
revision: Math.min(keyframeRevision(keyframe, documentId, scope), keyframe.revision),
|
|
1253
|
+
document: keyframe.document
|
|
1254
|
+
};
|
|
1106
1255
|
}
|
|
1107
1256
|
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
1108
1257
|
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1109
1258
|
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
1259
|
+
const documentScopeBound = scope === "document" ? targetRevision : void 0;
|
|
1110
1260
|
let document;
|
|
1111
1261
|
let startRevision;
|
|
1112
1262
|
let documentType;
|
|
1113
1263
|
const validatedUpgrades = [];
|
|
1264
|
+
let lastDocumentScopeOperation;
|
|
1114
1265
|
if (keyframe) {
|
|
1115
1266
|
document = keyframe.document;
|
|
1116
1267
|
startRevision = keyframe.revision;
|
|
1117
1268
|
documentType = keyframe.document.header.documentType;
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1269
|
+
const documentScopeResume = scope === "document" ? keyframe.revision : keyframeRevision(keyframe, documentId, "document");
|
|
1270
|
+
const docScopeOpsAfterKeyframe = await this.operationStore.getSince(documentId, "document", branch, documentScopeResume, void 0, void 0, signal);
|
|
1271
|
+
for (const operation of docScopeOpsAfterKeyframe.results) {
|
|
1272
|
+
if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
|
|
1273
|
+
lastDocumentScopeOperation = operation;
|
|
1274
|
+
if (operation.error || isDenied(operation)) continue;
|
|
1275
|
+
if (operation.action.type === "UPGRADE_DOCUMENT") {
|
|
1276
|
+
const upgradeAction = operation.action;
|
|
1277
|
+
const fromVersion = upgradeAction.input.fromVersion;
|
|
1278
|
+
const toVersion = upgradeAction.input.toVersion;
|
|
1279
|
+
if (fromVersion > 0 && fromVersion < toVersion) {
|
|
1280
|
+
let upgradePath;
|
|
1281
|
+
try {
|
|
1282
|
+
upgradePath = this.registry.computeUpgradePath(documentType, fromVersion, toVersion);
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
if (upgradeAction.input.initialState !== void 0) upgradePath = void 0;
|
|
1285
|
+
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 });
|
|
1286
|
+
}
|
|
1287
|
+
validatedUpgrades.push({
|
|
1288
|
+
fromVersion,
|
|
1289
|
+
toVersion,
|
|
1290
|
+
revision: upgradeAction.input.revision,
|
|
1291
|
+
timestampUtcMs: operation.timestampUtcMs
|
|
1292
|
+
});
|
|
1293
|
+
document = applyUpgradeDocumentAction(document, upgradeAction, upgradePath);
|
|
1130
1294
|
}
|
|
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);
|
|
1295
|
+
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1296
|
+
}
|
|
1140
1297
|
} else {
|
|
1141
1298
|
startRevision = -1;
|
|
1142
1299
|
const createOpResult = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, {
|
|
1143
1300
|
cursor: "0",
|
|
1144
1301
|
limit: 1
|
|
1145
1302
|
}, signal);
|
|
1146
|
-
if (createOpResult.results.length === 0) throw new
|
|
1303
|
+
if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1147
1304
|
const createOp = createOpResult.results[0];
|
|
1148
1305
|
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
1306
|
const documentCreateAction = createOp.action;
|
|
1150
1307
|
documentType = documentCreateAction.input.model;
|
|
1151
1308
|
if (!documentType) throw new Error(`Failed to rebuild document ${documentId}: CREATE_DOCUMENT action missing model in input`);
|
|
1152
1309
|
document = createDocumentFromAction(documentCreateAction);
|
|
1310
|
+
lastDocumentScopeOperation = createOp;
|
|
1153
1311
|
let docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1154
1312
|
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, 0, void 0, void 0, signal);
|
|
1155
1313
|
for (const operation of docScopeOps.results) {
|
|
1314
|
+
if (documentScopeBound !== void 0 && operation.index > documentScopeBound) break;
|
|
1315
|
+
lastDocumentScopeOperation = operation;
|
|
1156
1316
|
if (operation.index === 0) continue;
|
|
1317
|
+
if (operation.error || isDenied(operation)) continue;
|
|
1157
1318
|
if (operation.action.type === "UPGRADE_DOCUMENT") {
|
|
1158
1319
|
const upgradeAction = operation.action;
|
|
1159
1320
|
const fromVersion = upgradeAction.input.fromVersion;
|
|
@@ -1177,7 +1338,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1177
1338
|
docModule = this.registry.getModule(documentType, extractModuleVersion(document));
|
|
1178
1339
|
} else if (operation.action.type === "DELETE_DOCUMENT") applyDeleteDocumentAction(document, operation.action);
|
|
1179
1340
|
else {
|
|
1180
|
-
const protocolVersion = document.header
|
|
1341
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1181
1342
|
document = docModule.reducer(document, operation.action, void 0, {
|
|
1182
1343
|
skip: operation.skip,
|
|
1183
1344
|
protocolVersion
|
|
@@ -1185,6 +1346,21 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1185
1346
|
}
|
|
1186
1347
|
}
|
|
1187
1348
|
}
|
|
1349
|
+
if (scope === "document") {
|
|
1350
|
+
const last = lastDocumentScopeOperation ?? await this.operationAt(documentId, "document", branch, startRevision, signal);
|
|
1351
|
+
document.operations = {
|
|
1352
|
+
...document.operations,
|
|
1353
|
+
document: last ? [last] : []
|
|
1354
|
+
};
|
|
1355
|
+
return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
1356
|
+
}
|
|
1357
|
+
if (keyframe) {
|
|
1358
|
+
const resumeOperation = await this.operationAt(documentId, scope, branch, startRevision, signal);
|
|
1359
|
+
if (resumeOperation) document.operations = {
|
|
1360
|
+
...document.operations,
|
|
1361
|
+
[scope]: [resumeOperation]
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1188
1364
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
1189
1365
|
const getModuleCached = (version) => {
|
|
1190
1366
|
const key = version ?? 0;
|
|
@@ -1209,11 +1385,14 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1209
1385
|
for (const operation of result.results) {
|
|
1210
1386
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1211
1387
|
const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, extractModuleVersion(document));
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1388
|
+
if (isDenied(operation)) document = appendWithoutApplying(document, scope, operation);
|
|
1389
|
+
else {
|
|
1390
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1391
|
+
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
1392
|
+
skip: operation.skip,
|
|
1393
|
+
protocolVersion
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1217
1396
|
}
|
|
1218
1397
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
1219
1398
|
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
@@ -1222,11 +1401,31 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1222
1401
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1223
1402
|
}
|
|
1224
1403
|
} while (hasMorePages);
|
|
1404
|
+
return this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Copies the current document revisions onto the document. Overwrites the
|
|
1408
|
+
* requested scope revision with the target revision, if provided.
|
|
1409
|
+
*/
|
|
1410
|
+
async stampRevisions(document, documentId, scope, branch, targetRevision, signal) {
|
|
1225
1411
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1226
1412
|
document.header.revision = revisions.revision;
|
|
1413
|
+
if (targetRevision !== void 0) document.header.revision = {
|
|
1414
|
+
...document.header.revision,
|
|
1415
|
+
[scope]: targetRevision + 1
|
|
1416
|
+
};
|
|
1227
1417
|
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1228
1418
|
return document;
|
|
1229
1419
|
}
|
|
1420
|
+
/** The stored operation at `index`, or undefined if it is no longer there. */
|
|
1421
|
+
async operationAt(documentId, scope, branch, index, signal) {
|
|
1422
|
+
if (index < 0) return;
|
|
1423
|
+
const operation = (await this.operationStore.getSince(documentId, scope, branch, index - 1, void 0, {
|
|
1424
|
+
cursor: "0",
|
|
1425
|
+
limit: 1
|
|
1426
|
+
}, signal)).results[0];
|
|
1427
|
+
return operation && operation.index === index ? operation : void 0;
|
|
1428
|
+
}
|
|
1230
1429
|
/**
|
|
1231
1430
|
* Resolves which module version to use for a given operation in phase 2.
|
|
1232
1431
|
*
|
|
@@ -1250,19 +1449,22 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1250
1449
|
async warmMissRebuild(baseDocument, baseRevision, documentId, scope, branch, targetRevision, signal) {
|
|
1251
1450
|
const documentType = baseDocument.header.documentType;
|
|
1252
1451
|
const docScopeNextIndex = baseDocument.header.revision["document"] ?? 0;
|
|
1253
|
-
if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.
|
|
1452
|
+
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
1453
|
const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
|
|
1255
|
-
let document = baseDocument;
|
|
1454
|
+
let document = copyDocument(baseDocument);
|
|
1256
1455
|
try {
|
|
1257
1456
|
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
1258
1457
|
for (const operation of pagedResults.results) {
|
|
1259
1458
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1260
1459
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1460
|
+
if (isDenied(operation)) document = appendWithoutApplying(document, scope, operation);
|
|
1461
|
+
else {
|
|
1462
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
1463
|
+
document = module.reducer(document, operation.action, void 0, {
|
|
1464
|
+
skip: operation.skip,
|
|
1465
|
+
protocolVersion
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1266
1468
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
1267
1469
|
}
|
|
1268
1470
|
} catch (err) {
|
|
@@ -1270,6 +1472,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1270
1472
|
}
|
|
1271
1473
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
1272
1474
|
document.header.revision = revisions.revision;
|
|
1475
|
+
if (targetRevision !== void 0) document.header.revision = {
|
|
1476
|
+
...document.header.revision,
|
|
1477
|
+
[scope]: targetRevision + 1
|
|
1478
|
+
};
|
|
1273
1479
|
document.header.lastModifiedAtUtcIso = revisions.latestTimestamp;
|
|
1274
1480
|
return document;
|
|
1275
1481
|
}
|
|
@@ -1436,6 +1642,260 @@ function reshuffleByTimestamp(startIndex, opsA, opsB) {
|
|
|
1436
1642
|
}));
|
|
1437
1643
|
}
|
|
1438
1644
|
//#endregion
|
|
1645
|
+
//#region src/decision/build-decision-model.ts
|
|
1646
|
+
/**
|
|
1647
|
+
* Reads each projection's stream through the write cache, recording the
|
|
1648
|
+
* revision observed. Static projections resolve first; derived projections
|
|
1649
|
+
* see only those and contribute a map from document id to state. Each
|
|
1650
|
+
* distinct stream is read once and yields one append condition entry.
|
|
1651
|
+
*/
|
|
1652
|
+
async function buildDecisionModel(cache, definition, target, signal) {
|
|
1653
|
+
const decisionModel = definition(target);
|
|
1654
|
+
const projections = Object.entries(decisionModel.projections);
|
|
1655
|
+
const reads = /* @__PURE__ */ new Map();
|
|
1656
|
+
const model = {};
|
|
1657
|
+
for (const [key, projection] of projections) {
|
|
1658
|
+
if (typeof projection.query === "function") continue;
|
|
1659
|
+
model[key] = (await readStream(cache, projection.query, reads, signal)).state;
|
|
1660
|
+
}
|
|
1661
|
+
const staticModel = { ...model };
|
|
1662
|
+
for (const [key, projection] of projections) {
|
|
1663
|
+
if (typeof projection.query !== "function") continue;
|
|
1664
|
+
const queries = projection.query(staticModel);
|
|
1665
|
+
const value = {};
|
|
1666
|
+
for (const query of queries) {
|
|
1667
|
+
const read = await readStream(cache, query, reads, signal);
|
|
1668
|
+
value[query.documentId] = read.state;
|
|
1669
|
+
}
|
|
1670
|
+
model[key] = value;
|
|
1671
|
+
}
|
|
1672
|
+
return {
|
|
1673
|
+
model,
|
|
1674
|
+
appendCondition: { streams: [...reads.values()].map((read) => read.stream) }
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
async function readStream(cache, query, reads, signal) {
|
|
1678
|
+
const key = `${query.documentId}:${query.scope}:${query.branch}`;
|
|
1679
|
+
const existing = reads.get(key);
|
|
1680
|
+
if (existing) return existing;
|
|
1681
|
+
const document = await cache.getState(query.documentId, query.scope, query.branch, void 0, signal);
|
|
1682
|
+
const read = {
|
|
1683
|
+
state: document.state[query.scope],
|
|
1684
|
+
stream: {
|
|
1685
|
+
documentId: query.documentId,
|
|
1686
|
+
scope: query.scope,
|
|
1687
|
+
branch: query.branch,
|
|
1688
|
+
revision: observedRevision(document, query.scope)
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
reads.set(key, read);
|
|
1692
|
+
return read;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* The highest operation index the document reflects for the scope, or -1 if
|
|
1696
|
+
* empty. `header.revision` is authoritative, not the rebuilt operation list.
|
|
1697
|
+
*/
|
|
1698
|
+
function observedRevision(document, scope) {
|
|
1699
|
+
if (scope in document.header.revision) return document.header.revision[scope] - 1;
|
|
1700
|
+
if (scope in document.operations) {
|
|
1701
|
+
const operations = document.operations[scope];
|
|
1702
|
+
if (operations.length > 0) return operations[operations.length - 1].index;
|
|
1703
|
+
}
|
|
1704
|
+
if (!(scope in document.header.revision)) return -1;
|
|
1705
|
+
return document.header.revision[scope] - 1;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The streams a model reads whose queries are known before it is built. A
|
|
1709
|
+
* derived query needs the statically-queried projections first, so it is not
|
|
1710
|
+
* included here.
|
|
1711
|
+
*/
|
|
1712
|
+
function staticReadSet(definition) {
|
|
1713
|
+
const streams = [];
|
|
1714
|
+
for (const projection of Object.values(definition.projections)) {
|
|
1715
|
+
if (typeof projection.query === "function") continue;
|
|
1716
|
+
streams.push({
|
|
1717
|
+
query: projection.query,
|
|
1718
|
+
decidingActions: projection.decidingActions,
|
|
1719
|
+
apply: projection.apply
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return streams;
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1725
|
+
//#region src/decision/document-decision-model.ts
|
|
1726
|
+
/** Why the model refused an operation. */
|
|
1727
|
+
const DOCUMENT_DELETED_REASON = "document deleted";
|
|
1728
|
+
/**
|
|
1729
|
+
* The simplest decision model: one projection over the document scope, which
|
|
1730
|
+
* rejects on a deleted document.
|
|
1731
|
+
*/
|
|
1732
|
+
function documentDecisionModel(target) {
|
|
1733
|
+
return {
|
|
1734
|
+
projections: { document: {
|
|
1735
|
+
decidingActions: ["DELETE_DOCUMENT"],
|
|
1736
|
+
apply: (document, operation) => operation.action.type === "DELETE_DOCUMENT" ? applyDeleteDocumentAction({
|
|
1737
|
+
...document,
|
|
1738
|
+
state: { ...document.state }
|
|
1739
|
+
}, operation.action) : document,
|
|
1740
|
+
query: {
|
|
1741
|
+
documentId: target.documentId,
|
|
1742
|
+
branch: target.branch,
|
|
1743
|
+
scope: "document"
|
|
1744
|
+
}
|
|
1745
|
+
} },
|
|
1746
|
+
judgesScope() {
|
|
1747
|
+
return true;
|
|
1748
|
+
},
|
|
1749
|
+
decide(model) {
|
|
1750
|
+
return model.document.isDeleted ? "deny" : "allow";
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
//#endregion
|
|
1755
|
+
//#region src/decision/merged-order.ts
|
|
1756
|
+
/** Identifies a stream within a walk. */
|
|
1757
|
+
function streamKey(query) {
|
|
1758
|
+
return `${query.documentId}:${query.scope}:${query.branch}`;
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Orders two operations from different streams by position. Timestamp decides;
|
|
1762
|
+
* an equal timestamp falls to the action id and then the operation id, so that
|
|
1763
|
+
* two replicas holding the same operations agree on the order whatever order
|
|
1764
|
+
* they happen to store them in.
|
|
1765
|
+
*/
|
|
1766
|
+
function comparePositions(a, b) {
|
|
1767
|
+
const aTime = Date.parse(a.operation.timestampUtcMs);
|
|
1768
|
+
const bTime = Date.parse(b.operation.timestampUtcMs);
|
|
1769
|
+
if (aTime !== bTime) return aTime - bTime;
|
|
1770
|
+
const actionIds = (a.operation.action.id ?? "").localeCompare(b.operation.action.id ?? "");
|
|
1771
|
+
if (actionIds !== 0) return actionIds;
|
|
1772
|
+
return (a.operation.id ?? "").localeCompare(b.operation.id ?? "");
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Merges the read-set streams into one sequence by position. Each stream keeps
|
|
1776
|
+
* its stored order; only how far to go is decided by the timestamp, so a stream
|
|
1777
|
+
* whose stored order disagrees with its timestamp order still applies in the
|
|
1778
|
+
* order it is stored.
|
|
1779
|
+
*
|
|
1780
|
+
* An operation's position in the result is the bound a decision at that
|
|
1781
|
+
* operation reads to: every operation before it has been applied, and it has
|
|
1782
|
+
* not.
|
|
1783
|
+
*/
|
|
1784
|
+
function mergeByPosition(streams) {
|
|
1785
|
+
const merged = [];
|
|
1786
|
+
for (const stream of streams) for (const operation of stream.operations) merged.push({
|
|
1787
|
+
streamKey: stream.streamKey,
|
|
1788
|
+
operation
|
|
1789
|
+
});
|
|
1790
|
+
return merged.sort(comparePositions);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* For auth-related reshuffles, we may need to retract previous operations.
|
|
1794
|
+
* This function calculates the needed skip value.
|
|
1795
|
+
*/
|
|
1796
|
+
function retractionSkip(nextIndex, firstRetractedIndex) {
|
|
1797
|
+
return nextIndex - firstRetractedIndex;
|
|
1798
|
+
}
|
|
1799
|
+
//#endregion
|
|
1800
|
+
//#region src/decision/walk.ts
|
|
1801
|
+
/**
|
|
1802
|
+
* Visits every operation in the read-set once, in the order their positions
|
|
1803
|
+
* fall, and hands back the state each stream held just before it. That state is
|
|
1804
|
+
* what a decision at that operation reads: everything ahead of it has been
|
|
1805
|
+
* applied and it has not.
|
|
1806
|
+
*
|
|
1807
|
+
* Skips are resolved first (i.e. this is performed on a garbage collected
|
|
1808
|
+
* stream), which means we can do a single forward pass.
|
|
1809
|
+
*
|
|
1810
|
+
* A denied operation is visited but not applied.
|
|
1811
|
+
*/
|
|
1812
|
+
function* walkByPosition(streams) {
|
|
1813
|
+
const merged = mergeByPosition(streams.map((stream) => ({
|
|
1814
|
+
streamKey: stream.streamKey,
|
|
1815
|
+
operations: garbageCollect(sortOperations([...stream.operations]))
|
|
1816
|
+
})));
|
|
1817
|
+
const states = new Map(streams.map((stream) => [stream.streamKey, stream.document]));
|
|
1818
|
+
for (const { streamKey, operation } of merged) {
|
|
1819
|
+
yield {
|
|
1820
|
+
streamKey,
|
|
1821
|
+
operation,
|
|
1822
|
+
states: new Map(states)
|
|
1823
|
+
};
|
|
1824
|
+
if (isDenied(operation)) continue;
|
|
1825
|
+
const stream = streams.find((candidate) => candidate.streamKey === streamKey);
|
|
1826
|
+
const before = states.get(streamKey);
|
|
1827
|
+
if (before === void 0 || stream === void 0) throw new Error(`No state for stream ${streamKey}`);
|
|
1828
|
+
states.set(streamKey, stream.apply(before, operation));
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
//#endregion
|
|
1832
|
+
//#region src/decision/deletion-verdicts.ts
|
|
1833
|
+
const WRITTEN = "written";
|
|
1834
|
+
/** Whether a read stream counts this action as one that changes a verdict. */
|
|
1835
|
+
function canRefuseOthers(operation, readSet) {
|
|
1836
|
+
return readSet.some((stream) => stream.decidingActions.includes(operation.action.type));
|
|
1837
|
+
}
|
|
1838
|
+
/** The deleted one of these, or the first if none of them is deleted. */
|
|
1839
|
+
function firstDeleted(candidates) {
|
|
1840
|
+
for (const candidate of candidates) if (candidate?.state.document.isDeleted) return candidate.state.document;
|
|
1841
|
+
return candidates[0].state.document;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* This function determines which operations will be refused because of
|
|
1845
|
+
* deletes, returning reasons in a parallel array (undefined means the
|
|
1846
|
+
* operation is not refused).
|
|
1847
|
+
*
|
|
1848
|
+
* A deletion already in the stream refuses the operations timestamped after it
|
|
1849
|
+
* and leaves the earlier ones alone. A deletion among the ones passed in does
|
|
1850
|
+
* the same to those after it.
|
|
1851
|
+
*/
|
|
1852
|
+
async function deletionVerdictsByPosition(documentId, scope, branch, operations, writeCache, operationStore, signal) {
|
|
1853
|
+
const definition = documentDecisionModel({
|
|
1854
|
+
documentId,
|
|
1855
|
+
branch
|
|
1856
|
+
});
|
|
1857
|
+
const readSet = staticReadSet(definition);
|
|
1858
|
+
if (!definition.judgesScope(scope)) return operations.map(() => void 0);
|
|
1859
|
+
const readStreams = await Promise.all(readSet.map(async (stream) => ({
|
|
1860
|
+
stream,
|
|
1861
|
+
operations: (await operationStore.getSince(stream.query.documentId, stream.query.scope, stream.query.branch, 0, { actionTypes: stream.decidingActions }, void 0, signal)).results
|
|
1862
|
+
})));
|
|
1863
|
+
const decidingWritten = operations.filter((operation) => canRefuseOthers(operation, readSet));
|
|
1864
|
+
if (readStreams.every((read) => read.operations.length === 0) && decidingWritten.length === 0) return operations.map(() => void 0);
|
|
1865
|
+
const walked = [];
|
|
1866
|
+
for (const read of readStreams) {
|
|
1867
|
+
const before = await writeCache.getState(read.stream.query.documentId, read.stream.query.scope, read.stream.query.branch, 0, signal);
|
|
1868
|
+
walked.push({
|
|
1869
|
+
streamKey: streamKey(read.stream.query),
|
|
1870
|
+
document: before,
|
|
1871
|
+
operations: read.operations,
|
|
1872
|
+
apply: read.stream.apply
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
const writtenStreamIsRead = readSet.find((stream) => stream.query.scope === scope);
|
|
1876
|
+
walked.push({
|
|
1877
|
+
streamKey: WRITTEN,
|
|
1878
|
+
document: walked[0].document,
|
|
1879
|
+
operations,
|
|
1880
|
+
apply: writtenStreamIsRead?.apply ?? ((document) => document)
|
|
1881
|
+
});
|
|
1882
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
1883
|
+
for (const step of walkByPosition(walked)) {
|
|
1884
|
+
if (step.streamKey !== WRITTEN) continue;
|
|
1885
|
+
const document = firstDeleted([...step.states.values()]);
|
|
1886
|
+
const decision = definition.decide({ document }, {
|
|
1887
|
+
address: void 0,
|
|
1888
|
+
key: void 0
|
|
1889
|
+
}, {
|
|
1890
|
+
verb: "execute",
|
|
1891
|
+
scope,
|
|
1892
|
+
operation: step.operation.action.type
|
|
1893
|
+
}, { scopeState: void 0 });
|
|
1894
|
+
reasons.set(step.operation.id, decision === "deny" ? DOCUMENT_DELETED_REASON : void 0);
|
|
1895
|
+
}
|
|
1896
|
+
return operations.map((operation) => reasons.get(operation.id));
|
|
1897
|
+
}
|
|
1898
|
+
//#endregion
|
|
1439
1899
|
//#region src/cache/operation-index-types.ts
|
|
1440
1900
|
const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
1441
1901
|
/**
|
|
@@ -1487,11 +1947,11 @@ var DocumentActionHandler = class {
|
|
|
1487
1947
|
this.logger = logger;
|
|
1488
1948
|
this.driveContainerTypes = driveContainerTypes;
|
|
1489
1949
|
}
|
|
1490
|
-
async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
1950
|
+
async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal, verdictAlreadyDecided = false) {
|
|
1491
1951
|
switch (action.type) {
|
|
1492
1952
|
case "CREATE_DOCUMENT": return this.executeCreate(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
|
|
1493
|
-
case "DELETE_DOCUMENT": return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1494
|
-
case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
|
|
1953
|
+
case "DELETE_DOCUMENT": return this.executeDelete(job, action, startTime, indexTxn, stores, sourceRemote, signal, verdictAlreadyDecided);
|
|
1954
|
+
case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal, verdictAlreadyDecided);
|
|
1495
1955
|
case "ADD_RELATIONSHIP": return this.executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1496
1956
|
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
1497
1957
|
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
@@ -1524,7 +1984,7 @@ var DocumentActionHandler = class {
|
|
|
1524
1984
|
...document.operations,
|
|
1525
1985
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1526
1986
|
};
|
|
1527
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
1987
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1528
1988
|
indexTxn.write([{
|
|
1529
1989
|
...operation,
|
|
1530
1990
|
documentId: document.header.id,
|
|
@@ -1545,7 +2005,7 @@ var DocumentActionHandler = class {
|
|
|
1545
2005
|
});
|
|
1546
2006
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
1547
2007
|
}
|
|
1548
|
-
async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
2008
|
+
async executeDelete(job, action, startTime, indexTxn, stores, sourceRemote = "", signal, verdictAlreadyDecided = false) {
|
|
1549
2009
|
const input = action.input;
|
|
1550
2010
|
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1551
2011
|
const documentId = input.documentId;
|
|
@@ -1556,7 +2016,7 @@ var DocumentActionHandler = class {
|
|
|
1556
2016
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1557
2017
|
}
|
|
1558
2018
|
const documentState = document.state.document;
|
|
1559
|
-
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
2019
|
+
if (documentState.isDeleted && !verdictAlreadyDecided) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1560
2020
|
let operation = createOperation(action, getNextIndexForScope(document, job.scope), 0, {
|
|
1561
2021
|
documentId,
|
|
1562
2022
|
scope: job.scope,
|
|
@@ -1576,7 +2036,7 @@ var DocumentActionHandler = class {
|
|
|
1576
2036
|
...document.operations,
|
|
1577
2037
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1578
2038
|
};
|
|
1579
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
2039
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1580
2040
|
indexTxn.write([{
|
|
1581
2041
|
...operation,
|
|
1582
2042
|
documentId,
|
|
@@ -1592,7 +2052,7 @@ var DocumentActionHandler = class {
|
|
|
1592
2052
|
});
|
|
1593
2053
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
1594
2054
|
}
|
|
1595
|
-
async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
2055
|
+
async executeUpgrade(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal, verdictAlreadyDecided = false) {
|
|
1596
2056
|
const input = action.input;
|
|
1597
2057
|
if (!input.documentId) return buildErrorResult(job, /* @__PURE__ */ new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
1598
2058
|
const documentId = input.documentId;
|
|
@@ -1605,7 +2065,7 @@ var DocumentActionHandler = class {
|
|
|
1605
2065
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
1606
2066
|
}
|
|
1607
2067
|
const documentState = document.state.document;
|
|
1608
|
-
if (documentState.isDeleted) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
2068
|
+
if (documentState.isDeleted && !verdictAlreadyDecided) return buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
1609
2069
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
1610
2070
|
let upgradePath;
|
|
1611
2071
|
if (fromVersion > 0 && fromVersion < toVersion) try {
|
|
@@ -1643,7 +2103,7 @@ var DocumentActionHandler = class {
|
|
|
1643
2103
|
...document.operations,
|
|
1644
2104
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
1645
2105
|
};
|
|
1646
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
2106
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, SnapshotPosition.Head);
|
|
1647
2107
|
indexTxn.write([{
|
|
1648
2108
|
...operation,
|
|
1649
2109
|
documentId,
|
|
@@ -1714,7 +2174,7 @@ var DocumentActionHandler = class {
|
|
|
1714
2174
|
[job.scope]: scopeState === void 0 ? {} : structuredClone(scopeState)
|
|
1715
2175
|
};
|
|
1716
2176
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
1717
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
2177
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, SnapshotPosition.Head);
|
|
1718
2178
|
indexTxn.write([{
|
|
1719
2179
|
...operation,
|
|
1720
2180
|
documentId: input.sourceId,
|
|
@@ -1746,10 +2206,11 @@ var DocumentActionHandler = class {
|
|
|
1746
2206
|
} catch (error) {
|
|
1747
2207
|
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
1748
2208
|
stores.writeCache.invalidate(documentId, scope, branch);
|
|
2209
|
+
if (AppendConditionFailedError.isError(error)) for (const stream of error.condition.streams) stores.writeCache.invalidate(stream.documentId, stream.scope, stream.branch);
|
|
1749
2210
|
return {
|
|
1750
2211
|
job,
|
|
1751
2212
|
success: false,
|
|
1752
|
-
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2213
|
+
error: AppendConditionFailedError.isError(error) ? error : /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
1753
2214
|
duration: Date.now() - startTime
|
|
1754
2215
|
};
|
|
1755
2216
|
}
|
|
@@ -1827,6 +2288,7 @@ const documentScopeActions = [
|
|
|
1827
2288
|
*/
|
|
1828
2289
|
var SimpleJobExecutor = class {
|
|
1829
2290
|
config;
|
|
2291
|
+
featureFlags;
|
|
1830
2292
|
signatureVerifierModule;
|
|
1831
2293
|
documentActionHandler;
|
|
1832
2294
|
executionScope;
|
|
@@ -1841,6 +2303,7 @@ var SimpleJobExecutor = class {
|
|
|
1841
2303
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
1842
2304
|
this.driveContainerTypes = driveContainerTypes;
|
|
1843
2305
|
this.config = {
|
|
2306
|
+
featureFlags: config.featureFlags ?? {},
|
|
1844
2307
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
1845
2308
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
1846
2309
|
jobTimeoutMs: config.jobTimeoutMs ?? 3e4,
|
|
@@ -1848,6 +2311,7 @@ var SimpleJobExecutor = class {
|
|
|
1848
2311
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
1849
2312
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
1850
2313
|
};
|
|
2314
|
+
this.featureFlags = { documentDecisions: config.featureFlags?.documentDecisions ?? false };
|
|
1851
2315
|
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
1852
2316
|
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes);
|
|
1853
2317
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
@@ -1931,7 +2395,7 @@ var SimpleJobExecutor = class {
|
|
|
1931
2395
|
const documentIds = [...new Set(operations.map((op) => op.context.documentId))];
|
|
1932
2396
|
return stores.collectionMembershipCache.getCollectionsForDocuments(documentIds);
|
|
1933
2397
|
}
|
|
1934
|
-
async processActions(job, actions, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "", signal) {
|
|
2398
|
+
async processActions(job, actions, startTime, indexTxn, stores, skipValues, sourceOperations, sourceRemote = "", signal, deniedReasons) {
|
|
1935
2399
|
const generatedOperations = [];
|
|
1936
2400
|
const operationsWithContext = [];
|
|
1937
2401
|
try {
|
|
@@ -1955,7 +2419,8 @@ var SimpleJobExecutor = class {
|
|
|
1955
2419
|
const action = actions[actionIndex];
|
|
1956
2420
|
const skip = skipValues?.[actionIndex] ?? 0;
|
|
1957
2421
|
const sourceOperation = sourceOperations?.[actionIndex];
|
|
1958
|
-
const
|
|
2422
|
+
const deniedReason = deniedReasons?.[actionIndex];
|
|
2423
|
+
const result = documentScopeActions.includes(action.type) ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal, this.featureFlags.documentDecisions && job.kind === "load") : await this.executeRegularAction(job, action, startTime, indexTxn, stores, skip, sourceOperation, sourceRemote, signal, deniedReason);
|
|
1959
2424
|
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
1960
2425
|
if (error !== null) return {
|
|
1961
2426
|
success: false,
|
|
@@ -1980,14 +2445,43 @@ var SimpleJobExecutor = class {
|
|
|
1980
2445
|
operationsWithContext
|
|
1981
2446
|
};
|
|
1982
2447
|
}
|
|
1983
|
-
async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "", signal) {
|
|
1984
|
-
let
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2448
|
+
async executeRegularAction(job, action, startTime, indexTxn, stores, skip = 0, sourceOperation, sourceRemote = "", signal, deniedReason) {
|
|
2449
|
+
let appendCondition;
|
|
2450
|
+
let documentVersion;
|
|
2451
|
+
const verdictAlreadyDecided = this.featureFlags.documentDecisions && job.kind === "load";
|
|
2452
|
+
if (this.featureFlags.documentDecisions && !verdictAlreadyDecided) {
|
|
2453
|
+
const target = {
|
|
2454
|
+
documentId: job.documentId,
|
|
2455
|
+
branch: job.branch
|
|
2456
|
+
};
|
|
2457
|
+
const definition = documentDecisionModel(target);
|
|
2458
|
+
let built;
|
|
2459
|
+
try {
|
|
2460
|
+
built = await buildDecisionModel(stores.writeCache, () => definition, target, signal);
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2463
|
+
}
|
|
2464
|
+
if (definition.decide(built.model, {
|
|
2465
|
+
address: action.context?.signer?.user.address,
|
|
2466
|
+
key: action.context?.signer?.app.key
|
|
2467
|
+
}, {
|
|
2468
|
+
verb: "execute",
|
|
2469
|
+
scope: action.scope,
|
|
2470
|
+
operation: action.type
|
|
2471
|
+
}, { scopeState: void 0 }) === "deny") return buildErrorResult(job, new DocumentDeletedError(job.documentId, built.model.document.deletedAtUtcIso), startTime);
|
|
2472
|
+
appendCondition = built.appendCondition;
|
|
2473
|
+
documentVersion = built.model.document.version;
|
|
2474
|
+
} else if (verdictAlreadyDecided) documentVersion = (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).state.document.version;
|
|
2475
|
+
else {
|
|
2476
|
+
let docMeta;
|
|
2477
|
+
try {
|
|
2478
|
+
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2481
|
+
}
|
|
2482
|
+
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2483
|
+
documentVersion = docMeta.state.version;
|
|
1989
2484
|
}
|
|
1990
|
-
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
1991
2485
|
if (isUndoRedo(action) || action.type === "PRUNE" || action.type === "NOOP" && skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
1992
2486
|
let document;
|
|
1993
2487
|
try {
|
|
@@ -1995,16 +2489,42 @@ var SimpleJobExecutor = class {
|
|
|
1995
2489
|
} catch (error) {
|
|
1996
2490
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
1997
2491
|
}
|
|
2492
|
+
if (job.kind !== "load") {
|
|
2493
|
+
const subject = {
|
|
2494
|
+
address: action.context?.signer?.user.address,
|
|
2495
|
+
key: action.context?.signer?.app.key
|
|
2496
|
+
};
|
|
2497
|
+
if (decide(document.state.auth, subject, {
|
|
2498
|
+
verb: "execute",
|
|
2499
|
+
scope: action.scope,
|
|
2500
|
+
operation: action.type
|
|
2501
|
+
}) === "deny") return buildErrorResult(job, new AuthorizationDeniedError(job.documentId, action.scope, action.type, subject.address), startTime);
|
|
2502
|
+
}
|
|
1998
2503
|
let module;
|
|
1999
2504
|
try {
|
|
2000
|
-
const moduleVersion =
|
|
2505
|
+
const moduleVersion = documentVersion === 0 ? void 0 : documentVersion;
|
|
2001
2506
|
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
2002
2507
|
} catch (error) {
|
|
2003
2508
|
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
2004
2509
|
}
|
|
2005
2510
|
let updatedDocument;
|
|
2006
|
-
|
|
2007
|
-
const
|
|
2511
|
+
if (deniedReason !== void 0) {
|
|
2512
|
+
const denied = createOperation(action, getNextIndexForScope(document, job.scope), skip, {
|
|
2513
|
+
documentId: job.documentId,
|
|
2514
|
+
scope: job.scope,
|
|
2515
|
+
branch: job.branch
|
|
2516
|
+
});
|
|
2517
|
+
denied.deniedReason = deniedReason;
|
|
2518
|
+
denied.hash = hashDocumentStateForScope(document, job.scope);
|
|
2519
|
+
updatedDocument = {
|
|
2520
|
+
...document,
|
|
2521
|
+
operations: {
|
|
2522
|
+
...document.operations,
|
|
2523
|
+
[job.scope]: [...document.operations[job.scope] ?? [], denied]
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
} else try {
|
|
2527
|
+
const protocolVersion = baseReducerVersion(document.header);
|
|
2008
2528
|
const reducerOptions = sourceOperation ? {
|
|
2009
2529
|
skip,
|
|
2010
2530
|
branch: job.branch,
|
|
@@ -2035,14 +2555,15 @@ var SimpleJobExecutor = class {
|
|
|
2035
2555
|
try {
|
|
2036
2556
|
storedOperations = await stores.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
2037
2557
|
txn.addOperations(newOperation);
|
|
2038
|
-
}, signal);
|
|
2558
|
+
}, signal, appendCondition);
|
|
2039
2559
|
} catch (error) {
|
|
2040
2560
|
this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
|
|
2041
2561
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2562
|
+
if (AppendConditionFailedError.isError(error)) for (const stream of error.condition.streams) stores.writeCache.invalidate(stream.documentId, stream.scope, stream.branch);
|
|
2042
2563
|
return {
|
|
2043
2564
|
job,
|
|
2044
2565
|
success: false,
|
|
2045
|
-
error: /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2566
|
+
error: AppendConditionFailedError.isError(error) ? error : /* @__PURE__ */ new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
2046
2567
|
duration: Date.now() - startTime
|
|
2047
2568
|
};
|
|
2048
2569
|
}
|
|
@@ -2051,7 +2572,7 @@ var SimpleJobExecutor = class {
|
|
|
2051
2572
|
...updatedDocument.header.revision,
|
|
2052
2573
|
[scope]: storedOperation.index + 1
|
|
2053
2574
|
};
|
|
2054
|
-
stores.writeCache.putState(job.documentId, scope, job.branch, storedOperation.index, updatedDocument);
|
|
2575
|
+
stores.writeCache.putState(job.documentId, scope, job.branch, storedOperation.index, updatedDocument, SnapshotPosition.Head);
|
|
2055
2576
|
indexTxn.write([{
|
|
2056
2577
|
...storedOperation,
|
|
2057
2578
|
documentId: job.documentId,
|
|
@@ -2078,13 +2599,43 @@ var SimpleJobExecutor = class {
|
|
|
2078
2599
|
duration: Date.now() - startTime
|
|
2079
2600
|
};
|
|
2080
2601
|
}
|
|
2602
|
+
/**
|
|
2603
|
+
* If an operation happened that could change a decision model verdict, we
|
|
2604
|
+
* need to re-evaluate across the streams that could be affected. Previously
|
|
2605
|
+
* approved operations may need to be denied, which will apply new denied
|
|
2606
|
+
* operations with appropriate skip.
|
|
2607
|
+
*/
|
|
2608
|
+
async reevaluateReadingScopes(job, startTime, indexTxn, stores, signal) {
|
|
2609
|
+
const definition = documentDecisionModel({
|
|
2610
|
+
documentId: job.documentId,
|
|
2611
|
+
branch: job.branch
|
|
2612
|
+
});
|
|
2613
|
+
const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
|
|
2614
|
+
for (const scope of Object.keys(revisions.revision)) {
|
|
2615
|
+
if (!definition.judgesScope(scope)) continue;
|
|
2616
|
+
const stored = (await stores.operationStore.getSince(job.documentId, scope, job.branch, -1, void 0, void 0, signal)).results;
|
|
2617
|
+
const effective = garbageCollect(sortOperations([...stored]));
|
|
2618
|
+
if (effective.length === 0) continue;
|
|
2619
|
+
const recomputed = await deletionVerdictsByPosition(job.documentId, scope, job.branch, effective, stores.writeCache, stores.operationStore, signal);
|
|
2620
|
+
const firstChange = effective.findIndex((operation, i) => operation.deniedReason !== recomputed[i]);
|
|
2621
|
+
if (firstChange === -1) continue;
|
|
2622
|
+
const tail = effective.slice(firstChange);
|
|
2623
|
+
const nextIndex = revisions.revision[scope];
|
|
2624
|
+
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2625
|
+
const result = await this.processActions({
|
|
2626
|
+
...job,
|
|
2627
|
+
scope
|
|
2628
|
+
}, tail.map((operation) => operation.action), startTime, indexTxn, stores, tail.map((_, i) => i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0), void 0, "", signal, recomputed.slice(firstChange));
|
|
2629
|
+
if (!result.success) return result.error ?? /* @__PURE__ */ new Error(`Re-evaluation of ${job.documentId} ${scope} failed`);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2081
2632
|
async executeLoadJob(job, startTime, indexTxn, stores, signal) {
|
|
2082
2633
|
if (job.operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("Load job must include at least one operation"), startTime);
|
|
2083
2634
|
let docMeta;
|
|
2084
2635
|
try {
|
|
2085
2636
|
docMeta = await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal);
|
|
2086
2637
|
} catch {}
|
|
2087
|
-
if (docMeta?.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2638
|
+
if (docMeta?.state.isDeleted && !this.featureFlags.documentDecisions) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
2088
2639
|
const scope = job.scope;
|
|
2089
2640
|
let latestRevision;
|
|
2090
2641
|
try {
|
|
@@ -2173,8 +2724,19 @@ var SimpleJobExecutor = class {
|
|
|
2173
2724
|
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP") operation.skip = 1;
|
|
2174
2725
|
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
2175
2726
|
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
2727
|
+
let deniedReasons;
|
|
2728
|
+
if (this.featureFlags.documentDecisions) try {
|
|
2729
|
+
deniedReasons = await deletionVerdictsByPosition(job.documentId, scope, job.branch, reshuffledOperations, stores.writeCache, stores.operationStore, signal);
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
return {
|
|
2732
|
+
job,
|
|
2733
|
+
success: false,
|
|
2734
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
2735
|
+
duration: Date.now() - startTime
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2176
2738
|
const effectiveSourceRemote = skipCount > 0 ? "" : job.meta.sourceRemote || "";
|
|
2177
|
-
const result = await this.processActions(job, actions, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote, signal);
|
|
2739
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, stores, skipValues, reshuffledOperations, effectiveSourceRemote, signal, deniedReasons);
|
|
2178
2740
|
if (!result.success) return {
|
|
2179
2741
|
job,
|
|
2180
2742
|
success: false,
|
|
@@ -2183,6 +2745,23 @@ var SimpleJobExecutor = class {
|
|
|
2183
2745
|
};
|
|
2184
2746
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
2185
2747
|
if (scope === "document") stores.documentMetaCache.invalidate(job.documentId, job.branch);
|
|
2748
|
+
const readsThisStream = staticReadSet(documentDecisionModel({
|
|
2749
|
+
documentId: job.documentId,
|
|
2750
|
+
branch: job.branch
|
|
2751
|
+
})).some((stream) => stream.query.documentId === job.documentId && stream.query.scope === scope && stream.query.branch === job.branch);
|
|
2752
|
+
if (this.featureFlags.documentDecisions && readsThisStream) {
|
|
2753
|
+
const revisions = await stores.operationStore.getRevisions(job.documentId, job.branch, signal);
|
|
2754
|
+
const latest = Date.parse(revisions.latestTimestamp);
|
|
2755
|
+
if (result.generatedOperations.some((operation) => Date.parse(operation.timestampUtcMs) < latest)) {
|
|
2756
|
+
const error = await this.reevaluateReadingScopes(job, startTime, indexTxn, stores, signal);
|
|
2757
|
+
if (error) return {
|
|
2758
|
+
job,
|
|
2759
|
+
success: false,
|
|
2760
|
+
error,
|
|
2761
|
+
duration: Date.now() - startTime
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2186
2765
|
return {
|
|
2187
2766
|
job,
|
|
2188
2767
|
success: true,
|
|
@@ -2419,36 +2998,6 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
2419
2998
|
};
|
|
2420
2999
|
}
|
|
2421
3000
|
//#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
3001
|
//#region src/storage/txn.ts
|
|
2453
3002
|
var AtomicTransaction = class {
|
|
2454
3003
|
operations = [];
|
|
@@ -2473,6 +3022,7 @@ var AtomicTransaction = class {
|
|
|
2473
3022
|
action: JSON.stringify(op.action),
|
|
2474
3023
|
skip: op.skip,
|
|
2475
3024
|
error: op.error || null,
|
|
3025
|
+
deniedReason: op.deniedReason || null,
|
|
2476
3026
|
hash: op.hash
|
|
2477
3027
|
});
|
|
2478
3028
|
}
|
|
@@ -2506,12 +3056,12 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2506
3056
|
instance.trx = trx;
|
|
2507
3057
|
return instance;
|
|
2508
3058
|
}
|
|
2509
|
-
async apply(documentId, documentType, scope, branch, revision, fn, signal) {
|
|
3059
|
+
async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
2510
3060
|
if (this.trx) {
|
|
2511
3061
|
let executeResult = null;
|
|
2512
3062
|
let uniqueCtx = null;
|
|
2513
3063
|
try {
|
|
2514
|
-
executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
3064
|
+
executeResult = await this.executeApply(this.trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
|
|
2515
3065
|
} catch (error) {
|
|
2516
3066
|
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
2517
3067
|
else throw error;
|
|
@@ -2523,7 +3073,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2523
3073
|
let uniqueCtx = null;
|
|
2524
3074
|
try {
|
|
2525
3075
|
transactionResult = await this.db.transaction().execute(async (trx) => {
|
|
2526
|
-
return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal);
|
|
3076
|
+
return this.executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition);
|
|
2527
3077
|
});
|
|
2528
3078
|
} catch (error) {
|
|
2529
3079
|
if (error instanceof _UniqueConstraintContext) uniqueCtx = error;
|
|
@@ -2542,12 +3092,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2542
3092
|
const op = ctx.stagedOps[0];
|
|
2543
3093
|
throw new DuplicateOperationError(`${op.opId} at index ${op.index} with skip ${op.skip}`);
|
|
2544
3094
|
}
|
|
2545
|
-
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
|
|
3095
|
+
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
2546
3096
|
throwIfAborted(signal);
|
|
2547
3097
|
const atomicTxn = new AtomicTransaction(documentId, documentType, scope, branch, revision);
|
|
2548
3098
|
await fn(atomicTxn);
|
|
2549
3099
|
const operations = atomicTxn.getOperations();
|
|
2550
3100
|
if (operations.length === 0) return [];
|
|
3101
|
+
if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
|
|
2551
3102
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
2552
3103
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
2553
3104
|
if (currentRevision !== revision - 1) {
|
|
@@ -2563,22 +3114,91 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2563
3114
|
op.prevOpId = prevOpId;
|
|
2564
3115
|
prevOpId = op.opId;
|
|
2565
3116
|
}
|
|
3117
|
+
let insertedCount = operations.length;
|
|
2566
3118
|
try {
|
|
2567
|
-
|
|
3119
|
+
if (condition && condition.streams.length > 0) insertedCount = await this.insertGuarded(trx, operations, condition);
|
|
3120
|
+
else await trx.insertInto("Operation").values(operations).execute();
|
|
2568
3121
|
} catch (error) {
|
|
2569
3122
|
if (error instanceof Error && error.message.includes("unique constraint")) throw new _UniqueConstraintContext(documentId, scope, branch, revision, operations);
|
|
2570
3123
|
throw error;
|
|
2571
3124
|
}
|
|
3125
|
+
if (insertedCount !== operations.length) throw new AppendConditionFailedError(condition);
|
|
2572
3126
|
return operations.map((op) => ({
|
|
2573
3127
|
index: op.index,
|
|
2574
3128
|
timestampUtcMs: op.timestampUtcMs.toISOString(),
|
|
2575
3129
|
hash: op.hash,
|
|
2576
3130
|
skip: op.skip,
|
|
2577
3131
|
error: op.error || void 0,
|
|
3132
|
+
deniedReason: op.deniedReason || void 0,
|
|
2578
3133
|
id: op.opId,
|
|
2579
3134
|
action: JSON.parse(op.action)
|
|
2580
3135
|
}));
|
|
2581
3136
|
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Locks the written stream and every read-set stream, in sorted key order
|
|
3139
|
+
* so that overlapping concurrent appends serialize rather than deadlock.
|
|
3140
|
+
* The locks are still taken one row at a time, so the query preserves that
|
|
3141
|
+
* order. It must stay separate from the guarded insert, which would
|
|
3142
|
+
* otherwise read a snapshot taken before the locks were held.
|
|
3143
|
+
*/
|
|
3144
|
+
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
3145
|
+
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
3146
|
+
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
3147
|
+
await sql`
|
|
3148
|
+
with ordered as materialized (
|
|
3149
|
+
select key
|
|
3150
|
+
from unnest(array[${sql.join([...keys].sort())}]::text[]) with ordinality as t(key, ord)
|
|
3151
|
+
order by ord
|
|
3152
|
+
)
|
|
3153
|
+
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
3154
|
+
`.execute(trx);
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Inserts the staged operations with the condition compiled in as a WHERE
|
|
3158
|
+
* NOT EXISTS guard, making the check and the append one statement. Returns
|
|
3159
|
+
* the rows inserted; zero means the guard failed and nothing was written.
|
|
3160
|
+
*/
|
|
3161
|
+
async insertGuarded(trx, operations, condition) {
|
|
3162
|
+
const branches = operations.map((op) => trx.selectNoFrom([
|
|
3163
|
+
sql`${op.jobId}::text`.as("jobId"),
|
|
3164
|
+
sql`${op.opId}::text`.as("opId"),
|
|
3165
|
+
sql`${op.prevOpId}::text`.as("prevOpId"),
|
|
3166
|
+
sql`${op.documentId}::text`.as("documentId"),
|
|
3167
|
+
sql`${op.documentType}::text`.as("documentType"),
|
|
3168
|
+
sql`${op.scope}::text`.as("scope"),
|
|
3169
|
+
sql`${op.branch}::text`.as("branch"),
|
|
3170
|
+
sql`${op.timestampUtcMs}::timestamptz`.as("timestampUtcMs"),
|
|
3171
|
+
sql`${op.index}::integer`.as("index"),
|
|
3172
|
+
sql`${op.action}::jsonb`.as("action"),
|
|
3173
|
+
sql`${op.skip}::integer`.as("skip"),
|
|
3174
|
+
sql`${op.error ?? null}::text`.as("error"),
|
|
3175
|
+
sql`${op.deniedReason ?? null}::text`.as("deniedReason"),
|
|
3176
|
+
sql`${op.hash}::text`.as("hash")
|
|
3177
|
+
]).where((eb) => eb.not(eb.exists(eb.selectFrom("Operation").select("Operation.id").where((web) => web.or(condition.streams.map((s) => web.and([
|
|
3178
|
+
web("Operation.documentId", "=", s.documentId),
|
|
3179
|
+
web("Operation.scope", "=", s.scope),
|
|
3180
|
+
web("Operation.branch", "=", s.branch),
|
|
3181
|
+
web("Operation.index", ">", s.revision)
|
|
3182
|
+
]))))))));
|
|
3183
|
+
let expression = branches[0];
|
|
3184
|
+
for (let i = 1; i < branches.length; i++) expression = expression.unionAll(branches[i]);
|
|
3185
|
+
return (await trx.insertInto("Operation").columns([
|
|
3186
|
+
"jobId",
|
|
3187
|
+
"opId",
|
|
3188
|
+
"prevOpId",
|
|
3189
|
+
"documentId",
|
|
3190
|
+
"documentType",
|
|
3191
|
+
"scope",
|
|
3192
|
+
"branch",
|
|
3193
|
+
"timestampUtcMs",
|
|
3194
|
+
"index",
|
|
3195
|
+
"action",
|
|
3196
|
+
"skip",
|
|
3197
|
+
"error",
|
|
3198
|
+
"deniedReason",
|
|
3199
|
+
"hash"
|
|
3200
|
+
]).expression(expression).returning("id").execute()).length;
|
|
3201
|
+
}
|
|
2582
3202
|
async findIdempotentReplay(executor, documentId, scope, branch, revision, stagedOps) {
|
|
2583
3203
|
const minIndex = revision;
|
|
2584
3204
|
const maxIndex = revision + stagedOps.length - 1;
|
|
@@ -2665,6 +3285,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
2665
3285
|
hash: row.hash,
|
|
2666
3286
|
skip: row.skip,
|
|
2667
3287
|
error: row.error || void 0,
|
|
3288
|
+
deniedReason: row.deniedReason || void 0,
|
|
2668
3289
|
id: row.opId,
|
|
2669
3290
|
action: row.action
|
|
2670
3291
|
};
|
|
@@ -2750,8 +3371,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
2750
3371
|
}
|
|
2751
3372
|
//#endregion
|
|
2752
3373
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
2753
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2754
|
-
async function up$
|
|
3374
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
3375
|
+
async function up$14(db) {
|
|
2755
3376
|
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
3377
|
"documentId",
|
|
2757
3378
|
"scope",
|
|
@@ -2776,8 +3397,8 @@ async function up$13(db) {
|
|
|
2776
3397
|
}
|
|
2777
3398
|
//#endregion
|
|
2778
3399
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
2779
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2780
|
-
async function up$
|
|
3400
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
3401
|
+
async function up$13(db) {
|
|
2781
3402
|
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
3403
|
"documentId",
|
|
2783
3404
|
"scope",
|
|
@@ -2793,14 +3414,14 @@ async function up$12(db) {
|
|
|
2793
3414
|
}
|
|
2794
3415
|
//#endregion
|
|
2795
3416
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
2796
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2797
|
-
async function up$
|
|
3417
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
3418
|
+
async function up$12(db) {
|
|
2798
3419
|
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
3420
|
}
|
|
2800
3421
|
//#endregion
|
|
2801
3422
|
//#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$
|
|
3423
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$11 });
|
|
3424
|
+
async function up$11(db) {
|
|
2804
3425
|
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
3426
|
"sourceId",
|
|
2806
3427
|
"targetId",
|
|
@@ -2812,14 +3433,14 @@ async function up$10(db) {
|
|
|
2812
3433
|
}
|
|
2813
3434
|
//#endregion
|
|
2814
3435
|
//#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$
|
|
3436
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$10 });
|
|
3437
|
+
async function up$10(db) {
|
|
2817
3438
|
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
3439
|
}
|
|
2819
3440
|
//#endregion
|
|
2820
3441
|
//#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$
|
|
3442
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$9 });
|
|
3443
|
+
async function up$9(db) {
|
|
2823
3444
|
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
3445
|
"documentId",
|
|
2825
3446
|
"scope",
|
|
@@ -2840,8 +3461,8 @@ async function up$8(db) {
|
|
|
2840
3461
|
}
|
|
2841
3462
|
//#endregion
|
|
2842
3463
|
//#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$
|
|
3464
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$8 });
|
|
3465
|
+
async function up$8(db) {
|
|
2845
3466
|
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
3467
|
"documentId",
|
|
2847
3468
|
"scope",
|
|
@@ -2851,14 +3472,14 @@ async function up$7(db) {
|
|
|
2851
3472
|
}
|
|
2852
3473
|
//#endregion
|
|
2853
3474
|
//#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$
|
|
3475
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$7 });
|
|
3476
|
+
async function up$7(db) {
|
|
2856
3477
|
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
3478
|
}
|
|
2858
3479
|
//#endregion
|
|
2859
3480
|
//#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$
|
|
3481
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$6 });
|
|
3482
|
+
async function up$6(db) {
|
|
2862
3483
|
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
3484
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
2864
3485
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -2872,8 +3493,8 @@ async function up$5(db) {
|
|
|
2872
3493
|
}
|
|
2873
3494
|
//#endregion
|
|
2874
3495
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
2875
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
2876
|
-
async function up$
|
|
3496
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$5 });
|
|
3497
|
+
async function up$5(db) {
|
|
2877
3498
|
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
3499
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
2879
3500
|
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 +3502,8 @@ async function up$4(db) {
|
|
|
2881
3502
|
}
|
|
2882
3503
|
//#endregion
|
|
2883
3504
|
//#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$
|
|
3505
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$4 });
|
|
3506
|
+
async function up$4(db) {
|
|
2886
3507
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
2887
3508
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
2888
3509
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -2891,24 +3512,43 @@ async function up$3(db) {
|
|
|
2891
3512
|
}
|
|
2892
3513
|
//#endregion
|
|
2893
3514
|
//#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$
|
|
3515
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$3 });
|
|
3516
|
+
async function up$3(db) {
|
|
2896
3517
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
2897
3518
|
}
|
|
2898
3519
|
//#endregion
|
|
2899
3520
|
//#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$
|
|
3521
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$2 });
|
|
3522
|
+
async function up$2(db) {
|
|
2902
3523
|
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
3524
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
2904
3525
|
}
|
|
2905
3526
|
//#endregion
|
|
2906
3527
|
//#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) {
|
|
3528
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$1 });
|
|
3529
|
+
async function up$1(db) {
|
|
2909
3530
|
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
3531
|
}
|
|
2911
3532
|
//#endregion
|
|
3533
|
+
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
3534
|
+
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
3535
|
+
down: () => down,
|
|
3536
|
+
up: () => up
|
|
3537
|
+
});
|
|
3538
|
+
/**
|
|
3539
|
+
* Records why authorization refused an operation. Separate from `error` so a
|
|
3540
|
+
* denial is distinguishable from a reducer failure without matching on a
|
|
3541
|
+
* message. Null for every operation written before decisions were enforced.
|
|
3542
|
+
*/
|
|
3543
|
+
async function up(db) {
|
|
3544
|
+
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
3545
|
+
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
3546
|
+
}
|
|
3547
|
+
async function down(db) {
|
|
3548
|
+
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
3549
|
+
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
3550
|
+
}
|
|
3551
|
+
//#endregion
|
|
2912
3552
|
//#region src/storage/migrations/migrator.ts
|
|
2913
3553
|
const REACTOR_SCHEMA = "reactor";
|
|
2914
3554
|
const migrations = {
|
|
@@ -2925,7 +3565,8 @@ const migrations = {
|
|
|
2925
3565
|
"011_add_cursor_type_column": _011_add_cursor_type_column_exports,
|
|
2926
3566
|
"012_add_source_remote_column": _012_add_source_remote_column_exports,
|
|
2927
3567
|
"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
|
|
3568
|
+
"014_create_processor_cursor_table": _014_create_processor_cursor_table_exports,
|
|
3569
|
+
"015_add_operation_denied_reason": _015_add_operation_denied_reason_exports
|
|
2929
3570
|
};
|
|
2930
3571
|
var ProgrammaticMigrationProvider = class {
|
|
2931
3572
|
getMigrations() {
|
|
@@ -2979,6 +3620,6 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
2979
3620
|
//#region src/core/drive-container-types.ts
|
|
2980
3621
|
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
2981
3622
|
//#endregion
|
|
2982
|
-
export {
|
|
3623
|
+
export { AuthorizationDeniedError as A, DuplicateOperationError as C, DuplicateModuleError as D, DuplicateManifestError as E, throwIfAborted as F, __exportAll as I, DocumentNotFoundError as M, matchesScope as N, InvalidModuleError as O, parsePagingOptions as P, AppendConditionFailedError as S, RevisionMismatchError as T, DocumentMetaCache as _, createForwardingPoolInstrumentation as a, CollectionMembershipCache as b, KyselyKeyframeStore as c, DriveCollectionId as d, buildDecisionModel as f, KyselyOperationIndex as g, KyselyWriteCache as h, runMigrations as i, DocumentDeletedError as j, ModuleNotFoundError as k, DocumentModelRegistry as l, EventBus as m, REACTOR_SCHEMA as n, instrumentPgPool as o, KyselyExecutionScope as p, getMigrationStatus as r, KyselyOperationStore as s, DEFAULT_DRIVE_CONTAINER_TYPES as t, SimpleJobExecutor as u, createConsistencyToken as v, OptimisticLockError as w, APPEND_CONDITION_FAILED_PREFIX as x, createEmptyConsistencyToken as y };
|
|
2983
3624
|
|
|
2984
|
-
//# sourceMappingURL=drive-container-types-
|
|
3625
|
+
//# sourceMappingURL=drive-container-types-BNwEHEXP.js.map
|