@powerhousedao/reactor 6.2.3-dev.34 → 6.2.3-dev.36
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-BonPsKdx.js → build-worker-executor-B9IfcRij.js} +8 -2
- package/dist/build-worker-executor-B9IfcRij.js.map +1 -0
- package/dist/{document-indexer-I6i_YzsR.js → document-indexer-CZqIpdz2.js} +124 -26
- package/dist/document-indexer-CZqIpdz2.js.map +1 -0
- package/dist/entry.js +5 -2
- package/dist/entry.js.map +1 -1
- package/dist/{forwarding-logger-BkwlvD9W.js → forwarding-logger-DMqDs9tE.js} +980 -148
- package/dist/forwarding-logger-DMqDs9tE.js.map +1 -0
- package/dist/index.d.ts +1006 -517
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1593 -215
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +42 -6
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CJIdjsd8.js → projection-shard-manager-BSK-GEyP.js} +2 -2
- package/dist/{projection-shard-manager-CJIdjsd8.js.map → projection-shard-manager-BSK-GEyP.js.map} +1 -1
- package/dist/{worker-handle-DjbEQkq-.js → worker-handle-weNGCZme.js} +9 -2
- package/dist/worker-handle-weNGCZme.js.map +1 -0
- package/package.json +4 -4
- package/dist/build-worker-executor-BonPsKdx.js.map +0 -1
- package/dist/document-indexer-I6i_YzsR.js.map +0 -1
- package/dist/forwarding-logger-BkwlvD9W.js.map +0 -1
- package/dist/worker-handle-DjbEQkq-.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PEER_CAPABILITIES, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isUndoRedo, isV2ActionHash, localSupports, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
|
|
1
|
+
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PEER_CAPABILITIES, PURGE_DOCUMENT, actionSignerIdentity, actionSigningTarget, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, base58Decode, baseReducerVersion, buildOperationSignatureMessage, buildOperationSignatureParams, canonicalJson, createDocumentState, createPresignedHeader, decide, defaultBaseState, deriveDocumentId, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashActionV2, hashBrowser, hashDocumentStateForScope, isDenied, isDerivedDocumentId, isPurgeMarker, isUndoRedo, isV2ActionHash, localSupports, mentionedGroupIds, normalizeDocumentModelVersion, operationOutcome, purgeDocumentAction, purgeMarkerOperation, purgedProtocolVersions, referencedGroupIds, signaturePolicyOf, sortOperations, targetDocumentId, v2TupleProblem, withProtocolVersions } from "@powerhousedao/shared/document-model";
|
|
2
2
|
import { v4 } from "uuid";
|
|
3
3
|
import { Migrator, TableNode, sql } from "kysely";
|
|
4
4
|
//#region \0rolldown/runtime.js
|
|
@@ -271,8 +271,81 @@ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
|
|
|
271
271
|
this.documentId = documentId;
|
|
272
272
|
Error.captureStackTrace(this, DocumentNotFoundError);
|
|
273
273
|
}
|
|
274
|
+
/** Also true for DocumentPurgedError: only the name crosses the queue. */
|
|
274
275
|
static isError(error) {
|
|
275
|
-
return Error.isError(error) && error.name === "DocumentNotFoundError";
|
|
276
|
+
return Error.isError(error) && (error.name === "DocumentNotFoundError" || error.name === "DocumentPurgedError");
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
/** The document was purged; readers treat it as absent. Terminal. */
|
|
280
|
+
var DocumentPurgedError = class DocumentPurgedError extends DocumentNotFoundError {
|
|
281
|
+
purged = true;
|
|
282
|
+
constructor(documentId, message) {
|
|
283
|
+
super(documentId, message ?? `Document ${documentId} was purged`);
|
|
284
|
+
this.name = "DocumentPurgedError";
|
|
285
|
+
Error.captureStackTrace(this, DocumentPurgedError);
|
|
286
|
+
}
|
|
287
|
+
static isError(error) {
|
|
288
|
+
return Error.isError(error) && error.name === "DocumentPurgedError";
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
/** A purge was asked for a document that is not deleted. Terminal. */
|
|
292
|
+
var DocumentNotDeletedError = class DocumentNotDeletedError extends Error {
|
|
293
|
+
documentId;
|
|
294
|
+
constructor(documentId, message) {
|
|
295
|
+
super(message ?? `Document ${documentId} is not deleted`);
|
|
296
|
+
this.name = "DocumentNotDeletedError";
|
|
297
|
+
this.documentId = documentId;
|
|
298
|
+
Error.captureStackTrace(this, DocumentNotDeletedError);
|
|
299
|
+
}
|
|
300
|
+
static isError(error) {
|
|
301
|
+
return Error.isError(error) && error.name === "DocumentNotDeletedError";
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/** A surviving document's accepted auth history names the group. Terminal. */
|
|
305
|
+
var GroupInUseError = class GroupInUseError extends Error {
|
|
306
|
+
groupId;
|
|
307
|
+
referencingDocumentIds;
|
|
308
|
+
constructor(groupId, referencingDocumentIds) {
|
|
309
|
+
super(`Group ${groupId} cannot be purged: the auth history of ${referencingDocumentIds.join(", ")} names it`);
|
|
310
|
+
this.name = "GroupInUseError";
|
|
311
|
+
this.groupId = groupId;
|
|
312
|
+
this.referencingDocumentIds = referencingDocumentIds;
|
|
313
|
+
Error.captureStackTrace(this, GroupInUseError);
|
|
314
|
+
}
|
|
315
|
+
static isError(error) {
|
|
316
|
+
return Error.isError(error) && error.name === "GroupInUseError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
/** A purge above maxPurgeOperations without allowLarge. Terminal. */
|
|
320
|
+
var PurgeTooLargeError = class PurgeTooLargeError extends Error {
|
|
321
|
+
documentId;
|
|
322
|
+
operationCount;
|
|
323
|
+
maxPurgeOperations;
|
|
324
|
+
constructor(documentId, operationCount, maxPurgeOperations) {
|
|
325
|
+
super(`Document ${documentId} has ${operationCount} operations, above the purge limit of ${maxPurgeOperations}; request it with allowLarge`);
|
|
326
|
+
this.name = "PurgeTooLargeError";
|
|
327
|
+
this.documentId = documentId;
|
|
328
|
+
this.operationCount = operationCount;
|
|
329
|
+
this.maxPurgeOperations = maxPurgeOperations;
|
|
330
|
+
Error.captureStackTrace(this, PurgeTooLargeError);
|
|
331
|
+
}
|
|
332
|
+
static isError(error) {
|
|
333
|
+
return Error.isError(error) && error.name === "PurgeTooLargeError";
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
/** A regular write carried an action type only the reactor issues. Terminal. */
|
|
337
|
+
var ReservedActionError = class ReservedActionError extends Error {
|
|
338
|
+
documentId;
|
|
339
|
+
actionType;
|
|
340
|
+
constructor(documentId, actionType) {
|
|
341
|
+
super(`Action ${actionType} is reserved and cannot be submitted to document ${documentId}`);
|
|
342
|
+
this.name = "ReservedActionError";
|
|
343
|
+
this.documentId = documentId;
|
|
344
|
+
this.actionType = actionType;
|
|
345
|
+
Error.captureStackTrace(this, ReservedActionError);
|
|
346
|
+
}
|
|
347
|
+
static isError(error) {
|
|
348
|
+
return Error.isError(error) && error.name === "ReservedActionError";
|
|
276
349
|
}
|
|
277
350
|
};
|
|
278
351
|
/**
|
|
@@ -490,9 +563,13 @@ async function buildDecisionModel(reader, definition, target, signal) {
|
|
|
490
563
|
model[key] = (await readStream(reader, projection.query, reads, signal)).state;
|
|
491
564
|
}
|
|
492
565
|
const staticModel = { ...model };
|
|
566
|
+
const derived = [];
|
|
493
567
|
for (const [key, projection] of projections) {
|
|
494
568
|
if (typeof projection.query !== "function") continue;
|
|
495
|
-
|
|
569
|
+
derived.push([key, projection.query(staticModel)]);
|
|
570
|
+
}
|
|
571
|
+
if (derived.length > 0 && reader.fence) await reader.fence(derived.flatMap(([, queries]) => queries.map((q) => q.documentId)));
|
|
572
|
+
for (const [key, queries] of derived) {
|
|
496
573
|
const value = {};
|
|
497
574
|
for (const query of queries) {
|
|
498
575
|
let read;
|
|
@@ -1047,6 +1124,95 @@ function summarizeSubmittedActions(operations, submitted) {
|
|
|
1047
1124
|
allApplied: actions.every((action) => action.kind === "applied")
|
|
1048
1125
|
};
|
|
1049
1126
|
}
|
|
1127
|
+
/** Tombstones a job has read, each under the shared lock it took first. */
|
|
1128
|
+
var PurgeFence = class {
|
|
1129
|
+
locks;
|
|
1130
|
+
checked;
|
|
1131
|
+
purged;
|
|
1132
|
+
constructor(locks, checked, purged) {
|
|
1133
|
+
this.locks = locks;
|
|
1134
|
+
this.checked = checked;
|
|
1135
|
+
this.purged = purged;
|
|
1136
|
+
}
|
|
1137
|
+
/** Locks and reads an id the job start did not resolve. */
|
|
1138
|
+
async isPurged(documentId) {
|
|
1139
|
+
await this.isPurgedMany([documentId]);
|
|
1140
|
+
return this.purged.has(documentId);
|
|
1141
|
+
}
|
|
1142
|
+
/** Locks and reads the unresolved ids in one lock and one lookup. */
|
|
1143
|
+
async isPurgedMany(documentIds) {
|
|
1144
|
+
const unchecked = [...new Set(documentIds)].filter((id) => !this.checked.has(id));
|
|
1145
|
+
if (unchecked.length > 0) {
|
|
1146
|
+
await this.locks.shared(unchecked);
|
|
1147
|
+
const found = await this.locks.purged(unchecked);
|
|
1148
|
+
for (const id of unchecked) this.checked.add(id);
|
|
1149
|
+
for (const id of found) this.purged.add(id);
|
|
1150
|
+
}
|
|
1151
|
+
return new Set(documentIds.filter((id) => this.purged.has(id)));
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
/** Locks and checks an id on its first read; a purged id reads as purged. */
|
|
1155
|
+
var FencedWriteCache = class {
|
|
1156
|
+
inner;
|
|
1157
|
+
purgeFence;
|
|
1158
|
+
evict;
|
|
1159
|
+
evicted = /* @__PURE__ */ new Set();
|
|
1160
|
+
constructor(inner, purgeFence, evict) {
|
|
1161
|
+
this.inner = inner;
|
|
1162
|
+
this.purgeFence = purgeFence;
|
|
1163
|
+
this.evict = evict;
|
|
1164
|
+
}
|
|
1165
|
+
/** Fences ids known before their reads in one round trip. */
|
|
1166
|
+
async fence(documentIds) {
|
|
1167
|
+
await this.purgeFence.isPurgedMany(documentIds);
|
|
1168
|
+
}
|
|
1169
|
+
async getState(documentId, scope, branch, targetRevision, signal) {
|
|
1170
|
+
if (await this.purgeFence.isPurged(documentId)) {
|
|
1171
|
+
if (!this.evicted.has(documentId)) {
|
|
1172
|
+
this.evicted.add(documentId);
|
|
1173
|
+
this.evict(documentId);
|
|
1174
|
+
}
|
|
1175
|
+
throw new DocumentPurgedError(documentId);
|
|
1176
|
+
}
|
|
1177
|
+
return this.inner.getState(documentId, scope, branch, targetRevision, signal);
|
|
1178
|
+
}
|
|
1179
|
+
putState(documentId, scope, branch, revision, document, position) {
|
|
1180
|
+
this.inner.putState(documentId, scope, branch, revision, document, position);
|
|
1181
|
+
}
|
|
1182
|
+
putRun(documentId, scope, branch, run) {
|
|
1183
|
+
this.inner.putRun(documentId, scope, branch, run);
|
|
1184
|
+
}
|
|
1185
|
+
invalidate(documentId, scope, branch) {
|
|
1186
|
+
return this.inner.invalidate(documentId, scope, branch);
|
|
1187
|
+
}
|
|
1188
|
+
clear() {
|
|
1189
|
+
this.inner.clear();
|
|
1190
|
+
}
|
|
1191
|
+
startup() {
|
|
1192
|
+
return this.inner.startup();
|
|
1193
|
+
}
|
|
1194
|
+
shutdown() {
|
|
1195
|
+
return this.inner.shutdown();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
/** The target whose membership an ADD or REMOVE_RELATIONSHIP writes. */
|
|
1199
|
+
function relationshipTarget(action) {
|
|
1200
|
+
if (action.type !== "ADD_RELATIONSHIP" && action.type !== "REMOVE_RELATIONSHIP") return;
|
|
1201
|
+
const target = action.input?.targetId;
|
|
1202
|
+
return typeof target === "string" && target.length > 0 ? target : void 0;
|
|
1203
|
+
}
|
|
1204
|
+
/** The ids a job's locks cover, sorted: its write ids and groups it names. */
|
|
1205
|
+
function jobWriteIds(job) {
|
|
1206
|
+
const ids = /* @__PURE__ */ new Set([job.documentId]);
|
|
1207
|
+
const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
|
|
1208
|
+
for (const action of actions) {
|
|
1209
|
+
if (DOCUMENT_SCOPE_ACTIONS.has(action.type)) ids.add(targetDocumentId(action, job.documentId));
|
|
1210
|
+
const target = relationshipTarget(action);
|
|
1211
|
+
if (target !== void 0) ids.add(target);
|
|
1212
|
+
if (action.scope === "auth") for (const groupId of mentionedGroupIds(action)) ids.add(groupId);
|
|
1213
|
+
}
|
|
1214
|
+
return [...ids].sort();
|
|
1215
|
+
}
|
|
1050
1216
|
//#endregion
|
|
1051
1217
|
//#region src/registry/errors.ts
|
|
1052
1218
|
/**
|
|
@@ -1129,6 +1295,8 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
1129
1295
|
};
|
|
1130
1296
|
//#endregion
|
|
1131
1297
|
//#region src/executor/types.ts
|
|
1298
|
+
/** 200k operations purge in 1.6 s on local Postgres; jobs time out at 30 s. */
|
|
1299
|
+
const DEFAULT_MAX_PURGE_OPERATIONS = 2e5;
|
|
1132
1300
|
/** How long a deferred job waits for its document before it fails. */
|
|
1133
1301
|
const DEFAULT_DEFERRED_JOB_TTL_MS = 3e4;
|
|
1134
1302
|
/**
|
|
@@ -1342,27 +1510,44 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1342
1510
|
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
|
|
1343
1511
|
if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1344
1512
|
const createOp = docScopeOps.results[0];
|
|
1513
|
+
if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
|
|
1345
1514
|
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
1346
1515
|
const createAction = createOp.action;
|
|
1347
1516
|
const documentType = createAction.input.model;
|
|
1348
1517
|
let document = createDocumentFromAction(createAction);
|
|
1349
1518
|
let documentScopeRevision = 0;
|
|
1519
|
+
let purged = false;
|
|
1350
1520
|
for (const op of docScopeOps.results) {
|
|
1351
1521
|
if (targetRevision !== void 0 && op.index > targetRevision) break;
|
|
1352
1522
|
documentScopeRevision = op.index;
|
|
1353
1523
|
if (op.action.type === "UPGRADE_DOCUMENT") {
|
|
1354
1524
|
const upgradeAction = op.action;
|
|
1355
1525
|
document = applyUpgradeDocumentAction$1(document, upgradeAction);
|
|
1356
|
-
} else if (op.action.type === "DELETE_DOCUMENT"
|
|
1526
|
+
} else if (op.action.type === "DELETE_DOCUMENT" || op.action.type === PURGE_DOCUMENT) {
|
|
1527
|
+
document = applyDeleteDocumentAction$1(document, op.action);
|
|
1528
|
+
purged ||= op.action.type === PURGE_DOCUMENT;
|
|
1529
|
+
}
|
|
1357
1530
|
}
|
|
1358
1531
|
return {
|
|
1359
1532
|
state: document.state.document,
|
|
1360
1533
|
documentType,
|
|
1361
|
-
protocolVersions: createAction.input.protocolVersions,
|
|
1534
|
+
protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
|
|
1362
1535
|
documentScopeRevision: documentScopeRevision + 1
|
|
1363
1536
|
};
|
|
1364
1537
|
}
|
|
1365
1538
|
};
|
|
1539
|
+
/** A lone marker has no CREATE_DOCUMENT; the marker describes the stream. */
|
|
1540
|
+
function purgedMeta(action) {
|
|
1541
|
+
return {
|
|
1542
|
+
state: createDocumentState({
|
|
1543
|
+
isDeleted: true,
|
|
1544
|
+
deletedAtUtcIso: action.input.purgedAtUtcIso
|
|
1545
|
+
}),
|
|
1546
|
+
documentType: action.input.documentType,
|
|
1547
|
+
protocolVersions: purgedProtocolVersions(),
|
|
1548
|
+
documentScopeRevision: 1
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1366
1551
|
//#endregion
|
|
1367
1552
|
//#region src/catch-up/settled-watermark.ts
|
|
1368
1553
|
const MAX_PENDING_PROBES = 64;
|
|
@@ -1569,6 +1754,36 @@ async function describeWaitingSessions(db, xids) {
|
|
|
1569
1754
|
return [];
|
|
1570
1755
|
}
|
|
1571
1756
|
}
|
|
1757
|
+
//#endregion
|
|
1758
|
+
//#region src/storage/kysely/document-purges.ts
|
|
1759
|
+
/** Class key of every purge lock; the stream locks use the single-key space. */
|
|
1760
|
+
const PURGE_NS = 1347571013;
|
|
1761
|
+
/** Ids share this many lock keys, so a job's lock count stays bounded. */
|
|
1762
|
+
const PURGE_LOCK_BUCKETS = 1024;
|
|
1763
|
+
async function acquirePurgeLocks(trx, ids, mode) {
|
|
1764
|
+
const unique = [...new Set(ids)];
|
|
1765
|
+
if (unique.length === 0) return;
|
|
1766
|
+
const lock = sql.raw(mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock");
|
|
1767
|
+
await sql`
|
|
1768
|
+
with keys as materialized (
|
|
1769
|
+
select distinct hashtext(id) & ${sql.lit(1023)} as key
|
|
1770
|
+
from unnest(${unique}::text[]) as t(id)
|
|
1771
|
+
order by key
|
|
1772
|
+
)
|
|
1773
|
+
select ${lock}(${sql.lit(PURGE_NS)}, key) from keys order by key
|
|
1774
|
+
`.execute(trx);
|
|
1775
|
+
}
|
|
1776
|
+
/** The ids among `ids` that have a tombstone, read in the handle's schema. */
|
|
1777
|
+
async function findPurged(db, ids) {
|
|
1778
|
+
const unique = [...new Set(ids)];
|
|
1779
|
+
if (unique.length === 0) return /* @__PURE__ */ new Set();
|
|
1780
|
+
const rows = await db.selectFrom("document_purges").select("documentId").where(sql`"documentId" = any(${unique}::text[])`).execute();
|
|
1781
|
+
return new Set(rows.map((row) => row.documentId));
|
|
1782
|
+
}
|
|
1783
|
+
/** Every tombstoned id, read in the handle's schema. */
|
|
1784
|
+
async function listPurged(db) {
|
|
1785
|
+
return (await db.selectFrom("document_purges").select("documentId").orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1786
|
+
}
|
|
1572
1787
|
/** Bind slots per statement: 16-bit on the wire, read signed in-process. */
|
|
1573
1788
|
const MAX_BIND_PARAMETERS = 32767;
|
|
1574
1789
|
/** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
|
|
@@ -1650,15 +1865,18 @@ var KyselyOperationIndexTxn = class {
|
|
|
1650
1865
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1651
1866
|
db;
|
|
1652
1867
|
trx;
|
|
1868
|
+
liveIds;
|
|
1653
1869
|
constructor(db) {
|
|
1654
1870
|
this.db = db;
|
|
1655
1871
|
}
|
|
1656
1872
|
get queryExecutor() {
|
|
1657
1873
|
return this.trx ?? this.db;
|
|
1658
1874
|
}
|
|
1659
|
-
|
|
1875
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
1876
|
+
withTransaction(trx, liveIds) {
|
|
1660
1877
|
const instance = new KyselyOperationIndex(this.db);
|
|
1661
1878
|
instance.trx = trx;
|
|
1879
|
+
instance.liveIds = liveIds;
|
|
1662
1880
|
return instance;
|
|
1663
1881
|
}
|
|
1664
1882
|
start() {
|
|
@@ -1709,6 +1927,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1709
1927
|
}
|
|
1710
1928
|
let operationOrdinals = [];
|
|
1711
1929
|
if (operations.length > 0) {
|
|
1930
|
+
await this.refusePurgedOperations(trx, operations);
|
|
1712
1931
|
await this.assignXid(trx);
|
|
1713
1932
|
const operationRows = operations.map((op) => ({
|
|
1714
1933
|
opId: op.id || "",
|
|
@@ -1760,6 +1979,13 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1760
1979
|
}
|
|
1761
1980
|
return operationOrdinals;
|
|
1762
1981
|
}
|
|
1982
|
+
async getCollectionsInRange(after, through, among, signal) {
|
|
1983
|
+
signal?.throwIfAborted();
|
|
1984
|
+
if (through <= after || among?.length === 0) return [];
|
|
1985
|
+
let query = this.queryExecutor.selectFrom("operation_index_operations as oi").innerJoin("document_collections as dc", "oi.documentId", "dc.documentId").select("dc.collectionId").distinct().where("oi.ordinal", ">", after).where("oi.ordinal", "<=", through);
|
|
1986
|
+
if (among !== void 0) query = query.where("dc.collectionId", "in", [...among]);
|
|
1987
|
+
return (await query.execute()).map((row) => row.collectionId);
|
|
1988
|
+
}
|
|
1763
1989
|
async getOrdinalsInRange(after, through, limit, signal) {
|
|
1764
1990
|
signal?.throwIfAborted();
|
|
1765
1991
|
if (through <= after || limit <= 0) return [];
|
|
@@ -1777,9 +2003,9 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1777
2003
|
results.sort((a, b) => a.context.ordinal - b.context.ordinal);
|
|
1778
2004
|
return results;
|
|
1779
2005
|
}
|
|
1780
|
-
async getStreamAfter(stream, after, signal) {
|
|
2006
|
+
async getStreamAfter(stream, after, signal, limit) {
|
|
1781
2007
|
signal?.throwIfAborted();
|
|
1782
|
-
return (await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", stream.documentId).where("branch", "=", stream.branch).where("scope", "=", stream.scope).where("ordinal", ">", after).orderBy("ordinal", "asc").execute()).map((row) => this.rowToOperationWithContext(row));
|
|
2008
|
+
return (await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("documentId", "=", stream.documentId).where("branch", "=", stream.branch).where("scope", "=", stream.scope).where("ordinal", ">", after).orderBy("ordinal", "asc").$if(limit !== void 0, (qb) => qb.limit(limit)).execute()).map((row) => this.rowToOperationWithContext(row));
|
|
1783
2009
|
}
|
|
1784
2010
|
async getGroupReferencers(groupId, signal) {
|
|
1785
2011
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -1893,6 +2119,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1893
2119
|
}, signal) : void 0
|
|
1894
2120
|
};
|
|
1895
2121
|
}
|
|
2122
|
+
/** Backstop to the store's refusal: only a marker indexes a purged id. */
|
|
2123
|
+
async refusePurgedOperations(trx, operations) {
|
|
2124
|
+
const ids = new Set(operations.filter((operation) => !isPurgeMarker(operation)).map((operation) => operation.documentId).filter((id) => !this.liveIds?.has(id)));
|
|
2125
|
+
const purged = await findPurged(trx, ids);
|
|
2126
|
+
for (const id of ids) if (purged.has(id)) throw new DocumentPurgedError(id);
|
|
2127
|
+
}
|
|
1896
2128
|
/** Takes the xid before the first ordinal, as the settled watermark needs. */
|
|
1897
2129
|
async assignXid(trx) {
|
|
1898
2130
|
let fn = xidFunctions.get(this.db);
|
|
@@ -2056,42 +2288,6 @@ function copyDocument(document) {
|
|
|
2056
2288
|
operations: { ...document.operations }
|
|
2057
2289
|
};
|
|
2058
2290
|
}
|
|
2059
|
-
/**
|
|
2060
|
-
* In-memory write cache with keyframe persistence for PHDocuments.
|
|
2061
|
-
*
|
|
2062
|
-
* Caches document snapshots in ring buffers with LRU eviction. On cache miss,
|
|
2063
|
-
* rebuilds documents from nearest keyframe or full operation history.
|
|
2064
|
-
*
|
|
2065
|
-
* **Performance Characteristics:**
|
|
2066
|
-
* - Cache hit: O(1) lookup in ring buffer
|
|
2067
|
-
* - Cold miss: O(n) where n is total operation count, or O(k) where k is operations since keyframe
|
|
2068
|
-
* - Warm miss: O(m) where m is operations since cached revision
|
|
2069
|
-
* - Eviction: O(1) for LRU tracking and removal
|
|
2070
|
-
*
|
|
2071
|
-
* **Thread Safety:**
|
|
2072
|
-
* Not thread-safe. Designed for single-threaded job executor environment.
|
|
2073
|
-
* External synchronization required for concurrent access across multiple executors.
|
|
2074
|
-
*
|
|
2075
|
-
* **Example:**
|
|
2076
|
-
* ```typescript
|
|
2077
|
-
* const cache = new KyselyWriteCache(
|
|
2078
|
-
* keyframeStore,
|
|
2079
|
-
* operationStore,
|
|
2080
|
-
* registry,
|
|
2081
|
-
* { maxDocuments: 1000, ringBufferSize: 10, keyframeInterval: 10 }
|
|
2082
|
-
* );
|
|
2083
|
-
*
|
|
2084
|
-
* await cache.startup();
|
|
2085
|
-
*
|
|
2086
|
-
* // Retrieve or rebuild document
|
|
2087
|
-
* const doc = await cache.getState(docId, docType, scope, branch, revision);
|
|
2088
|
-
*
|
|
2089
|
-
* // Cache result after job execution
|
|
2090
|
-
* cache.putState(docId, docType, scope, branch, newRevision, updatedDoc);
|
|
2091
|
-
*
|
|
2092
|
-
* await cache.shutdown();
|
|
2093
|
-
* ```
|
|
2094
|
-
*/
|
|
2095
2291
|
var KyselyWriteCache = class KyselyWriteCache {
|
|
2096
2292
|
streams;
|
|
2097
2293
|
lruTracker;
|
|
@@ -2099,6 +2295,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2099
2295
|
operationStore;
|
|
2100
2296
|
registry;
|
|
2101
2297
|
config;
|
|
2298
|
+
reads = /* @__PURE__ */ new Map();
|
|
2102
2299
|
constructor(keyframeStore, operationStore, registry, config) {
|
|
2103
2300
|
this.keyframeStore = keyframeStore;
|
|
2104
2301
|
this.operationStore = operationStore;
|
|
@@ -2115,6 +2312,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2115
2312
|
const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
|
|
2116
2313
|
scoped.streams = this.streams;
|
|
2117
2314
|
scoped.lruTracker = this.lruTracker;
|
|
2315
|
+
scoped.reads = this.reads;
|
|
2118
2316
|
return scoped;
|
|
2119
2317
|
}
|
|
2120
2318
|
/**
|
|
@@ -2148,11 +2346,27 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2148
2346
|
* @returns The document at the target revision
|
|
2149
2347
|
* @throws {Error} "Operation aborted" if signal is aborted
|
|
2150
2348
|
* @throws {ModuleNotFoundError} If document type not registered in registry
|
|
2349
|
+
* @throws {DocumentPurgedError} If the stream's only row is a purge marker
|
|
2151
2350
|
* @throws {Error} "Failed to rebuild document" if operation store fails
|
|
2152
2351
|
* @throws {Error} If reducer throws during operation application
|
|
2153
2352
|
* @throws {Error} If document serialization fails
|
|
2154
2353
|
*/
|
|
2155
2354
|
async getState(documentId, scope, branch, targetRevision, signal) {
|
|
2355
|
+
const read = { evicted: false };
|
|
2356
|
+
let pending = this.reads.get(documentId);
|
|
2357
|
+
if (!pending) {
|
|
2358
|
+
pending = /* @__PURE__ */ new Set();
|
|
2359
|
+
this.reads.set(documentId, pending);
|
|
2360
|
+
}
|
|
2361
|
+
pending.add(read);
|
|
2362
|
+
try {
|
|
2363
|
+
return await this.readState(documentId, scope, branch, targetRevision, signal, read);
|
|
2364
|
+
} finally {
|
|
2365
|
+
pending.delete(read);
|
|
2366
|
+
if (pending.size === 0) this.reads.delete(documentId);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
async readState(documentId, scope, branch, targetRevision, signal, read) {
|
|
2156
2370
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2157
2371
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
2158
2372
|
const stream = this.streams.get(streamKey);
|
|
@@ -2166,7 +2380,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2166
2380
|
}
|
|
2167
2381
|
if (newest) {
|
|
2168
2382
|
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
2169
|
-
this.
|
|
2383
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
2170
2384
|
this.lruTracker.touch(streamKey);
|
|
2171
2385
|
return document;
|
|
2172
2386
|
}
|
|
@@ -2179,7 +2393,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2179
2393
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
2180
2394
|
if (newestOlder) {
|
|
2181
2395
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
2182
|
-
this.
|
|
2396
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
|
|
2183
2397
|
this.lruTracker.touch(streamKey);
|
|
2184
2398
|
return document;
|
|
2185
2399
|
}
|
|
@@ -2187,7 +2401,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2187
2401
|
}
|
|
2188
2402
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
2189
2403
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
2190
|
-
this.
|
|
2404
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
2191
2405
|
return document;
|
|
2192
2406
|
}
|
|
2193
2407
|
/**
|
|
@@ -2224,6 +2438,9 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2224
2438
|
const head = run[run.length - 1];
|
|
2225
2439
|
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
2226
2440
|
}
|
|
2441
|
+
storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
|
|
2442
|
+
if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
|
|
2443
|
+
}
|
|
2227
2444
|
store(documentId, scope, branch, revision, document, position) {
|
|
2228
2445
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
2229
2446
|
const stream = this.getOrCreateStream(streamKey);
|
|
@@ -2264,6 +2481,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2264
2481
|
* @returns The number of streams evicted
|
|
2265
2482
|
*/
|
|
2266
2483
|
invalidate(documentId, scope, branch) {
|
|
2484
|
+
for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
|
|
2267
2485
|
let evicted = 0;
|
|
2268
2486
|
if (scope === void 0 && branch === void 0) {
|
|
2269
2487
|
for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
|
|
@@ -2292,6 +2510,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2292
2510
|
* Resets LRU tracking state. This operation always succeeds.
|
|
2293
2511
|
*/
|
|
2294
2512
|
clear() {
|
|
2513
|
+
for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
|
|
2295
2514
|
this.streams.clear();
|
|
2296
2515
|
this.lruTracker.clear();
|
|
2297
2516
|
}
|
|
@@ -2371,7 +2590,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2371
2590
|
subsequentDeletes: []
|
|
2372
2591
|
});
|
|
2373
2592
|
}
|
|
2374
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2593
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
2375
2594
|
applyDeleteDocumentAction(document, operation.action);
|
|
2376
2595
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2377
2596
|
}
|
|
@@ -2384,6 +2603,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2384
2603
|
}, signal);
|
|
2385
2604
|
if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
2386
2605
|
const createOp = createOpResult.results[0];
|
|
2606
|
+
if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
|
|
2387
2607
|
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}`);
|
|
2388
2608
|
const documentCreateAction = createOp.action;
|
|
2389
2609
|
documentType = documentCreateAction.input.model;
|
|
@@ -2423,7 +2643,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2423
2643
|
});
|
|
2424
2644
|
} else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
|
|
2425
2645
|
docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
|
|
2426
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2646
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
2427
2647
|
applyDeleteDocumentAction(document, operation.action);
|
|
2428
2648
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2429
2649
|
} else {
|
|
@@ -2751,7 +2971,154 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2751
2971
|
}
|
|
2752
2972
|
}
|
|
2753
2973
|
//#endregion
|
|
2974
|
+
//#region src/storage/kysely/document-purger.ts
|
|
2975
|
+
/** An `Operation` row whose DELETE_DOCUMENT applied: not errored, not denied. */
|
|
2976
|
+
const appliedDelete = sql`action->>'type' = 'DELETE_DOCUMENT' and coalesce(error, '') = '' and coalesce("deniedReason", '') = ''`;
|
|
2977
|
+
/** Rows removed per statement from the tables a document can fill. */
|
|
2978
|
+
const DEFAULT_PURGE_DELETE_BATCH = 5e3;
|
|
2979
|
+
/** A purge transaction's statements; bound to the job's transaction. */
|
|
2980
|
+
var KyselyDocumentPurger = class {
|
|
2981
|
+
batchSize;
|
|
2982
|
+
db;
|
|
2983
|
+
constructor(db, batchSize = DEFAULT_PURGE_DELETE_BATCH) {
|
|
2984
|
+
this.batchSize = batchSize;
|
|
2985
|
+
this.db = db;
|
|
2986
|
+
}
|
|
2987
|
+
/** Every stream the document holds operations in. */
|
|
2988
|
+
async streams(documentId) {
|
|
2989
|
+
return this.db.selectFrom("Operation").select(["scope", "branch"]).distinct().where("documentId", "=", documentId).orderBy("scope").orderBy("branch").execute();
|
|
2990
|
+
}
|
|
2991
|
+
async operationCount(documentId) {
|
|
2992
|
+
const row = await this.db.selectFrom("operation_index_operations").select((eb) => eb.fn.countAll().as("count")).where("documentId", "=", documentId).executeTakeFirst();
|
|
2993
|
+
return Number(row?.count ?? 0);
|
|
2994
|
+
}
|
|
2995
|
+
/** Untombstoned documents group_references records as naming the group. */
|
|
2996
|
+
async groupReferencers(groupId) {
|
|
2997
|
+
const ids = (await this.db.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).where("documentId", "<>", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
2998
|
+
const purged = await findPurged(this.db, ids);
|
|
2999
|
+
return ids.filter((id) => !purged.has(id));
|
|
3000
|
+
}
|
|
3001
|
+
/** Survivors whose accepted auth history names the group; refusals do not. */
|
|
3002
|
+
async groupReferencersInHistory(groupId) {
|
|
3003
|
+
const referencing = [];
|
|
3004
|
+
for (const documentId of await this.groupReferencers(groupId)) if ([...(await this.authOperations(documentId)).values()].some((operations) => garbageCollect(sortOperations(operations)).some((operation) => operation.deniedReason === void 0 && operation.error === void 0 && mentionedGroupIds(operation.action).includes(groupId)))) referencing.push(documentId);
|
|
3005
|
+
return referencing;
|
|
3006
|
+
}
|
|
3007
|
+
/** The document's auth-scope operations, per branch, in index order. */
|
|
3008
|
+
async authOperations(documentId) {
|
|
3009
|
+
const rows = await this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", "auth").orderBy("branch").orderBy("index").execute();
|
|
3010
|
+
const byBranch = /* @__PURE__ */ new Map();
|
|
3011
|
+
for (const row of rows) {
|
|
3012
|
+
const operations = byBranch.get(row.branch) ?? [];
|
|
3013
|
+
operations.push({
|
|
3014
|
+
index: row.index,
|
|
3015
|
+
timestampUtcMs: row.timestampUtcMs.toISOString(),
|
|
3016
|
+
hash: row.hash,
|
|
3017
|
+
skip: row.skip,
|
|
3018
|
+
error: row.error || void 0,
|
|
3019
|
+
deniedReason: row.deniedReason || void 0,
|
|
3020
|
+
id: row.opId,
|
|
3021
|
+
action: row.action
|
|
3022
|
+
});
|
|
3023
|
+
byBranch.set(row.branch, operations);
|
|
3024
|
+
}
|
|
3025
|
+
return byBranch;
|
|
3026
|
+
}
|
|
3027
|
+
/** Every document ever in the collection, but its owner and its own row. */
|
|
3028
|
+
async collectionMembers(collectionId, ownerId, branch) {
|
|
3029
|
+
return (await this.db.selectFrom("document_collections as dc").select("dc.documentId").select(sql`dc."leftOrdinal" is null`.as("openHere")).select((eb) => eb.exists(eb.selectFrom("Operation as op").select("op.opId").whereRef("op.documentId", "=", "dc.documentId").where("op.branch", "=", branch).where("op.scope", "=", "document").where(appliedDelete)).as("deleted")).select((eb) => eb.exists(eb.selectFrom("document_collections as other").select("other.documentId").whereRef("other.documentId", "=", "dc.documentId").where("other.collectionId", "<>", collectionId).where("other.leftOrdinal", "is", null)).as("openElsewhere")).where("dc.collectionId", "=", collectionId).where("dc.documentId", "not in", [ownerId, collectionId]).orderBy("dc.documentId").execute()).map((row) => {
|
|
3030
|
+
const openElsewhere = Boolean(row.openElsewhere);
|
|
3031
|
+
return {
|
|
3032
|
+
documentId: row.documentId,
|
|
3033
|
+
openElsewhere,
|
|
3034
|
+
required: !openElsewhere && (Boolean(row.openHere) || Boolean(row.deleted))
|
|
3035
|
+
};
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
async remoteCollection(remoteName) {
|
|
3039
|
+
return (await this.db.selectFrom("sync_remotes").select("collection_id").where("name", "=", remoteName).executeTakeFirst())?.collection_id;
|
|
3040
|
+
}
|
|
3041
|
+
/** Deletes every row about the document; the counts are per table. */
|
|
3042
|
+
async deleteRows(documentId) {
|
|
3043
|
+
const removed = {};
|
|
3044
|
+
removed.Operation = await this.deleteBatched(async () => {
|
|
3045
|
+
const result = await this.db.deleteFrom("Operation").where("id", "in", (eb) => eb.selectFrom("Operation").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3046
|
+
return Number(result.numDeletedRows);
|
|
3047
|
+
});
|
|
3048
|
+
removed.operation_index_operations = await this.deleteBatched(async () => {
|
|
3049
|
+
const result = await this.db.deleteFrom("operation_index_operations").where("ordinal", "in", (eb) => eb.selectFrom("operation_index_operations").select("ordinal").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3050
|
+
return Number(result.numDeletedRows);
|
|
3051
|
+
});
|
|
3052
|
+
removed.Keyframe = await this.deleteBatched(async () => {
|
|
3053
|
+
const result = await this.db.deleteFrom("Keyframe").where("id", "in", (eb) => eb.selectFrom("Keyframe").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3054
|
+
return Number(result.numDeletedRows);
|
|
3055
|
+
});
|
|
3056
|
+
removed.DocumentSnapshot = await this.deleteBatched(async () => {
|
|
3057
|
+
const result = await this.db.deleteFrom("DocumentSnapshot").where("id", "in", (eb) => eb.selectFrom("DocumentSnapshot").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3058
|
+
return Number(result.numDeletedRows);
|
|
3059
|
+
});
|
|
3060
|
+
removed.SlugMapping = await this.deleteWhere("SlugMapping", "documentId", documentId);
|
|
3061
|
+
const relationships = await this.db.selectFrom("DocumentRelationship").select((eb) => eb.fn.countAll().as("count")).where((eb) => eb.or([eb("sourceId", "=", documentId), eb("targetId", "=", documentId)])).executeTakeFirst();
|
|
3062
|
+
removed.DocumentRelationship = Number(relationships?.count ?? 0);
|
|
3063
|
+
removed.Document = await this.deleteWhere("Document", "id", documentId);
|
|
3064
|
+
removed.group_references = await this.deleteWhere("group_references", "documentId", documentId);
|
|
3065
|
+
removed.sync_dead_letters = await this.deleteWhere("sync_dead_letters", "document_id", documentId);
|
|
3066
|
+
removed.sync_holds = await this.deleteWhere("sync_holds", "document_id", documentId);
|
|
3067
|
+
return removed;
|
|
3068
|
+
}
|
|
3069
|
+
async writeTombstone(tombstone) {
|
|
3070
|
+
const values = {
|
|
3071
|
+
documentId: tombstone.documentId,
|
|
3072
|
+
ordinal: tombstone.ordinal,
|
|
3073
|
+
removedRows: JSON.stringify(tombstone.removedRows),
|
|
3074
|
+
purgedAtUtc: tombstone.purgedAtUtc,
|
|
3075
|
+
requestId: tombstone.requestId
|
|
3076
|
+
};
|
|
3077
|
+
await this.db.insertInto("document_purges").values(values).onConflict((oc) => oc.column("documentId").doUpdateSet({
|
|
3078
|
+
ordinal: values.ordinal,
|
|
3079
|
+
removedRows: values.removedRows,
|
|
3080
|
+
purgedAtUtc: values.purgedAtUtc,
|
|
3081
|
+
requestId: values.requestId
|
|
3082
|
+
})).execute();
|
|
3083
|
+
}
|
|
3084
|
+
/** Reopens every membership at the marker ordinal, plus `collectionIds`. */
|
|
3085
|
+
async reopenMemberships(documentId, ordinal, collectionIds = []) {
|
|
3086
|
+
await this.db.updateTable("document_collections").set({
|
|
3087
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3088
|
+
leftOrdinal: null
|
|
3089
|
+
}).where("documentId", "=", documentId).execute();
|
|
3090
|
+
for (const collectionId of new Set(collectionIds)) await this.db.insertInto("document_collections").values({
|
|
3091
|
+
documentId,
|
|
3092
|
+
collectionId,
|
|
3093
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3094
|
+
leftOrdinal: null
|
|
3095
|
+
}).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
|
|
3096
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3097
|
+
leftOrdinal: null
|
|
3098
|
+
})).execute();
|
|
3099
|
+
return (await this.db.selectFrom("document_collections").select("collectionId").where("documentId", "=", documentId).orderBy("collectionId").execute()).map((row) => row.collectionId);
|
|
3100
|
+
}
|
|
3101
|
+
async deleteBatched(step) {
|
|
3102
|
+
let total = 0;
|
|
3103
|
+
for (;;) {
|
|
3104
|
+
const deleted = await step();
|
|
3105
|
+
total += deleted;
|
|
3106
|
+
if (deleted < this.batchSize) return total;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
async deleteWhere(table, column, documentId) {
|
|
3110
|
+
const result = await this.db.deleteFrom(table).where(column, "=", documentId).executeTakeFirst();
|
|
3111
|
+
return Number(result.numDeletedRows);
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
//#endregion
|
|
2754
3115
|
//#region src/executor/execution-scope.ts
|
|
3116
|
+
/** For scopes without a transaction to hold a lock in. */
|
|
3117
|
+
const NOOP_DOCUMENT_LOCKS = {
|
|
3118
|
+
shared: () => Promise.resolve(),
|
|
3119
|
+
exclusive: () => Promise.resolve(),
|
|
3120
|
+
purged: () => Promise.resolve(/* @__PURE__ */ new Set())
|
|
3121
|
+
};
|
|
2755
3122
|
var DefaultExecutionScope = class {
|
|
2756
3123
|
operationStore;
|
|
2757
3124
|
operationIndex;
|
|
@@ -2772,7 +3139,8 @@ var DefaultExecutionScope = class {
|
|
|
2772
3139
|
operationIndex: this.operationIndex,
|
|
2773
3140
|
writeCache: this.writeCache,
|
|
2774
3141
|
documentMetaCache: this.documentMetaCache,
|
|
2775
|
-
collectionMembershipCache: this.collectionMembershipCache
|
|
3142
|
+
collectionMembershipCache: this.collectionMembershipCache,
|
|
3143
|
+
documentLocks: NOOP_DOCUMENT_LOCKS
|
|
2776
3144
|
});
|
|
2777
3145
|
}
|
|
2778
3146
|
};
|
|
@@ -2796,15 +3164,32 @@ var KyselyExecutionScope = class {
|
|
|
2796
3164
|
async run(fn, signal) {
|
|
2797
3165
|
signal?.throwIfAborted();
|
|
2798
3166
|
return this.db.transaction().execute(async (trx) => {
|
|
2799
|
-
const
|
|
2800
|
-
const
|
|
3167
|
+
const sharedLocked = /* @__PURE__ */ new Set();
|
|
3168
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
3169
|
+
const scopedOperationStore = this.operationStore.withTransaction(trx, liveIds);
|
|
3170
|
+
const scopedOperationIndex = this.operationIndex.withTransaction(trx, liveIds);
|
|
2801
3171
|
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
2802
3172
|
return fn({
|
|
2803
3173
|
operationStore: scopedOperationStore,
|
|
2804
3174
|
operationIndex: scopedOperationIndex,
|
|
2805
3175
|
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
2806
3176
|
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
2807
|
-
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
|
|
3177
|
+
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex),
|
|
3178
|
+
documentLocks: {
|
|
3179
|
+
shared: async (ids) => {
|
|
3180
|
+
const list = [...ids];
|
|
3181
|
+
await acquirePurgeLocks(trx, list, "shared");
|
|
3182
|
+
for (const id of list) sharedLocked.add(id);
|
|
3183
|
+
},
|
|
3184
|
+
exclusive: (id) => acquirePurgeLocks(trx, [id], "exclusive"),
|
|
3185
|
+
purged: async (ids) => {
|
|
3186
|
+
const list = [...ids];
|
|
3187
|
+
const found = await findPurged(trx, list);
|
|
3188
|
+
for (const id of list) if (sharedLocked.has(id) && !found.has(id)) liveIds.add(id);
|
|
3189
|
+
return found;
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
purger: new KyselyDocumentPurger(trx)
|
|
2808
3193
|
});
|
|
2809
3194
|
});
|
|
2810
3195
|
}
|
|
@@ -2818,6 +3203,7 @@ const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
2818
3203
|
"ADD_RELATIONSHIP",
|
|
2819
3204
|
"REMOVE_RELATIONSHIP",
|
|
2820
3205
|
"UPDATE_RELATIONSHIP",
|
|
3206
|
+
"PURGE_DOCUMENT",
|
|
2821
3207
|
"ADD_FOLDER",
|
|
2822
3208
|
"UPDATE_FOLDER",
|
|
2823
3209
|
"REMOVE_FOLDER"
|
|
@@ -3023,6 +3409,15 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
3023
3409
|
const derivedSet = derivedReadSet(definition);
|
|
3024
3410
|
if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
|
|
3025
3411
|
const evaluating = new Set(operations.map((operation) => operation.id));
|
|
3412
|
+
const creation = operations.find((operation) => operation.action.type === "CREATE_DOCUMENT" && operation.action.input.documentId === target.documentId);
|
|
3413
|
+
async function stateBefore(query) {
|
|
3414
|
+
try {
|
|
3415
|
+
return await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
if (creation !== void 0 && query.documentId === target.documentId && error instanceof DocumentNotFoundError && !(error instanceof DocumentPurgedError)) return createDocumentFromAction(creation.action);
|
|
3418
|
+
throw error;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3026
3421
|
const readStreams = await Promise.all(readSet.map(async (stream) => ({
|
|
3027
3422
|
stream,
|
|
3028
3423
|
operations: (await operationStore.getSince(stream.query.documentId, stream.query.scope, stream.query.branch, -1, { actionTypes: stream.decidingActions }, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id))
|
|
@@ -3035,7 +3430,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
3035
3430
|
const histories = [];
|
|
3036
3431
|
for (const read of readStreams) {
|
|
3037
3432
|
const streamOperations = read.stream === writtenProjection ? [...read.operations, ...operations] : read.operations;
|
|
3038
|
-
const before = await
|
|
3433
|
+
const before = await stateBefore(read.stream.query);
|
|
3039
3434
|
walked.push({
|
|
3040
3435
|
streamKey: streamKey(read.stream.query),
|
|
3041
3436
|
scope: read.stream.query.scope,
|
|
@@ -3057,7 +3452,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
3057
3452
|
branch: target.branch
|
|
3058
3453
|
};
|
|
3059
3454
|
const storedOperations = (await operationStore.getSince(query.documentId, query.scope, query.branch, -1, void 0, void 0, signal)).results.filter((operation) => !evaluating.has(operation.id));
|
|
3060
|
-
const before = await
|
|
3455
|
+
const before = await stateBefore(query);
|
|
3061
3456
|
evaluatedStateKey = streamKey(query);
|
|
3062
3457
|
walked.push({
|
|
3063
3458
|
streamKey: evaluatedStateKey,
|
|
@@ -3175,6 +3570,7 @@ var DocumentActionHandler = class {
|
|
|
3175
3570
|
case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
|
|
3176
3571
|
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
|
|
3177
3572
|
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(write, executing);
|
|
3573
|
+
case "PURGE_DOCUMENT": return buildErrorResult(executing.job, new ReservedActionError(executing.job.documentId, action.type), executing.startTime);
|
|
3178
3574
|
default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
|
|
3179
3575
|
}
|
|
3180
3576
|
}
|
|
@@ -3496,18 +3892,36 @@ var DocumentActionHandler = class {
|
|
|
3496
3892
|
});
|
|
3497
3893
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
3498
3894
|
}
|
|
3499
|
-
|
|
3895
|
+
/** A submitted write naming a purged target was refused at job start. */
|
|
3896
|
+
async isTargetPurged(write, executing) {
|
|
3897
|
+
const target = relationshipTarget(write.action);
|
|
3898
|
+
if (target === void 0 || !executing.purgeFence) return false;
|
|
3899
|
+
return executing.purgeFence.isPurged(target);
|
|
3900
|
+
}
|
|
3901
|
+
async executeAddRelationship(write, executing) {
|
|
3902
|
+
let targetPurged;
|
|
3903
|
+
try {
|
|
3904
|
+
targetPurged = await this.isTargetPurged(write, executing);
|
|
3905
|
+
} catch (error) {
|
|
3906
|
+
return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
|
|
3907
|
+
}
|
|
3500
3908
|
return this.withRelationshipAction("ADD_RELATIONSHIP", write, executing, (input) => input.sourceId === input.targetId ? /* @__PURE__ */ new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)") : null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
3501
|
-
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3909
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3502
3910
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3503
3911
|
txn.addToCollection(collectionId, input.targetId);
|
|
3504
3912
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
3505
3913
|
}
|
|
3506
3914
|
});
|
|
3507
3915
|
}
|
|
3508
|
-
executeRemoveRelationship(write, executing) {
|
|
3916
|
+
async executeRemoveRelationship(write, executing) {
|
|
3917
|
+
let targetPurged;
|
|
3918
|
+
try {
|
|
3919
|
+
targetPurged = await this.isTargetPurged(write, executing);
|
|
3920
|
+
} catch (error) {
|
|
3921
|
+
return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
|
|
3922
|
+
}
|
|
3509
3923
|
return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
3510
|
-
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3924
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3511
3925
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3512
3926
|
txn.removeFromCollection(collectionId, input.targetId);
|
|
3513
3927
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
@@ -3531,6 +3945,7 @@ var DocumentActionHandler = class {
|
|
|
3531
3945
|
try {
|
|
3532
3946
|
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
3533
3947
|
} catch (error) {
|
|
3948
|
+
if (DocumentPurgedError.isError(error)) return buildErrorResult(job, error, startTime);
|
|
3534
3949
|
if (DocumentNotFoundError.isError(error)) return buildErrorResult(job, new DocumentNotFoundError(input.sourceId, `${actionTypeName}: source document ${input.sourceId} not found`), startTime);
|
|
3535
3950
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3536
3951
|
}
|
|
@@ -4047,6 +4462,21 @@ var SignatureAdmission = class {
|
|
|
4047
4462
|
}
|
|
4048
4463
|
return dropped;
|
|
4049
4464
|
}
|
|
4465
|
+
/** A marker is held to v2 and enforced whatever the policy and mode. */
|
|
4466
|
+
async admitMarker(job, marker, signal) {
|
|
4467
|
+
const entry = {
|
|
4468
|
+
...candidate(marker.action, {
|
|
4469
|
+
documentId: job.documentId,
|
|
4470
|
+
scope: "document",
|
|
4471
|
+
branch: job.branch
|
|
4472
|
+
}),
|
|
4473
|
+
operation: marker,
|
|
4474
|
+
policy: "v2-required"
|
|
4475
|
+
};
|
|
4476
|
+
const verdict = await this.verdict(entry, /* @__PURE__ */ new Set(), void 0, "load", signal);
|
|
4477
|
+
if (verdict.ok) return;
|
|
4478
|
+
return this.record(job, entry, verdict, "load", true);
|
|
4479
|
+
}
|
|
4050
4480
|
async verdict(entry, live, submitted, path, signal, known = false) {
|
|
4051
4481
|
const verdict = await verifyActionSignature(entry.action, {
|
|
4052
4482
|
documentId: entry.stream.documentId,
|
|
@@ -4148,8 +4578,7 @@ var SignatureAdmission = class {
|
|
|
4148
4578
|
return live;
|
|
4149
4579
|
}
|
|
4150
4580
|
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
4151
|
-
record(job, entry, refusal, path) {
|
|
4152
|
-
const enforced = this.mode === "enforce";
|
|
4581
|
+
record(job, entry, refusal, path, enforced = this.mode === "enforce") {
|
|
4153
4582
|
const event = {
|
|
4154
4583
|
jobId: job.id,
|
|
4155
4584
|
documentId: entry.stream.documentId,
|
|
@@ -4274,6 +4703,30 @@ function isValidISOTimestamp(value) {
|
|
|
4274
4703
|
* returned JobResult there, which is what the queue, the worker protocol and
|
|
4275
4704
|
* every test expect a failed job to look like.
|
|
4276
4705
|
*/
|
|
4706
|
+
/** The ids of a purge job's request, which a drive purge may precede. */
|
|
4707
|
+
function purgeRequestDocumentIds(job) {
|
|
4708
|
+
const ids = job.meta.purgeRequestDocumentIds;
|
|
4709
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
4710
|
+
}
|
|
4711
|
+
const MARKER_INPUT_KEYS = /* @__PURE__ */ new Set([
|
|
4712
|
+
"documentId",
|
|
4713
|
+
"documentType",
|
|
4714
|
+
"purgedAtUtcIso",
|
|
4715
|
+
"requestId"
|
|
4716
|
+
]);
|
|
4717
|
+
/** A peer's marker must name this job's id on main and carry nothing else. */
|
|
4718
|
+
function malformedMarker(job) {
|
|
4719
|
+
const action = job.operations.find((operation) => isPurgeMarker(operation))?.action;
|
|
4720
|
+
const input = action?.input;
|
|
4721
|
+
const refuse = (reason) => new InvalidSignatureError(job.documentId, "ID_MISMATCH", `marker ${action?.id ?? "?"} ${reason}`);
|
|
4722
|
+
if (typeof input !== "object" || input === null) return refuse("carries no input");
|
|
4723
|
+
const extra = Object.keys(input).filter((key) => !MARKER_INPUT_KEYS.has(key));
|
|
4724
|
+
if (extra.length > 0) return refuse(`carries unexpected input ${extra.join(", ")}`);
|
|
4725
|
+
for (const key of MARKER_INPUT_KEYS) if (typeof input[key] !== "string" || input[key] === "") return refuse(`input.${key} is not a non-empty string`);
|
|
4726
|
+
const purgedAt = Date.parse(input.purgedAtUtcIso);
|
|
4727
|
+
if (Number.isNaN(purgedAt) || new Date(purgedAt).toISOString() !== input.purgedAtUtcIso) return refuse("input.purgedAtUtcIso is not an ISO timestamp");
|
|
4728
|
+
if (input.documentId !== job.documentId || job.branch !== "main" || action?.scope !== "document") return refuse(`does not purge ${job.documentId} on document/main`);
|
|
4729
|
+
}
|
|
4277
4730
|
var JobRollbackSignal = class extends Error {
|
|
4278
4731
|
result;
|
|
4279
4732
|
constructor(result) {
|
|
@@ -4326,7 +4779,8 @@ var SimpleJobExecutor = class {
|
|
|
4326
4779
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
4327
4780
|
batchApplies: config.batchApplies ?? true,
|
|
4328
4781
|
signatureVerification: config.signatureVerification ?? "enforce",
|
|
4329
|
-
protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols
|
|
4782
|
+
protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols,
|
|
4783
|
+
maxPurgeOperations: config.maxPurgeOperations ?? 2e5
|
|
4330
4784
|
};
|
|
4331
4785
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
4332
4786
|
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
@@ -4362,6 +4816,7 @@ var SimpleJobExecutor = class {
|
|
|
4362
4816
|
const touchedStreams = new TouchedStreams();
|
|
4363
4817
|
const postCommitInvalidations = [];
|
|
4364
4818
|
const postCommitMembershipInvalidations = [];
|
|
4819
|
+
const postCommitMetaInvalidations = [];
|
|
4365
4820
|
let outcome;
|
|
4366
4821
|
try {
|
|
4367
4822
|
outcome = await this.executionScope.run(async (stores) => {
|
|
@@ -4372,7 +4827,8 @@ var SimpleJobExecutor = class {
|
|
|
4372
4827
|
signal,
|
|
4373
4828
|
touchedStreams,
|
|
4374
4829
|
postCommitInvalidations,
|
|
4375
|
-
postCommitMembershipInvalidations
|
|
4830
|
+
postCommitMembershipInvalidations,
|
|
4831
|
+
postCommitMetaInvalidations
|
|
4376
4832
|
});
|
|
4377
4833
|
if (!scoped.result.success) throw new JobRollbackSignal(scoped.result);
|
|
4378
4834
|
return scoped;
|
|
@@ -4384,6 +4840,7 @@ var SimpleJobExecutor = class {
|
|
|
4384
4840
|
}
|
|
4385
4841
|
for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
|
|
4386
4842
|
for (const documentId of postCommitMembershipInvalidations) this.collectionMembershipCache.invalidate(documentId);
|
|
4843
|
+
for (const documentId of postCommitMetaInvalidations) this.documentMetaCache.invalidate(documentId);
|
|
4387
4844
|
const { pendingEvent } = outcome;
|
|
4388
4845
|
if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
|
|
4389
4846
|
this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
|
|
@@ -4399,8 +4856,36 @@ var SimpleJobExecutor = class {
|
|
|
4399
4856
|
* has not committed yet has nothing to announce.
|
|
4400
4857
|
*/
|
|
4401
4858
|
async executeInScope(params) {
|
|
4402
|
-
const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
4859
|
+
const { job, startTime, stores: scopeStores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
4403
4860
|
let pendingEvent;
|
|
4861
|
+
if (job.kind === "mutation") {
|
|
4862
|
+
const reserved = job.actions.find((action) => isPurgeMarker(action));
|
|
4863
|
+
if (reserved) return { result: buildErrorResult(job, new ReservedActionError(job.documentId, reserved.type), startTime) };
|
|
4864
|
+
}
|
|
4865
|
+
if (job.kind === "purge" || job.kind === "load" && job.operations.some((op) => isPurgeMarker(op))) return this.executePurge(params);
|
|
4866
|
+
const lockedIds = jobWriteIds(job);
|
|
4867
|
+
const lockFailure = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
|
|
4868
|
+
try {
|
|
4869
|
+
await scopeStores.documentLocks.shared(lockedIds);
|
|
4870
|
+
} catch (error) {
|
|
4871
|
+
return lockFailure(error);
|
|
4872
|
+
}
|
|
4873
|
+
let purged;
|
|
4874
|
+
try {
|
|
4875
|
+
purged = await scopeStores.documentLocks.purged(lockedIds);
|
|
4876
|
+
} catch (error) {
|
|
4877
|
+
return lockFailure(error);
|
|
4878
|
+
}
|
|
4879
|
+
const purgedRefusal = this.purgedRefusal(job, purged);
|
|
4880
|
+
if (purgedRefusal) return { result: buildErrorResult(job, purgedRefusal, startTime) };
|
|
4881
|
+
const purgeFence = new PurgeFence(scopeStores.documentLocks, new Set(lockedIds), purged);
|
|
4882
|
+
const stores = {
|
|
4883
|
+
...scopeStores,
|
|
4884
|
+
writeCache: new FencedWriteCache(scopeStores.writeCache, purgeFence, (documentId) => {
|
|
4885
|
+
scopeStores.writeCache.invalidate(documentId);
|
|
4886
|
+
scopeStores.documentMetaCache.invalidate(documentId);
|
|
4887
|
+
})
|
|
4888
|
+
};
|
|
4404
4889
|
const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
|
|
4405
4890
|
if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
|
|
4406
4891
|
const indexTxn = stores.operationIndex.start();
|
|
@@ -4415,7 +4900,8 @@ var SimpleJobExecutor = class {
|
|
|
4415
4900
|
evaluatedByPosition: false,
|
|
4416
4901
|
postCommitInvalidations,
|
|
4417
4902
|
postCommitMembershipInvalidations,
|
|
4418
|
-
touchedStreams
|
|
4903
|
+
touchedStreams,
|
|
4904
|
+
purgeFence
|
|
4419
4905
|
});
|
|
4420
4906
|
if (loadResult.success && loadResult.operationsWithContext) {
|
|
4421
4907
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -4445,7 +4931,8 @@ var SimpleJobExecutor = class {
|
|
|
4445
4931
|
evaluatedByPosition: false,
|
|
4446
4932
|
postCommitInvalidations,
|
|
4447
4933
|
postCommitMembershipInvalidations,
|
|
4448
|
-
touchedStreams
|
|
4934
|
+
touchedStreams,
|
|
4935
|
+
purgeFence
|
|
4449
4936
|
});
|
|
4450
4937
|
if (reevalResult.success && reevalResult.operationsWithContext) {
|
|
4451
4938
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -4497,7 +4984,8 @@ var SimpleJobExecutor = class {
|
|
|
4497
4984
|
evaluatedByPosition: positioned.evaluatedByPosition,
|
|
4498
4985
|
postCommitInvalidations,
|
|
4499
4986
|
postCommitMembershipInvalidations,
|
|
4500
|
-
touchedStreams
|
|
4987
|
+
touchedStreams,
|
|
4988
|
+
purgeFence
|
|
4501
4989
|
};
|
|
4502
4990
|
const actionResult = await this.processActions(positioned.writes, executing);
|
|
4503
4991
|
if (!actionResult.success) return { result: {
|
|
@@ -4521,6 +5009,264 @@ var SimpleJobExecutor = class {
|
|
|
4521
5009
|
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
4522
5010
|
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
4523
5011
|
}
|
|
5012
|
+
/** A load names only its own id purged; a foreign one is ID_MISMATCH. */
|
|
5013
|
+
purgedRefusal(job, purged) {
|
|
5014
|
+
if (purged.size === 0) return;
|
|
5015
|
+
if (purged.has(job.documentId)) return new DocumentPurgedError(job.documentId);
|
|
5016
|
+
const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
|
|
5017
|
+
for (const action of actions) {
|
|
5018
|
+
if (!DOCUMENT_SCOPE_ACTIONS.has(action.type)) continue;
|
|
5019
|
+
const target = targetDocumentId(action, job.documentId);
|
|
5020
|
+
if (!purged.has(target)) continue;
|
|
5021
|
+
if (job.kind === "mutation") return new DocumentPurgedError(target);
|
|
5022
|
+
return new InvalidSignatureError(job.documentId, "ID_MISMATCH", `${action.type} ${action.id} in ${job.documentId} writes purged ${target}`);
|
|
5023
|
+
}
|
|
5024
|
+
if (job.kind !== "mutation") return;
|
|
5025
|
+
for (const action of job.actions) {
|
|
5026
|
+
const target = relationshipTarget(action);
|
|
5027
|
+
if (action.type === "ADD_RELATIONSHIP" && target !== void 0 && purged.has(target)) return new DocumentPurgedError(target, `${action.type} target ${target} was purged`);
|
|
5028
|
+
if (action.scope !== "auth") continue;
|
|
5029
|
+
const group = mentionedGroupIds(action).find((id) => purged.has(id));
|
|
5030
|
+
if (group !== void 0) return new DocumentPurgedError(group, `${action.type} names purged group ${group}`);
|
|
5031
|
+
}
|
|
5032
|
+
}
|
|
5033
|
+
/** A purge job or a marker load: the exclusive lock is the job's only lock. */
|
|
5034
|
+
async executePurge(params) {
|
|
5035
|
+
const { job, startTime, stores } = params;
|
|
5036
|
+
const fail = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
|
|
5037
|
+
if (job.kind === "load") {
|
|
5038
|
+
const malformed = malformedMarker(job);
|
|
5039
|
+
if (malformed) return fail(malformed);
|
|
5040
|
+
}
|
|
5041
|
+
const purger = stores.purger;
|
|
5042
|
+
if (!purger) return fail(/* @__PURE__ */ new Error(`Purge of ${job.documentId} needs a transactional scope`));
|
|
5043
|
+
try {
|
|
5044
|
+
await stores.documentLocks.exclusive(job.documentId);
|
|
5045
|
+
} catch (error) {
|
|
5046
|
+
return fail(error);
|
|
5047
|
+
}
|
|
5048
|
+
let alreadyPurged;
|
|
5049
|
+
try {
|
|
5050
|
+
alreadyPurged = (await stores.documentLocks.purged([job.documentId])).has(job.documentId);
|
|
5051
|
+
} catch (error) {
|
|
5052
|
+
return fail(error);
|
|
5053
|
+
}
|
|
5054
|
+
if (alreadyPurged) return {
|
|
5055
|
+
result: {
|
|
5056
|
+
job,
|
|
5057
|
+
success: true,
|
|
5058
|
+
operations: [],
|
|
5059
|
+
operationsWithContext: [],
|
|
5060
|
+
duration: Date.now() - startTime
|
|
5061
|
+
},
|
|
5062
|
+
pendingEvent: {
|
|
5063
|
+
jobId: job.id,
|
|
5064
|
+
operations: [],
|
|
5065
|
+
jobMeta: job.meta,
|
|
5066
|
+
collectionMemberships: {}
|
|
5067
|
+
}
|
|
5068
|
+
};
|
|
5069
|
+
let commit;
|
|
5070
|
+
try {
|
|
5071
|
+
commit = job.kind === "purge" ? await this.preparePurgeJob(params, purger) : await this.preparePurgeLoad(params, purger);
|
|
5072
|
+
} catch (error) {
|
|
5073
|
+
return fail(error);
|
|
5074
|
+
}
|
|
5075
|
+
if (commit instanceof Error) return fail(commit);
|
|
5076
|
+
try {
|
|
5077
|
+
return await this.commitPurge(params, commit);
|
|
5078
|
+
} catch (error) {
|
|
5079
|
+
return fail(error);
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
/** Preconditions 1-5, then the marker this host signs. */
|
|
5083
|
+
async preparePurgeJob(params, purger) {
|
|
5084
|
+
const { job, stores, signal } = params;
|
|
5085
|
+
const documentId = job.documentId;
|
|
5086
|
+
if (!job.purge) return /* @__PURE__ */ new Error(`Purge job ${job.id} carries no purge options`);
|
|
5087
|
+
const held = await this.heldStreams(purger, documentId);
|
|
5088
|
+
if (held.branches.length === 0) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not held here`);
|
|
5089
|
+
let documentType;
|
|
5090
|
+
for (const branch of held.branches) {
|
|
5091
|
+
let meta;
|
|
5092
|
+
try {
|
|
5093
|
+
meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
5094
|
+
} catch (error) {
|
|
5095
|
+
return new DocumentNotDeletedError(documentId, `Document ${documentId} has no readable state on ${branch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5096
|
+
}
|
|
5097
|
+
if (!meta.state.isDeleted) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not deleted on branch ${branch}`);
|
|
5098
|
+
documentType ??= meta.documentType;
|
|
5099
|
+
}
|
|
5100
|
+
if (documentType === void 0) return new DocumentNotDeletedError(documentId);
|
|
5101
|
+
if (documentType === groupDocumentType) {
|
|
5102
|
+
const referencers = await purger.groupReferencersInHistory(documentId);
|
|
5103
|
+
if (referencers.length > 0) return new GroupInUseError(documentId, referencers);
|
|
5104
|
+
}
|
|
5105
|
+
let survivors = [];
|
|
5106
|
+
if (this.driveContainerTypes.has(documentType)) {
|
|
5107
|
+
const requested = new Set(purgeRequestDocumentIds(job));
|
|
5108
|
+
const members = await this.driveMembers(purger, stores, documentId, held.branches);
|
|
5109
|
+
const blocking = members.required.filter((id) => !requested.has(id));
|
|
5110
|
+
if (blocking.length > 0) return new DocumentNotDeletedError(documentId, `Drive ${documentId} cannot be purged: ${blocking.join(", ")} neither purged nor in the request`);
|
|
5111
|
+
survivors = members.survivors;
|
|
5112
|
+
}
|
|
5113
|
+
const operationCount = await purger.operationCount(documentId);
|
|
5114
|
+
if (operationCount > this.config.maxPurgeOperations && !job.purge.allowLarge) return new PurgeTooLargeError(documentId, operationCount, this.config.maxPurgeOperations);
|
|
5115
|
+
return {
|
|
5116
|
+
purger,
|
|
5117
|
+
marker: await this.signMarker(documentId, documentType, job.purge.requestId, signal),
|
|
5118
|
+
documentType,
|
|
5119
|
+
held,
|
|
5120
|
+
sourceRemote: "",
|
|
5121
|
+
collectionIds: [],
|
|
5122
|
+
survivors
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
/** A peer's marker, admitted alone; a receiver cannot refuse an erasure. */
|
|
5126
|
+
async preparePurgeLoad(params, purger) {
|
|
5127
|
+
const { job, stores, signal } = params;
|
|
5128
|
+
const documentId = job.documentId;
|
|
5129
|
+
const marker = job.operations.find((operation) => isPurgeMarker(operation));
|
|
5130
|
+
const refusal = await this.signatureAdmission.admitMarker(job, marker, signal);
|
|
5131
|
+
if (refusal) return refusal;
|
|
5132
|
+
const held = await this.heldStreams(purger, documentId);
|
|
5133
|
+
let documentType = marker.action.input.documentType;
|
|
5134
|
+
let appliedDeletion = held.branches.length > 0;
|
|
5135
|
+
if (held.branches.length > 0) try {
|
|
5136
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, held.branches.includes("main") ? "main" : held.branches[0], signal);
|
|
5137
|
+
documentType = meta.documentType;
|
|
5138
|
+
appliedDeletion = !meta.state.isDeleted;
|
|
5139
|
+
} catch {}
|
|
5140
|
+
let survivors = [];
|
|
5141
|
+
if (this.driveContainerTypes.has(documentType)) {
|
|
5142
|
+
const members = await this.driveMembers(purger, stores, documentId, held.branches);
|
|
5143
|
+
survivors = [...members.required, ...members.survivors];
|
|
5144
|
+
}
|
|
5145
|
+
const sourceRemote = typeof job.meta.sourceRemote === "string" ? job.meta.sourceRemote : "";
|
|
5146
|
+
const collectionId = sourceRemote ? await purger.remoteCollection(sourceRemote) : void 0;
|
|
5147
|
+
return {
|
|
5148
|
+
purger,
|
|
5149
|
+
marker: purgeMarkerOperation(marker.action),
|
|
5150
|
+
documentType,
|
|
5151
|
+
held,
|
|
5152
|
+
sourceRemote,
|
|
5153
|
+
collectionIds: collectionId === void 0 ? [] : [collectionId],
|
|
5154
|
+
survivors,
|
|
5155
|
+
...appliedDeletion ? { appliedDeletion: true } : {}
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
/** Execution: delete, write the marker and its twin, tombstone, reopen. */
|
|
5159
|
+
async commitPurge(params, commit) {
|
|
5160
|
+
const { job, startTime, stores, signal, touchedStreams } = params;
|
|
5161
|
+
const { purger, marker, documentType, held, sourceRemote } = commit;
|
|
5162
|
+
const documentId = job.documentId;
|
|
5163
|
+
const streams = [...held.streams, {
|
|
5164
|
+
scope: "document",
|
|
5165
|
+
branch: "main"
|
|
5166
|
+
}];
|
|
5167
|
+
for (const stream of streams) {
|
|
5168
|
+
touchedStreams.add(documentId, stream.scope, stream.branch);
|
|
5169
|
+
params.postCommitInvalidations.push({
|
|
5170
|
+
documentId,
|
|
5171
|
+
...stream
|
|
5172
|
+
});
|
|
5173
|
+
}
|
|
5174
|
+
params.postCommitMetaInvalidations.push(documentId);
|
|
5175
|
+
const removedRows = await purger.deleteRows(documentId);
|
|
5176
|
+
const [stored] = await stores.operationStore.apply(documentId, documentType, "document", "main", 0, (txn) => {
|
|
5177
|
+
txn.addOperations(marker);
|
|
5178
|
+
}, signal);
|
|
5179
|
+
const indexTxn = stores.operationIndex.start();
|
|
5180
|
+
indexTxn.write([{
|
|
5181
|
+
...stored,
|
|
5182
|
+
documentId,
|
|
5183
|
+
documentType,
|
|
5184
|
+
scope: "document",
|
|
5185
|
+
branch: "main",
|
|
5186
|
+
sourceRemote
|
|
5187
|
+
}]);
|
|
5188
|
+
const [ordinal] = await stores.operationIndex.commit(indexTxn, signal);
|
|
5189
|
+
await purger.writeTombstone({
|
|
5190
|
+
documentId,
|
|
5191
|
+
ordinal,
|
|
5192
|
+
removedRows,
|
|
5193
|
+
purgedAtUtc: marker.action.input.purgedAtUtcIso,
|
|
5194
|
+
requestId: marker.action.input.requestId
|
|
5195
|
+
});
|
|
5196
|
+
const collections = await purger.reopenMemberships(documentId, ordinal, commit.collectionIds);
|
|
5197
|
+
params.postCommitMembershipInvalidations.push(documentId, ...commit.survivors, ...indexTxn.getMembershipInvalidations());
|
|
5198
|
+
const operationWithContext = {
|
|
5199
|
+
operation: stored,
|
|
5200
|
+
context: {
|
|
5201
|
+
documentId,
|
|
5202
|
+
scope: "document",
|
|
5203
|
+
branch: "main",
|
|
5204
|
+
documentType,
|
|
5205
|
+
ordinal,
|
|
5206
|
+
...commit.appliedDeletion ? { appliedDeletion: true } : {}
|
|
5207
|
+
}
|
|
5208
|
+
};
|
|
5209
|
+
return {
|
|
5210
|
+
result: {
|
|
5211
|
+
job,
|
|
5212
|
+
success: true,
|
|
5213
|
+
operations: [stored],
|
|
5214
|
+
operationsWithContext: [operationWithContext],
|
|
5215
|
+
duration: Date.now() - startTime
|
|
5216
|
+
},
|
|
5217
|
+
pendingEvent: {
|
|
5218
|
+
jobId: job.id,
|
|
5219
|
+
operations: [operationWithContext],
|
|
5220
|
+
jobMeta: job.meta,
|
|
5221
|
+
collectionMemberships: { [documentId]: collections }
|
|
5222
|
+
}
|
|
5223
|
+
};
|
|
5224
|
+
}
|
|
5225
|
+
async heldStreams(purger, documentId) {
|
|
5226
|
+
const streams = await purger.streams(documentId);
|
|
5227
|
+
return {
|
|
5228
|
+
streams,
|
|
5229
|
+
branches: [...new Set(streams.filter((stream) => stream.scope === "document").map((stream) => stream.branch))]
|
|
5230
|
+
};
|
|
5231
|
+
}
|
|
5232
|
+
/** Members a drive purge needs gone, and those living on elsewhere. */
|
|
5233
|
+
async driveMembers(purger, stores, driveId, branches) {
|
|
5234
|
+
const required = /* @__PURE__ */ new Set();
|
|
5235
|
+
const survivors = /* @__PURE__ */ new Set();
|
|
5236
|
+
for (const branch of branches.length > 0 ? branches : ["main"]) {
|
|
5237
|
+
const collectionId = DriveCollectionId.forDrive(driveId, branch).key;
|
|
5238
|
+
const members = await purger.collectionMembers(collectionId, driveId, branch);
|
|
5239
|
+
const purged = await stores.documentLocks.purged(members.map((member) => member.documentId));
|
|
5240
|
+
for (const member of members) {
|
|
5241
|
+
if (purged.has(member.documentId)) continue;
|
|
5242
|
+
if (member.required) required.add(member.documentId);
|
|
5243
|
+
else survivors.add(member.documentId);
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
return {
|
|
5247
|
+
required: [...required],
|
|
5248
|
+
survivors: [...survivors]
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
/** The marker, signed by this executor's signer as any peer admits it. */
|
|
5252
|
+
async signMarker(documentId, documentType, requestId, signal) {
|
|
5253
|
+
const action = purgeDocumentAction({
|
|
5254
|
+
documentId,
|
|
5255
|
+
documentType,
|
|
5256
|
+
requestId
|
|
5257
|
+
});
|
|
5258
|
+
const signature = await this.signer.signAction(action, {
|
|
5259
|
+
documentId,
|
|
5260
|
+
branch: "main"
|
|
5261
|
+
}, signal);
|
|
5262
|
+
return purgeMarkerOperation({
|
|
5263
|
+
...action,
|
|
5264
|
+
context: { signer: {
|
|
5265
|
+
...actionSignerIdentity(this.signer),
|
|
5266
|
+
signatures: [signature]
|
|
5267
|
+
} }
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
4524
5270
|
/** A stored document at a version this reactor does not run is read-only here. */
|
|
4525
5271
|
async unsupportedStoredProtocol(job, stores, signal) {
|
|
4526
5272
|
let versions;
|
|
@@ -5671,19 +6417,28 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
|
5671
6417
|
}
|
|
5672
6418
|
async putKeyframe(documentId, scope, branch, revision, document, signal) {
|
|
5673
6419
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
5674
|
-
|
|
5675
|
-
documentId,
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
6420
|
+
const write = async (trx) => {
|
|
6421
|
+
await acquirePurgeLocks(trx, [documentId], "shared");
|
|
6422
|
+
if ((await findPurged(trx, [documentId])).has(documentId)) return;
|
|
6423
|
+
await trx.insertInto("Keyframe").values({
|
|
6424
|
+
documentId,
|
|
6425
|
+
documentType: document.header.documentType,
|
|
6426
|
+
scope,
|
|
6427
|
+
branch,
|
|
6428
|
+
revision,
|
|
6429
|
+
document
|
|
6430
|
+
}).onConflict((oc) => oc.columns([
|
|
6431
|
+
"documentId",
|
|
6432
|
+
"scope",
|
|
6433
|
+
"branch",
|
|
6434
|
+
"revision"
|
|
6435
|
+
]).doUpdateSet({ document })).execute();
|
|
6436
|
+
};
|
|
6437
|
+
if (this.trx) {
|
|
6438
|
+
await write(this.trx);
|
|
6439
|
+
return;
|
|
6440
|
+
}
|
|
6441
|
+
await this.db.transaction().execute(write);
|
|
5687
6442
|
}
|
|
5688
6443
|
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
5689
6444
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -5797,15 +6552,18 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
5797
6552
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
5798
6553
|
db;
|
|
5799
6554
|
trx;
|
|
6555
|
+
liveIds;
|
|
5800
6556
|
constructor(db) {
|
|
5801
6557
|
this.db = db;
|
|
5802
6558
|
}
|
|
5803
6559
|
get queryExecutor() {
|
|
5804
6560
|
return this.trx ?? this.db;
|
|
5805
6561
|
}
|
|
5806
|
-
|
|
6562
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
6563
|
+
withTransaction(trx, liveIds) {
|
|
5807
6564
|
const instance = new KyselyOperationStore(this.db);
|
|
5808
6565
|
instance.trx = trx;
|
|
6566
|
+
instance.liveIds = liveIds;
|
|
5809
6567
|
return instance;
|
|
5810
6568
|
}
|
|
5811
6569
|
async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
@@ -5850,6 +6608,8 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
5850
6608
|
await fn(atomicTxn);
|
|
5851
6609
|
const operations = atomicTxn.getOperations();
|
|
5852
6610
|
if (operations.length === 0) return [];
|
|
6611
|
+
const purgedHead = this.liveIds?.has(documentId) ? void 0 : await this.refusePurgedAppend(trx, documentId, operations);
|
|
6612
|
+
if (purgedHead !== void 0) return purgedHead;
|
|
5853
6613
|
if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
|
|
5854
6614
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
5855
6615
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
@@ -5965,6 +6725,24 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
5965
6725
|
}
|
|
5966
6726
|
return storedRows.map((row) => this.rowToOperation(row));
|
|
5967
6727
|
}
|
|
6728
|
+
/** A purged stream refuses appends; a marker gets the stored one back. */
|
|
6729
|
+
async refusePurgedAppend(trx, documentId, operations) {
|
|
6730
|
+
if (!(await findPurged(trx, [documentId])).has(documentId)) return;
|
|
6731
|
+
if (!operations.every((operation) => this.isMarker(operation))) throw new DocumentPurgedError(documentId);
|
|
6732
|
+
return (await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where(sql`action->>'type' = ${PURGE_DOCUMENT}`).orderBy("index", "asc").execute()).map((row) => this.rowToOperation(row));
|
|
6733
|
+
}
|
|
6734
|
+
isMarker(operation) {
|
|
6735
|
+
return this.actionType(operation) === PURGE_DOCUMENT;
|
|
6736
|
+
}
|
|
6737
|
+
actionType(operation) {
|
|
6738
|
+
let action = operation.action;
|
|
6739
|
+
if (typeof action === "string") try {
|
|
6740
|
+
action = JSON.parse(action);
|
|
6741
|
+
} catch {
|
|
6742
|
+
return;
|
|
6743
|
+
}
|
|
6744
|
+
if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string") return action.type;
|
|
6745
|
+
}
|
|
5968
6746
|
/** True when the staged write creates a document rather than appending to one. */
|
|
5969
6747
|
isCreate(operations) {
|
|
5970
6748
|
for (const operation of operations) {
|
|
@@ -6162,8 +6940,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
6162
6940
|
}
|
|
6163
6941
|
//#endregion
|
|
6164
6942
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
6165
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6166
|
-
async function up$
|
|
6943
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
|
|
6944
|
+
async function up$25(db) {
|
|
6167
6945
|
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", [
|
|
6168
6946
|
"documentId",
|
|
6169
6947
|
"scope",
|
|
@@ -6188,8 +6966,8 @@ async function up$22(db) {
|
|
|
6188
6966
|
}
|
|
6189
6967
|
//#endregion
|
|
6190
6968
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
6191
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6192
|
-
async function up$
|
|
6969
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
|
|
6970
|
+
async function up$24(db) {
|
|
6193
6971
|
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", [
|
|
6194
6972
|
"documentId",
|
|
6195
6973
|
"scope",
|
|
@@ -6205,14 +6983,14 @@ async function up$21(db) {
|
|
|
6205
6983
|
}
|
|
6206
6984
|
//#endregion
|
|
6207
6985
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
6208
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6209
|
-
async function up$
|
|
6986
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
|
|
6987
|
+
async function up$23(db) {
|
|
6210
6988
|
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();
|
|
6211
6989
|
}
|
|
6212
6990
|
//#endregion
|
|
6213
6991
|
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
6214
|
-
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6215
|
-
async function up$
|
|
6992
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
|
|
6993
|
+
async function up$22(db) {
|
|
6216
6994
|
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", [
|
|
6217
6995
|
"sourceId",
|
|
6218
6996
|
"targetId",
|
|
@@ -6224,14 +7002,14 @@ async function up$19(db) {
|
|
|
6224
7002
|
}
|
|
6225
7003
|
//#endregion
|
|
6226
7004
|
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
6227
|
-
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6228
|
-
async function up$
|
|
7005
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
|
|
7006
|
+
async function up$21(db) {
|
|
6229
7007
|
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();
|
|
6230
7008
|
}
|
|
6231
7009
|
//#endregion
|
|
6232
7010
|
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
6233
|
-
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6234
|
-
async function up$
|
|
7011
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
|
|
7012
|
+
async function up$20(db) {
|
|
6235
7013
|
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", [
|
|
6236
7014
|
"documentId",
|
|
6237
7015
|
"scope",
|
|
@@ -6252,8 +7030,8 @@ async function up$17(db) {
|
|
|
6252
7030
|
}
|
|
6253
7031
|
//#endregion
|
|
6254
7032
|
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
6255
|
-
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6256
|
-
async function up$
|
|
7033
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
|
|
7034
|
+
async function up$19(db) {
|
|
6257
7035
|
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", [
|
|
6258
7036
|
"documentId",
|
|
6259
7037
|
"scope",
|
|
@@ -6263,14 +7041,14 @@ async function up$16(db) {
|
|
|
6263
7041
|
}
|
|
6264
7042
|
//#endregion
|
|
6265
7043
|
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
6266
|
-
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6267
|
-
async function up$
|
|
7044
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
|
|
7045
|
+
async function up$18(db) {
|
|
6268
7046
|
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();
|
|
6269
7047
|
}
|
|
6270
7048
|
//#endregion
|
|
6271
7049
|
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
6272
|
-
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6273
|
-
async function up$
|
|
7050
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
|
|
7051
|
+
async function up$17(db) {
|
|
6274
7052
|
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();
|
|
6275
7053
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
6276
7054
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -6284,8 +7062,8 @@ async function up$14(db) {
|
|
|
6284
7062
|
}
|
|
6285
7063
|
//#endregion
|
|
6286
7064
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
6287
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6288
|
-
async function up$
|
|
7065
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
|
|
7066
|
+
async function up$16(db) {
|
|
6289
7067
|
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();
|
|
6290
7068
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
6291
7069
|
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();
|
|
@@ -6293,8 +7071,8 @@ async function up$13(db) {
|
|
|
6293
7071
|
}
|
|
6294
7072
|
//#endregion
|
|
6295
7073
|
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
6296
|
-
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6297
|
-
async function up$
|
|
7074
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
7075
|
+
async function up$15(db) {
|
|
6298
7076
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
6299
7077
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
6300
7078
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -6303,64 +7081,64 @@ async function up$12(db) {
|
|
|
6303
7081
|
}
|
|
6304
7082
|
//#endregion
|
|
6305
7083
|
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
6306
|
-
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6307
|
-
async function up$
|
|
7084
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
7085
|
+
async function up$14(db) {
|
|
6308
7086
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
6309
7087
|
}
|
|
6310
7088
|
//#endregion
|
|
6311
7089
|
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
6312
|
-
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6313
|
-
async function up$
|
|
7090
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
7091
|
+
async function up$13(db) {
|
|
6314
7092
|
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();
|
|
6315
7093
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
6316
7094
|
}
|
|
6317
7095
|
//#endregion
|
|
6318
7096
|
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
6319
|
-
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
6320
|
-
async function up$
|
|
7097
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
7098
|
+
async function up$12(db) {
|
|
6321
7099
|
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();
|
|
6322
7100
|
}
|
|
6323
7101
|
//#endregion
|
|
6324
7102
|
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
6325
7103
|
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
6326
|
-
down: () => down$
|
|
6327
|
-
up: () => up$
|
|
7104
|
+
down: () => down$11,
|
|
7105
|
+
up: () => up$11
|
|
6328
7106
|
});
|
|
6329
7107
|
/**
|
|
6330
7108
|
* Records why authorization refused an operation. Separate from `error` so a
|
|
6331
7109
|
* denial is distinguishable from a reducer failure without matching on a
|
|
6332
7110
|
* message. Null for every operation written before decisions were enforced.
|
|
6333
7111
|
*/
|
|
6334
|
-
async function up$
|
|
7112
|
+
async function up$11(db) {
|
|
6335
7113
|
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
6336
7114
|
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
6337
7115
|
}
|
|
6338
|
-
async function down$
|
|
7116
|
+
async function down$11(db) {
|
|
6339
7117
|
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
6340
7118
|
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
6341
7119
|
}
|
|
6342
7120
|
//#endregion
|
|
6343
7121
|
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
6344
7122
|
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
6345
|
-
down: () => down$
|
|
6346
|
-
up: () => up$
|
|
7123
|
+
down: () => down$10,
|
|
7124
|
+
up: () => up$10
|
|
6347
7125
|
});
|
|
6348
7126
|
/**
|
|
6349
7127
|
* The classification a dead letter falls into, stored because it decides whether
|
|
6350
7128
|
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
6351
7129
|
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
6352
7130
|
*/
|
|
6353
|
-
async function up$
|
|
7131
|
+
async function up$10(db) {
|
|
6354
7132
|
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
6355
7133
|
}
|
|
6356
|
-
async function down$
|
|
7134
|
+
async function down$10(db) {
|
|
6357
7135
|
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
6358
7136
|
}
|
|
6359
7137
|
//#endregion
|
|
6360
7138
|
//#region src/storage/migrations/017_create_group_references.ts
|
|
6361
7139
|
var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
6362
|
-
down: () => down$
|
|
6363
|
-
up: () => up$
|
|
7140
|
+
down: () => down$9,
|
|
7141
|
+
up: () => up$9
|
|
6364
7142
|
});
|
|
6365
7143
|
/**
|
|
6366
7144
|
* One row per (document, group) reference ever discovered from an auth
|
|
@@ -6371,18 +7149,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
|
6371
7149
|
* requires (sync), and by groupId for the documents a group change affects
|
|
6372
7150
|
* (re-evaluation).
|
|
6373
7151
|
*/
|
|
6374
|
-
async function up$
|
|
7152
|
+
async function up$9(db) {
|
|
6375
7153
|
await db.schema.createTable("group_references").addColumn("documentId", "text", (col) => col.notNull()).addColumn("groupId", "text", (col) => col.notNull()).addPrimaryKeyConstraint("group_references_pkey", ["documentId", "groupId"]).execute();
|
|
6376
7154
|
await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
|
|
6377
7155
|
}
|
|
6378
|
-
async function down$
|
|
7156
|
+
async function down$9(db) {
|
|
6379
7157
|
await db.schema.dropTable("group_references").execute();
|
|
6380
7158
|
}
|
|
6381
7159
|
//#endregion
|
|
6382
7160
|
//#region src/storage/migrations/018_add_sync_remote_bound_address.ts
|
|
6383
7161
|
var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
6384
|
-
down: () => down$
|
|
6385
|
-
up: () => up$
|
|
7162
|
+
down: () => down$8,
|
|
7163
|
+
up: () => up$8
|
|
6386
7164
|
});
|
|
6387
7165
|
/**
|
|
6388
7166
|
* The address a sync channel is bound to, so a channel created by one subject
|
|
@@ -6393,17 +7171,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
|
6393
7171
|
* until its first authenticated poll adopts it. A default would claim them all
|
|
6394
7172
|
* for one address.
|
|
6395
7173
|
*/
|
|
6396
|
-
async function up$
|
|
7174
|
+
async function up$8(db) {
|
|
6397
7175
|
await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
|
|
6398
7176
|
}
|
|
6399
|
-
async function down$
|
|
7177
|
+
async function down$8(db) {
|
|
6400
7178
|
await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
|
|
6401
7179
|
}
|
|
6402
7180
|
//#endregion
|
|
6403
7181
|
//#region src/storage/migrations/019_require_action_id.ts
|
|
6404
7182
|
var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
6405
|
-
down: () => down$
|
|
6406
|
-
up: () => up$
|
|
7183
|
+
down: () => down$7,
|
|
7184
|
+
up: () => up$7
|
|
6407
7185
|
});
|
|
6408
7186
|
/**
|
|
6409
7187
|
* Makes an operation whose action carries no id physically unstorable.
|
|
@@ -6430,7 +7208,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
|
6430
7208
|
* The empty string is rejected alongside null. It derives the same colliding
|
|
6431
7209
|
* operation id as an absent id, so admitting it would leave the hole open.
|
|
6432
7210
|
*/
|
|
6433
|
-
async function up$
|
|
7211
|
+
async function up$7(db) {
|
|
6434
7212
|
await db.updateTable("Operation").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
|
|
6435
7213
|
await db.updateTable("operation_index_operations as oio").from("Operation as op").set({ action: sql`jsonb_set(oio.action, '{id}', to_jsonb(op.action->>'id'))` }).whereRef("oio.opId", "=", "op.opId").whereRef("oio.index", "=", "op.index").whereRef("oio.skip", "=", "op.skip").where(sql`jsonb_typeof(oio.action) = 'object' and coalesce(oio.action->>'id', '') = ''`).where(sql`coalesce(op.action->>'id', '') <> ''`).execute();
|
|
6436
7214
|
await db.updateTable("operation_index_operations").set({ action: sql`jsonb_set(action, '{id}', to_jsonb(gen_random_uuid()::text))` }).where(sql`jsonb_typeof(action) = 'object' and coalesce(action->>'id', '') = ''`).execute();
|
|
@@ -6442,74 +7220,118 @@ async function up$4(db) {
|
|
|
6442
7220
|
* their operations are now known by, and reverting them would reintroduce the
|
|
6443
7221
|
* collision the migration removed.
|
|
6444
7222
|
*/
|
|
6445
|
-
async function down$
|
|
7223
|
+
async function down$7(db) {
|
|
6446
7224
|
await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
|
|
6447
7225
|
await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
|
|
6448
7226
|
}
|
|
6449
7227
|
//#endregion
|
|
6450
7228
|
//#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
|
|
6451
7229
|
var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
|
|
6452
|
-
down: () => down$
|
|
6453
|
-
up: () => up$
|
|
7230
|
+
down: () => down$6,
|
|
7231
|
+
up: () => up$6
|
|
6454
7232
|
});
|
|
6455
7233
|
/** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
|
|
6456
|
-
async function up$
|
|
7234
|
+
async function up$6(db) {
|
|
6457
7235
|
await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
6458
7236
|
}
|
|
6459
|
-
async function down$
|
|
7237
|
+
async function down$6(db) {
|
|
6460
7238
|
await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
|
|
6461
7239
|
}
|
|
6462
7240
|
//#endregion
|
|
6463
7241
|
//#region src/storage/migrations/021_add_sync_remote_peer.ts
|
|
6464
7242
|
var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
|
|
6465
|
-
down: () => down$
|
|
6466
|
-
up: () => up$
|
|
7243
|
+
down: () => down$5,
|
|
7244
|
+
up: () => up$5
|
|
6467
7245
|
});
|
|
6468
7246
|
/**
|
|
6469
7247
|
* The peer's manifest as canonical JSON, and when it was heard. Both null: not
|
|
6470
7248
|
* yet heard. A time with no manifest: a silent peer, which gets the baselines.
|
|
6471
7249
|
*/
|
|
6472
|
-
async function up$
|
|
7250
|
+
async function up$5(db) {
|
|
6473
7251
|
await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
|
|
6474
7252
|
}
|
|
6475
|
-
async function down$
|
|
7253
|
+
async function down$5(db) {
|
|
6476
7254
|
await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
|
|
6477
7255
|
}
|
|
6478
7256
|
//#endregion
|
|
6479
7257
|
//#region src/storage/migrations/022_create_sync_holds.ts
|
|
6480
7258
|
var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
|
|
6481
|
-
down: () => down$
|
|
6482
|
-
up: () => up$
|
|
7259
|
+
down: () => down$4,
|
|
7260
|
+
up: () => up$4
|
|
6483
7261
|
});
|
|
6484
7262
|
/** Documents held back from a remote whose peer cannot run their versions. */
|
|
6485
|
-
async function up$
|
|
7263
|
+
async function up$4(db) {
|
|
6486
7264
|
await db.schema.createTable("sync_holds").addColumn("remote_name", "text", (col) => col.notNull().references("sync_remotes.name").onDelete("cascade")).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("protocol", "text", (col) => col.notNull()).addColumn("version", "integer", (col) => col.notNull()).addColumn("held_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_holds_pkey", [
|
|
6487
7265
|
"remote_name",
|
|
6488
7266
|
"document_id",
|
|
6489
7267
|
"branch"
|
|
6490
7268
|
]).execute();
|
|
6491
7269
|
}
|
|
6492
|
-
async function down$
|
|
7270
|
+
async function down$4(db) {
|
|
6493
7271
|
await db.schema.dropTable("sync_holds").execute();
|
|
6494
7272
|
}
|
|
6495
7273
|
//#endregion
|
|
6496
7274
|
//#region src/storage/migrations/023_index_created_protocol_versions.ts
|
|
6497
7275
|
var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
|
|
6498
|
-
down: () => down,
|
|
6499
|
-
up: () => up
|
|
7276
|
+
down: () => down$3,
|
|
7277
|
+
up: () => up$3
|
|
6500
7278
|
});
|
|
6501
7279
|
/**
|
|
6502
7280
|
* One entry per document created with protocolVersions, keyed by a hash so an
|
|
6503
7281
|
* oversized header cannot make its creation unstorable. Startup reads the
|
|
6504
7282
|
* distinct values without visiting every document.
|
|
6505
7283
|
*/
|
|
6506
|
-
async function up(db) {
|
|
7284
|
+
async function up$3(db) {
|
|
6507
7285
|
await db.schema.createIndex("idx_operation_created_protocol_versions").on("Operation").expression(sql`md5((action->'input'->'protocolVersions')::text)`).where(sql`scope = 'document' and "index" = 0 and (action->'input'->'protocolVersions') is not null`).execute();
|
|
6508
7286
|
}
|
|
6509
|
-
async function down(db) {
|
|
7287
|
+
async function down$3(db) {
|
|
6510
7288
|
await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
|
|
6511
7289
|
}
|
|
6512
7290
|
//#endregion
|
|
7291
|
+
//#region src/storage/migrations/024_create_document_purges.ts
|
|
7292
|
+
var _024_create_document_purges_exports = /* @__PURE__ */ __exportAll({
|
|
7293
|
+
down: () => down$2,
|
|
7294
|
+
up: () => up$2
|
|
7295
|
+
});
|
|
7296
|
+
/** One row per purged document: the tombstone. Written once, never deleted. */
|
|
7297
|
+
async function up$2(db) {
|
|
7298
|
+
await db.schema.createTable("document_purges").addColumn("documentId", "text", (col) => col.primaryKey()).addColumn("ordinal", "bigint", (col) => col.notNull()).addColumn("removedRows", "jsonb", (col) => col.notNull()).addColumn("purgedAtUtc", "timestamptz", (col) => col.notNull()).addColumn("requestId", "text", (col) => col.notNull()).execute();
|
|
7299
|
+
}
|
|
7300
|
+
async function down$2(db) {
|
|
7301
|
+
await db.schema.dropTable("document_purges").execute();
|
|
7302
|
+
}
|
|
7303
|
+
//#endregion
|
|
7304
|
+
//#region src/storage/migrations/025_create_sync_received_markers.ts
|
|
7305
|
+
var _025_create_sync_received_markers_exports = /* @__PURE__ */ __exportAll({
|
|
7306
|
+
down: () => down$1,
|
|
7307
|
+
up: () => up$1
|
|
7308
|
+
});
|
|
7309
|
+
/** Received purge markers awaiting their outcome; removed with their remote. */
|
|
7310
|
+
async function up$1(db) {
|
|
7311
|
+
await db.schema.createTable("sync_received_markers").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("marker_id", "text", (col) => col.notNull()).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("operation", "jsonb", (col) => col.notNull()).addColumn("received_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_received_markers_pkey", ["remote_name", "marker_id"]).execute();
|
|
7312
|
+
}
|
|
7313
|
+
async function down$1(db) {
|
|
7314
|
+
await db.schema.dropTable("sync_received_markers").execute();
|
|
7315
|
+
}
|
|
7316
|
+
//#endregion
|
|
7317
|
+
//#region src/storage/migrations/026_create_sync_purge_refusals.ts
|
|
7318
|
+
var _026_create_sync_purge_refusals_exports = /* @__PURE__ */ __exportAll({
|
|
7319
|
+
down: () => down,
|
|
7320
|
+
up: () => up
|
|
7321
|
+
});
|
|
7322
|
+
/** A remote's refusal of a purge marker; payload-free, kept like the tombstone. */
|
|
7323
|
+
async function up(db) {
|
|
7324
|
+
await db.schema.createTable("sync_purge_refusals").addColumn("remote_name", "text", (col) => col.notNull()).addColumn("document_id", "text", (col) => col.notNull()).addColumn("branch", "text", (col) => col.notNull()).addColumn("refused_at_utc_ms", "bigint", (col) => col.notNull()).addPrimaryKeyConstraint("sync_purge_refusals_pkey", [
|
|
7325
|
+
"remote_name",
|
|
7326
|
+
"document_id",
|
|
7327
|
+
"branch"
|
|
7328
|
+
]).execute();
|
|
7329
|
+
await db.schema.createIndex("idx_sync_purge_refusals_document").on("sync_purge_refusals").column("document_id").execute();
|
|
7330
|
+
}
|
|
7331
|
+
async function down(db) {
|
|
7332
|
+
await db.schema.dropTable("sync_purge_refusals").execute();
|
|
7333
|
+
}
|
|
7334
|
+
//#endregion
|
|
6513
7335
|
//#region src/storage/migrations/migrator.ts
|
|
6514
7336
|
const REACTOR_SCHEMA = "reactor";
|
|
6515
7337
|
const migrations = {
|
|
@@ -6535,7 +7357,10 @@ const migrations = {
|
|
|
6535
7357
|
"020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
|
|
6536
7358
|
"021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
|
|
6537
7359
|
"022_create_sync_holds": _022_create_sync_holds_exports,
|
|
6538
|
-
"023_index_created_protocol_versions": _023_index_created_protocol_versions_exports
|
|
7360
|
+
"023_index_created_protocol_versions": _023_index_created_protocol_versions_exports,
|
|
7361
|
+
"024_create_document_purges": _024_create_document_purges_exports,
|
|
7362
|
+
"025_create_sync_received_markers": _025_create_sync_received_markers_exports,
|
|
7363
|
+
"026_create_sync_purge_refusals": _026_create_sync_purge_refusals_exports
|
|
6539
7364
|
};
|
|
6540
7365
|
var ProgrammaticMigrationProvider = class {
|
|
6541
7366
|
getMigrations() {
|
|
@@ -6610,6 +7435,8 @@ function toErrorInfo(err, depth = 0) {
|
|
|
6610
7435
|
};
|
|
6611
7436
|
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
6612
7437
|
if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
|
|
7438
|
+
const documentId = err.documentId;
|
|
7439
|
+
if (typeof documentId === "string") info.documentId = documentId;
|
|
6613
7440
|
return info;
|
|
6614
7441
|
}
|
|
6615
7442
|
if (typeof err === "string") return {
|
|
@@ -6646,6 +7473,11 @@ function fromErrorInfo(info) {
|
|
|
6646
7473
|
writable: true
|
|
6647
7474
|
});
|
|
6648
7475
|
if (info.stack !== void 0) err.stack = info.stack;
|
|
7476
|
+
if (info.documentId !== void 0) Object.defineProperty(err, "documentId", {
|
|
7477
|
+
value: info.documentId,
|
|
7478
|
+
configurable: true,
|
|
7479
|
+
writable: true
|
|
7480
|
+
});
|
|
6649
7481
|
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
6650
7482
|
value: fromErrorInfo(info.cause),
|
|
6651
7483
|
configurable: true,
|
|
@@ -6735,6 +7567,6 @@ function createForwardingLogger(post, level = "info", tags = []) {
|
|
|
6735
7567
|
};
|
|
6736
7568
|
}
|
|
6737
7569
|
//#endregion
|
|
6738
|
-
export {
|
|
7570
|
+
export { selectDecisionModel as $, ProbeSettler as A, matchesScope as At, JobExecutorEventTypes as B, KyselyWriteCache as C, RelationshipNotFoundError as Ct, acquirePurgeLocks as D, UpgradePreconditionFailedError as Dt, PURGE_NS as E, UnsupportedStoredProtocolError as Et, snapshotFunctionsFor as F, GATED_DOCUMENT_ACTIONS as G, DuplicateModuleError as H, DocumentMetaCache as I, createEmptyConsistencyToken as J, createConsistencyToken as K, CollectionMembershipCache as L, createKyselyWatermarkProbe as M, throwIfAborted as Mt, describeWaitingSessions as N, yieldToMain as Nt, findPurged as O, EventBusAggregateError as Ot, parseSnapshot as P, __exportAll as Pt, decideAtHead as Q, DEFAULT_DEFERRED_JOB_TTL_MS as R, EventBus as S, PurgeTooLargeError as St, PURGE_LOCK_BUCKETS as T, UnsupportedProtocolVersionError as Tt, InvalidModuleError as U, DuplicateManifestError as V, ModuleNotFoundError as W, summarizeSubmittedActions as X, submittedActionIds as Y, targetDocumentId as Z, KyselyExecutionScope as _, DocumentPurgedError as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, AppendConditionFailedError as at, appliedDelete as b, InvalidOperationTimestampError as bt, runMigrations as c, DuplicateOperationError as ct, KyselyOperationStore as d, AuthEnforcementDisabledError as dt, documentDecisionModel as et, KyselyKeyframeStore as f, AuthTimestampNotMonotonicError as ft, PassthroughSigner as g, DocumentNotFoundError as gt, verifyActionSignature as h, DocumentNotDeletedError as ht, fromErrorInfo as i, APPEND_CONDITION_FAILED_PREFIX as it, SettledWatermark as j, parsePagingOptions as jt, listPurged as k, ReactorEventTypes as kt, createForwardingPoolInstrumentation as l, OptimisticLockError as lt, SimpleJobExecutor as m, DocumentDeletedError as mt, errorToInfo as n, buildDecisionModel as nt, REACTOR_SCHEMA as o, DocumentAlreadyExistsError as ot, DocumentModelRegistry as p, AuthorizationDeniedError as pt, createDocumentFromAction as q, sanitizeArg as r, DriveCollectionId as rt, getMigrationStatus as s, DocumentExistence as st, createForwardingLogger as t, authDecisionModel as tt, instrumentPgPool as u, RevisionMismatchError as ut, DEFAULT_PURGE_DELETE_BATCH as v, ExcessiveReshuffleError as vt, KyselyOperationIndex as w, ReservedActionError as wt, resolveFeatureFlags as x, InvalidSignatureError as xt, KyselyDocumentPurger as y, GroupInUseError as yt, DEFAULT_MAX_PURGE_OPERATIONS as z };
|
|
6739
7571
|
|
|
6740
|
-
//# sourceMappingURL=forwarding-logger-
|
|
7572
|
+
//# sourceMappingURL=forwarding-logger-DMqDs9tE.js.map
|