@powerhousedao/reactor 6.2.3-dev.4 → 6.2.3-dev.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-B9IfcRij.js} +30 -8
- package/dist/build-worker-executor-B9IfcRij.js.map +1 -0
- package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-CZqIpdz2.js} +981 -104
- package/dist/document-indexer-CZqIpdz2.js.map +1 -0
- package/dist/entry.d.ts +2 -1
- package/dist/entry.js +13 -6
- package/dist/entry.js.map +1 -1
- package/dist/{errors-D3S6Eysd.js → errors-BM_ULVWd.js} +9 -1
- package/dist/{errors-D3S6Eysd.js.map → errors-BM_ULVWd.js.map} +1 -1
- package/dist/{drive-container-types-yZrksiJR.js → forwarding-logger-DMqDs9tE.js} +2631 -369
- package/dist/forwarding-logger-DMqDs9tE.js.map +1 -0
- package/dist/index.d.ts +4811 -3193
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4103 -880
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.d.ts +2 -1
- package/dist/projection-entry.js +99 -21
- package/dist/projection-entry.js.map +1 -1
- package/dist/{projection-shard-manager-CPQc5XHL.js → projection-shard-manager-BSK-GEyP.js} +24 -4
- package/dist/projection-shard-manager-BSK-GEyP.js.map +1 -0
- package/dist/{projection-worker-wI4PwcV2.js → projection-worker-D8uNFdwX.js} +1 -1
- package/dist/{projection-worker-wI4PwcV2.js.map → projection-worker-D8uNFdwX.js.map} +1 -1
- package/dist/{transport-CuogVKN_.js → transport-B3G50KVd.js} +1 -1
- package/dist/{transport-CuogVKN_.js.map → transport-B3G50KVd.js.map} +1 -1
- package/dist/{transport-ByGviWdZ.js → transport-CZDHzfvy.js} +1 -1
- package/dist/{transport-ByGviWdZ.js.map → transport-CZDHzfvy.js.map} +1 -1
- package/dist/{worker-handle-CrERzl8s.js → worker-handle-D-19Jmpz.js} +30 -17
- package/dist/worker-handle-D-19Jmpz.js.map +1 -0
- package/package.json +20 -10
- package/dist/build-worker-executor-DnU3ZU4H.js.map +0 -1
- package/dist/document-indexer-C5Gsa1B3.js.map +0 -1
- package/dist/drive-container-types-yZrksiJR.js.map +0 -1
- package/dist/error-info-Cpu4OY3o.js +0 -62
- package/dist/error-info-Cpu4OY3o.js.map +0 -1
- package/dist/forwarding-logger-BBkMSxuJ.js +0 -85
- package/dist/forwarding-logger-BBkMSxuJ.js.map +0 -1
- package/dist/projection-shard-manager-CPQc5XHL.js.map +0 -1
- package/dist/types-DMKLa0Ok.js +0 -33
- package/dist/types-DMKLa0Ok.js.map +0 -1
- package/dist/worker-HPysBfhx.js +0 -22
- package/dist/worker-HPysBfhx.js.map +0 -1
- package/dist/worker-handle-CrERzl8s.js.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DowngradeNotSupportedError as DowngradeNotSupportedError$1, appendWithoutApplying, applyAuthAction, applyDeleteDocumentAction, applyDeleteDocumentAction as applyDeleteDocumentAction$1, applyUpgradeDocumentAction, applyUpgradeDocumentAction as applyUpgradeDocumentAction$1, baseReducerVersion, createPresignedHeader, decide, defaultBaseState, deriveOperationId, evaluate, garbageCollect, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, isDenied, isUndoRedo, mentionedGroupIds, normalizeDocumentModelVersion, referencedGroupIds, sortOperations } from "@powerhousedao/shared/document-model";
|
|
1
|
+
import { AUTH_ACTION_TYPES, AUTH_DENIED_BY_GRANT_REASON, AUTH_NO_GRANT_REASON, AUTH_VERSION_UNSUPPORTED_REASON, DOCUMENT_DELETED_REASON, DOCUMENT_SCOPE_ACTION_TYPES, DowngradeNotSupportedError as DowngradeNotSupportedError$1, PEER_CAPABILITIES, 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";
|
|
3
2
|
import { v4 } from "uuid";
|
|
4
|
-
import { Migrator, sql } from "kysely";
|
|
3
|
+
import { Migrator, TableNode, sql } from "kysely";
|
|
5
4
|
//#region \0rolldown/runtime.js
|
|
6
5
|
var __defProp = Object.defineProperty;
|
|
7
6
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -52,6 +51,38 @@ function parsePagingOptions(paging, defaultLimit) {
|
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
53
|
//#endregion
|
|
54
|
+
//#region src/events/types.ts
|
|
55
|
+
/**
|
|
56
|
+
* Custom error class that aggregates multiple errors from event subscribers.
|
|
57
|
+
*/
|
|
58
|
+
var EventBusAggregateError = class extends Error {
|
|
59
|
+
errors;
|
|
60
|
+
constructor(errors) {
|
|
61
|
+
const message = `EventBus emit failed with ${errors.length} error(s): ${errors.map((e) => {
|
|
62
|
+
if (e && typeof e === "object" && "message" in e) return e.message;
|
|
63
|
+
return String(e);
|
|
64
|
+
}).join("; ")}`;
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "EventBusAggregateError";
|
|
67
|
+
this.errors = errors;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Event types for reactor lifecycle events.
|
|
72
|
+
*/
|
|
73
|
+
const ReactorEventTypes = {
|
|
74
|
+
JOB_PENDING: 10001,
|
|
75
|
+
JOB_RUNNING: 10002,
|
|
76
|
+
JOB_WRITE_READY: 10003,
|
|
77
|
+
JOB_READ_READY: 10004,
|
|
78
|
+
JOB_FAILED: 10005,
|
|
79
|
+
READMODEL_BATCH_COMPLETED: 10006,
|
|
80
|
+
READMODEL_INDEXED: 10007,
|
|
81
|
+
MODEL_LOADED: 10008,
|
|
82
|
+
SIGNATURE_REFUSED: 10009,
|
|
83
|
+
CATCHUP_SWEPT: 10010
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
55
86
|
//#region src/shared/errors.ts
|
|
56
87
|
/**
|
|
57
88
|
* Error thrown when attempting to access a deleted document.
|
|
@@ -143,6 +174,23 @@ var InvalidOperationTimestampError = class InvalidOperationTimestampError extend
|
|
|
143
174
|
return Error.isError(error) && error.name === "InvalidOperationTimestampError";
|
|
144
175
|
}
|
|
145
176
|
};
|
|
177
|
+
/** A document requires a protocol version this reactor does not run. Terminal. */
|
|
178
|
+
var UnsupportedProtocolVersionError = class UnsupportedProtocolVersionError extends Error {
|
|
179
|
+
documentId;
|
|
180
|
+
protocol;
|
|
181
|
+
version;
|
|
182
|
+
constructor(documentId, protocol, version) {
|
|
183
|
+
super(`Document ${documentId} requires ${protocol} ${version}, which this reactor does not support`);
|
|
184
|
+
this.name = "UnsupportedProtocolVersionError";
|
|
185
|
+
this.documentId = documentId;
|
|
186
|
+
this.protocol = protocol;
|
|
187
|
+
this.version = version;
|
|
188
|
+
Error.captureStackTrace(this, UnsupportedProtocolVersionError);
|
|
189
|
+
}
|
|
190
|
+
static isError(error) {
|
|
191
|
+
return Error.isError(error) && error.name === "UnsupportedProtocolVersionError";
|
|
192
|
+
}
|
|
193
|
+
};
|
|
146
194
|
/**
|
|
147
195
|
* A load would move more operations than the bound allows, indicating a real
|
|
148
196
|
* divergence between local and incoming history. Counts only first-time moves,
|
|
@@ -167,19 +215,22 @@ var ExcessiveReshuffleError = class ExcessiveReshuffleError extends Error {
|
|
|
167
215
|
return Error.isError(error) && error.name === "ExcessiveReshuffleError";
|
|
168
216
|
}
|
|
169
217
|
};
|
|
170
|
-
/**
|
|
171
|
-
* Error thrown when an operation has an invalid signature.
|
|
172
|
-
*/
|
|
218
|
+
/** The message carries the code: only name and message cross the queue. */
|
|
173
219
|
var InvalidSignatureError = class InvalidSignatureError extends Error {
|
|
174
220
|
documentId;
|
|
221
|
+
code;
|
|
175
222
|
reason;
|
|
176
|
-
constructor(documentId, reason) {
|
|
177
|
-
super(`Invalid signature in document ${documentId}: ${reason}`);
|
|
223
|
+
constructor(documentId, code, reason) {
|
|
224
|
+
super(`Invalid signature in document ${documentId} [${code}]: ${reason}`);
|
|
178
225
|
this.name = "InvalidSignatureError";
|
|
179
226
|
this.documentId = documentId;
|
|
227
|
+
this.code = code;
|
|
180
228
|
this.reason = reason;
|
|
181
229
|
Error.captureStackTrace(this, InvalidSignatureError);
|
|
182
230
|
}
|
|
231
|
+
static isError(error) {
|
|
232
|
+
return Error.isError(error) && error.name === "InvalidSignatureError";
|
|
233
|
+
}
|
|
183
234
|
};
|
|
184
235
|
/**
|
|
185
236
|
* An UPGRADE_DOCUMENT action's preconditions (fromVersion and the per-scope
|
|
@@ -220,8 +271,81 @@ var DocumentNotFoundError = class DocumentNotFoundError extends Error {
|
|
|
220
271
|
this.documentId = documentId;
|
|
221
272
|
Error.captureStackTrace(this, DocumentNotFoundError);
|
|
222
273
|
}
|
|
274
|
+
/** Also true for DocumentPurgedError: only the name crosses the queue. */
|
|
275
|
+
static isError(error) {
|
|
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
|
+
}
|
|
223
347
|
static isError(error) {
|
|
224
|
-
return Error.isError(error) && error.name === "
|
|
348
|
+
return Error.isError(error) && error.name === "ReservedActionError";
|
|
225
349
|
}
|
|
226
350
|
};
|
|
227
351
|
/**
|
|
@@ -250,6 +374,46 @@ var AuthEnforcementDisabledError = class AuthEnforcementDisabledError extends Er
|
|
|
250
374
|
return Error.isError(error) && error.name === "AuthEnforcementDisabledError";
|
|
251
375
|
}
|
|
252
376
|
};
|
|
377
|
+
/**
|
|
378
|
+
* Error thrown when a relationship edge an operation names does not exist.
|
|
379
|
+
*
|
|
380
|
+
* Detection is by `name`, not `instanceof`: the SharedWorker RPC boundary
|
|
381
|
+
* rebuilds a thrown error from `{ name, message, stack, cause }` alone
|
|
382
|
+
* (`reactor-browser/src/rpc/error-info.ts`), so the class identity is lost in
|
|
383
|
+
* transit.
|
|
384
|
+
*/
|
|
385
|
+
var RelationshipNotFoundError = class RelationshipNotFoundError extends Error {
|
|
386
|
+
sourceId;
|
|
387
|
+
targetId;
|
|
388
|
+
relationshipType;
|
|
389
|
+
constructor(sourceId, targetId, relationshipType) {
|
|
390
|
+
super(`No ${relationshipType} relationship from ${sourceId} to ${targetId}`);
|
|
391
|
+
this.name = "RelationshipNotFoundError";
|
|
392
|
+
this.sourceId = sourceId;
|
|
393
|
+
this.targetId = targetId;
|
|
394
|
+
this.relationshipType = relationshipType;
|
|
395
|
+
Error.captureStackTrace(this, RelationshipNotFoundError);
|
|
396
|
+
}
|
|
397
|
+
static isError(error) {
|
|
398
|
+
return Error.isError(error) && error.name === "RelationshipNotFoundError";
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
/** The store holds documents at protocol versions this reactor does not run. */
|
|
402
|
+
var UnsupportedStoredProtocolError = class UnsupportedStoredProtocolError extends Error {
|
|
403
|
+
versions;
|
|
404
|
+
documents;
|
|
405
|
+
constructor(versions, documents) {
|
|
406
|
+
const named = versions.map(({ protocol, version }) => `${protocol} ${version}`).join(", ");
|
|
407
|
+
super(`${documents} stored document(s) require ${named}, which this reactor does not run. Start a build that runs them, or accept them read-only with withUnsupportedStoredDocuments("read-only")`);
|
|
408
|
+
this.name = "UnsupportedStoredProtocolError";
|
|
409
|
+
this.versions = versions;
|
|
410
|
+
this.documents = documents;
|
|
411
|
+
Error.captureStackTrace(this, UnsupportedStoredProtocolError);
|
|
412
|
+
}
|
|
413
|
+
static isError(error) {
|
|
414
|
+
return Error.isError(error) && error.name === "UnsupportedStoredProtocolError";
|
|
415
|
+
}
|
|
416
|
+
};
|
|
253
417
|
//#endregion
|
|
254
418
|
//#region src/storage/interfaces.ts
|
|
255
419
|
/**
|
|
@@ -302,6 +466,7 @@ const APPEND_CONDITION_FAILED_PREFIX = "Append condition failed: ";
|
|
|
302
466
|
* conflict, not a fault: the caller retries against the new stream heads.
|
|
303
467
|
*/
|
|
304
468
|
var AppendConditionFailedError = class extends Error {
|
|
469
|
+
condition;
|
|
305
470
|
constructor(condition) {
|
|
306
471
|
const streams = condition.streams.map((s) => `${s.documentId}:${s.scope}:${s.branch}@${s.revision}`).join(", ");
|
|
307
472
|
super(`${APPEND_CONDITION_FAILED_PREFIX}a read-set stream advanced [${streams}]`);
|
|
@@ -335,6 +500,52 @@ let DocumentExistence = /* @__PURE__ */ function(DocumentExistence) {
|
|
|
335
500
|
return DocumentExistence;
|
|
336
501
|
}({});
|
|
337
502
|
//#endregion
|
|
503
|
+
//#region src/cache/operation-index-types.ts
|
|
504
|
+
const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
505
|
+
/**
|
|
506
|
+
* Identifies the collection a remote synchronizes. Collections are drive-level
|
|
507
|
+
* abstractions (document-drive and reactor-drive), so a collection id is the
|
|
508
|
+
* drive document id plus the branch it scopes to rather than an opaque string.
|
|
509
|
+
*
|
|
510
|
+
* The canonical string form (`drive.${branch}.${driveId}`) is produced only by
|
|
511
|
+
* `key` and parsed only by `fromKey`; that string is the wire and storage
|
|
512
|
+
* representation and is byte-for-byte identical to the legacy
|
|
513
|
+
* `driveCollectionId(branch, driveId)` output, so existing `document_collections`
|
|
514
|
+
* rows and persisted remotes remain valid without migration.
|
|
515
|
+
*/
|
|
516
|
+
var DriveCollectionId = class DriveCollectionId {
|
|
517
|
+
driveId;
|
|
518
|
+
branch;
|
|
519
|
+
constructor(driveId, branch) {
|
|
520
|
+
this.driveId = driveId;
|
|
521
|
+
this.branch = branch;
|
|
522
|
+
}
|
|
523
|
+
static forDrive(driveId, branch = "main") {
|
|
524
|
+
return new DriveCollectionId(driveId, branch);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The single deserializer for the wire/storage form. `branch` may contain
|
|
528
|
+
* dots, while `driveId` is a dot-free document id, so the drive id is the
|
|
529
|
+
* final dot-delimited segment.
|
|
530
|
+
*/
|
|
531
|
+
static fromKey(key) {
|
|
532
|
+
if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
|
|
533
|
+
const rest = key.slice(6);
|
|
534
|
+
const lastDot = rest.lastIndexOf(".");
|
|
535
|
+
if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
|
|
536
|
+
return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
|
|
537
|
+
}
|
|
538
|
+
get key() {
|
|
539
|
+
return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
|
|
540
|
+
}
|
|
541
|
+
toString() {
|
|
542
|
+
return this.key;
|
|
543
|
+
}
|
|
544
|
+
equals(other) {
|
|
545
|
+
return this.driveId === other.driveId && this.branch === other.branch;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
//#endregion
|
|
338
549
|
//#region src/decision/build-decision-model.ts
|
|
339
550
|
/**
|
|
340
551
|
* Reads each projection's stream through the supplied reader, recording the
|
|
@@ -352,9 +563,13 @@ async function buildDecisionModel(reader, definition, target, signal) {
|
|
|
352
563
|
model[key] = (await readStream(reader, projection.query, reads, signal)).state;
|
|
353
564
|
}
|
|
354
565
|
const staticModel = { ...model };
|
|
566
|
+
const derived = [];
|
|
355
567
|
for (const [key, projection] of projections) {
|
|
356
568
|
if (typeof projection.query !== "function") continue;
|
|
357
|
-
|
|
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) {
|
|
358
573
|
const value = {};
|
|
359
574
|
for (const query of queries) {
|
|
360
575
|
let read;
|
|
@@ -690,34 +905,13 @@ function selectDecisionModel(flags, registry) {
|
|
|
690
905
|
//#endregion
|
|
691
906
|
//#region src/executor/util.ts
|
|
692
907
|
/** Actions the reactor reduces itself, onto the document scope. */
|
|
693
|
-
const DOCUMENT_SCOPE_ACTIONS =
|
|
694
|
-
"CREATE_DOCUMENT",
|
|
695
|
-
"DELETE_DOCUMENT",
|
|
696
|
-
"UPGRADE_DOCUMENT",
|
|
697
|
-
"ADD_RELATIONSHIP",
|
|
698
|
-
"REMOVE_RELATIONSHIP",
|
|
699
|
-
"UPDATE_RELATIONSHIP"
|
|
700
|
-
]);
|
|
908
|
+
const DOCUMENT_SCOPE_ACTIONS = DOCUMENT_SCOPE_ACTION_TYPES;
|
|
701
909
|
/**
|
|
702
910
|
* `CREATE_DOCUMENT` is exempt by necessity: it runs before the document exists,
|
|
703
911
|
* so building a decision model would throw and defer the job forever.
|
|
704
912
|
*/
|
|
705
913
|
const GATED_DOCUMENT_ACTIONS = new Set([...DOCUMENT_SCOPE_ACTIONS].filter((type) => type !== "CREATE_DOCUMENT"));
|
|
706
914
|
/**
|
|
707
|
-
* The document a document-scope action writes to, which is not always the job's
|
|
708
|
-
* own document: delete and upgrade name it in `input.documentId`, and the
|
|
709
|
-
* relationship actions in `input.sourceId`. `execute` only checks that a batch
|
|
710
|
-
* shares one scope, so a caller can submit an action whose target is a document
|
|
711
|
-
* other than the one the job is keyed by. The policy gate has to follow the
|
|
712
|
-
* action rather than the job, or it decides against a policy the caller may
|
|
713
|
-
* control instead of the one guarding the write.
|
|
714
|
-
*/
|
|
715
|
-
function targetDocumentId(action, fallback) {
|
|
716
|
-
const input = action.input;
|
|
717
|
-
if (action.type === "ADD_RELATIONSHIP" || action.type === "REMOVE_RELATIONSHIP" || action.type === "UPDATE_RELATIONSHIP") return typeof input?.sourceId === "string" && input.sourceId.length > 0 ? input.sourceId : fallback;
|
|
718
|
-
return typeof input?.documentId === "string" && input.documentId.length > 0 ? input.documentId : fallback;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
915
|
* Creates a PHDocument from a CREATE_DOCUMENT action input.
|
|
722
916
|
* Reconstructs the document header and initializes the base state.
|
|
723
917
|
*
|
|
@@ -896,6 +1090,129 @@ var TouchedStreams = class {
|
|
|
896
1090
|
return this.streams.values();
|
|
897
1091
|
}
|
|
898
1092
|
};
|
|
1093
|
+
/**
|
|
1094
|
+
* The ids of the actions the caller handed to a job. Load and reevaluation
|
|
1095
|
+
* jobs write operations nobody submitted, so they report none.
|
|
1096
|
+
*/
|
|
1097
|
+
function submittedActionIds(job) {
|
|
1098
|
+
return job.kind === "mutation" ? job.actions.map((action) => action.id) : [];
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Reports what became of each submitted action.
|
|
1102
|
+
*
|
|
1103
|
+
* A job's operations can include ones it only moved to a new index, so only
|
|
1104
|
+
* those carrying a submitted action are reported. Returns undefined when the
|
|
1105
|
+
* job submitted nothing, which keeps `JobInfo.result` null for the jobs that
|
|
1106
|
+
* have no caller to answer to.
|
|
1107
|
+
*/
|
|
1108
|
+
function summarizeSubmittedActions(operations, submitted) {
|
|
1109
|
+
if (!submitted || submitted.length === 0) return;
|
|
1110
|
+
const ids = new Set(submitted);
|
|
1111
|
+
const actions = [];
|
|
1112
|
+
for (const { operation, context } of operations) {
|
|
1113
|
+
if (!ids.has(operation.action.id)) continue;
|
|
1114
|
+
actions.push({
|
|
1115
|
+
actionId: operation.action.id,
|
|
1116
|
+
scope: context.scope,
|
|
1117
|
+
index: operation.index,
|
|
1118
|
+
...operationOutcome(operation)
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (actions.length === 0) return;
|
|
1122
|
+
return {
|
|
1123
|
+
actions,
|
|
1124
|
+
allApplied: actions.every((action) => action.kind === "applied")
|
|
1125
|
+
};
|
|
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
|
+
}
|
|
899
1216
|
//#endregion
|
|
900
1217
|
//#region src/registry/errors.ts
|
|
901
1218
|
/**
|
|
@@ -978,6 +1295,8 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
978
1295
|
};
|
|
979
1296
|
//#endregion
|
|
980
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;
|
|
981
1300
|
/** How long a deferred job waits for its document before it fails. */
|
|
982
1301
|
const DEFAULT_DEFERRED_JOB_TTL_MS = 3e4;
|
|
983
1302
|
/**
|
|
@@ -993,6 +1312,7 @@ const JobExecutorEventTypes = {
|
|
|
993
1312
|
//#endregion
|
|
994
1313
|
//#region src/cache/collection-membership-cache.ts
|
|
995
1314
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
1315
|
+
operationIndex;
|
|
996
1316
|
cache = /* @__PURE__ */ new Map();
|
|
997
1317
|
constructor(operationIndex) {
|
|
998
1318
|
this.operationIndex = operationIndex;
|
|
@@ -1190,26 +1510,294 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1190
1510
|
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
|
|
1191
1511
|
if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1192
1512
|
const createOp = docScopeOps.results[0];
|
|
1513
|
+
if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
|
|
1193
1514
|
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
1194
1515
|
const createAction = createOp.action;
|
|
1195
1516
|
const documentType = createAction.input.model;
|
|
1196
1517
|
let document = createDocumentFromAction(createAction);
|
|
1197
1518
|
let documentScopeRevision = 0;
|
|
1519
|
+
let purged = false;
|
|
1198
1520
|
for (const op of docScopeOps.results) {
|
|
1199
1521
|
if (targetRevision !== void 0 && op.index > targetRevision) break;
|
|
1200
1522
|
documentScopeRevision = op.index;
|
|
1201
1523
|
if (op.action.type === "UPGRADE_DOCUMENT") {
|
|
1202
1524
|
const upgradeAction = op.action;
|
|
1203
1525
|
document = applyUpgradeDocumentAction$1(document, upgradeAction);
|
|
1204
|
-
} 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
|
+
}
|
|
1205
1530
|
}
|
|
1206
1531
|
return {
|
|
1207
1532
|
state: document.state.document,
|
|
1208
1533
|
documentType,
|
|
1534
|
+
protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
|
|
1209
1535
|
documentScopeRevision: documentScopeRevision + 1
|
|
1210
1536
|
};
|
|
1211
1537
|
}
|
|
1212
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
|
+
}
|
|
1551
|
+
//#endregion
|
|
1552
|
+
//#region src/catch-up/settled-watermark.ts
|
|
1553
|
+
const MAX_PENDING_PROBES = 64;
|
|
1554
|
+
/** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
|
|
1555
|
+
function snapshotFunctionsFor(serverVersionNum) {
|
|
1556
|
+
if (serverVersionNum >= 13e4) return {
|
|
1557
|
+
snapshot: "pg_current_snapshot",
|
|
1558
|
+
currentXid: "pg_current_xact_id",
|
|
1559
|
+
xidIfAssigned: "pg_current_xact_id_if_assigned"
|
|
1560
|
+
};
|
|
1561
|
+
if (serverVersionNum >= 1e5) return {
|
|
1562
|
+
snapshot: "txid_current_snapshot",
|
|
1563
|
+
currentXid: "txid_current",
|
|
1564
|
+
xidIfAssigned: "txid_current_if_assigned"
|
|
1565
|
+
};
|
|
1566
|
+
throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
|
|
1567
|
+
}
|
|
1568
|
+
/** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
|
|
1569
|
+
function parseSnapshot(text) {
|
|
1570
|
+
const parts = text.split(":");
|
|
1571
|
+
if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
|
|
1572
|
+
const [xmin, xmax, xip] = parts;
|
|
1573
|
+
return {
|
|
1574
|
+
xmin: BigInt(xmin),
|
|
1575
|
+
xmax: BigInt(xmax),
|
|
1576
|
+
xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
async function readSnapshotFunctions(db) {
|
|
1580
|
+
const result = await sql`select current_setting('server_version_num') as version`.execute(db);
|
|
1581
|
+
return snapshotFunctionsFor(Number(result.rows[0].version));
|
|
1582
|
+
}
|
|
1583
|
+
/** A probe settles once an xmin passes the xid it took after its head. */
|
|
1584
|
+
var ProbeSettler = class {
|
|
1585
|
+
settled = 0;
|
|
1586
|
+
lastHead = 0;
|
|
1587
|
+
covered = 0;
|
|
1588
|
+
lastXip = [];
|
|
1589
|
+
pending = [];
|
|
1590
|
+
get settledThrough() {
|
|
1591
|
+
return this.settled;
|
|
1592
|
+
}
|
|
1593
|
+
get head() {
|
|
1594
|
+
return this.lastHead;
|
|
1595
|
+
}
|
|
1596
|
+
/** The highest head a settled or pending probe covers. */
|
|
1597
|
+
get probedHead() {
|
|
1598
|
+
return this.covered;
|
|
1599
|
+
}
|
|
1600
|
+
observe(head, xid, snapshot, nowUtcMs) {
|
|
1601
|
+
this.lastHead = Math.max(this.lastHead, head);
|
|
1602
|
+
this.lastXip = snapshot.xip;
|
|
1603
|
+
if (xid !== null && head > this.covered) {
|
|
1604
|
+
this.covered = head;
|
|
1605
|
+
this.pending.push({
|
|
1606
|
+
head,
|
|
1607
|
+
xid,
|
|
1608
|
+
takenAtUtcMs: nowUtcMs
|
|
1609
|
+
});
|
|
1610
|
+
if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
|
|
1611
|
+
}
|
|
1612
|
+
const stillPending = [];
|
|
1613
|
+
for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
|
|
1614
|
+
else stillPending.push(probe);
|
|
1615
|
+
this.pending = stillPending.filter((probe) => probe.head > this.settled);
|
|
1616
|
+
return this.settled;
|
|
1617
|
+
}
|
|
1618
|
+
/** Xids an unsettled probe still waits on. */
|
|
1619
|
+
waitingOn() {
|
|
1620
|
+
if (this.pending.length === 0) return [];
|
|
1621
|
+
const xid = this.pending[0].xid;
|
|
1622
|
+
return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
|
|
1623
|
+
}
|
|
1624
|
+
stalledSinceUtcMs() {
|
|
1625
|
+
return this.pending[0]?.takenAtUtcMs;
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
function operationIndexTableName(db) {
|
|
1629
|
+
const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
|
|
1630
|
+
const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
|
|
1631
|
+
const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
1632
|
+
return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
|
|
1633
|
+
}
|
|
1634
|
+
/** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
|
|
1635
|
+
function createKyselyWatermarkProbe(db) {
|
|
1636
|
+
let resolved = null;
|
|
1637
|
+
const resolve = async () => {
|
|
1638
|
+
const fns = await readSnapshotFunctions(db);
|
|
1639
|
+
const table = operationIndexTableName(db);
|
|
1640
|
+
const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
|
|
1641
|
+
if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
|
|
1642
|
+
return {
|
|
1643
|
+
fns,
|
|
1644
|
+
sequence
|
|
1645
|
+
};
|
|
1646
|
+
};
|
|
1647
|
+
return async (probedHead) => {
|
|
1648
|
+
resolved ??= resolve().catch((error) => {
|
|
1649
|
+
resolved = null;
|
|
1650
|
+
throw error;
|
|
1651
|
+
});
|
|
1652
|
+
const { fns, sequence } = await resolved;
|
|
1653
|
+
const headRow = (await sql`select coalesce(pg_sequence_last_value(${sequence}::regclass), 0) as head, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
|
|
1654
|
+
const head = Number(headRow.head);
|
|
1655
|
+
let xid = null;
|
|
1656
|
+
if (headRow.outside_write && head > probedHead) {
|
|
1657
|
+
const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
|
|
1658
|
+
xid = BigInt(xidResult.rows[0].xid);
|
|
1659
|
+
}
|
|
1660
|
+
const row = (await sql`select ${sql.raw(fns.snapshot)}()::text as snapshot, ${sql.raw(fns.xidIfAssigned)}() is null as outside_write`.execute(db)).rows[0];
|
|
1661
|
+
return {
|
|
1662
|
+
head,
|
|
1663
|
+
xid,
|
|
1664
|
+
snapshot: parseSnapshot(row.snapshot),
|
|
1665
|
+
outsideWrite: headRow.outside_write && row.outside_write
|
|
1666
|
+
};
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
/** Knows when an ordinal gap is final by asking which transactions are open. */
|
|
1670
|
+
var SettledWatermark = class {
|
|
1671
|
+
probe;
|
|
1672
|
+
logger;
|
|
1673
|
+
now;
|
|
1674
|
+
settler = new ProbeSettler();
|
|
1675
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1676
|
+
current;
|
|
1677
|
+
next;
|
|
1678
|
+
warnedInsideWrite = false;
|
|
1679
|
+
constructor(probe, logger, now = Date.now) {
|
|
1680
|
+
this.probe = probe;
|
|
1681
|
+
this.logger = logger;
|
|
1682
|
+
this.now = now;
|
|
1683
|
+
}
|
|
1684
|
+
get settledThrough() {
|
|
1685
|
+
return this.settler.settledThrough;
|
|
1686
|
+
}
|
|
1687
|
+
refresh(signal) {
|
|
1688
|
+
signal?.throwIfAborted();
|
|
1689
|
+
if (this.next !== void 0) return this.next;
|
|
1690
|
+
if (this.current === void 0) return this.start();
|
|
1691
|
+
const next = this.current.catch(() => this.settledThrough).then(() => {
|
|
1692
|
+
this.next = void 0;
|
|
1693
|
+
return this.start();
|
|
1694
|
+
});
|
|
1695
|
+
this.next = next;
|
|
1696
|
+
return next;
|
|
1697
|
+
}
|
|
1698
|
+
onAdvance(listener) {
|
|
1699
|
+
this.listeners.add(listener);
|
|
1700
|
+
return () => {
|
|
1701
|
+
this.listeners.delete(listener);
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
status() {
|
|
1705
|
+
const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
|
|
1706
|
+
return {
|
|
1707
|
+
head: this.settler.head,
|
|
1708
|
+
settledThrough: this.settledThrough,
|
|
1709
|
+
waitingOn: this.settler.waitingOn(),
|
|
1710
|
+
...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
start() {
|
|
1714
|
+
const run = this.probeOnce().finally(() => {
|
|
1715
|
+
if (this.current === run) this.current = void 0;
|
|
1716
|
+
});
|
|
1717
|
+
this.current = run;
|
|
1718
|
+
return run;
|
|
1719
|
+
}
|
|
1720
|
+
async probeOnce() {
|
|
1721
|
+
const reading = await this.probe(this.settler.probedHead);
|
|
1722
|
+
if (!reading.outsideWrite) {
|
|
1723
|
+
if (!this.warnedInsideWrite) {
|
|
1724
|
+
this.warnedInsideWrite = true;
|
|
1725
|
+
this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
|
|
1726
|
+
}
|
|
1727
|
+
return this.settledThrough;
|
|
1728
|
+
}
|
|
1729
|
+
const before = this.settledThrough;
|
|
1730
|
+
const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
|
|
1731
|
+
if (after > before) for (const listener of this.listeners) try {
|
|
1732
|
+
listener(after);
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
this.logger.error("settled watermark listener failed: @error", error);
|
|
1735
|
+
}
|
|
1736
|
+
return after;
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
/** Sessions holding these xids open; empty without pg_stat_activity. */
|
|
1740
|
+
async function describeWaitingSessions(db, xids) {
|
|
1741
|
+
if (xids.length === 0) return [];
|
|
1742
|
+
const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
|
|
1743
|
+
try {
|
|
1744
|
+
return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
|
|
1745
|
+
from pg_stat_activity
|
|
1746
|
+
where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
|
|
1747
|
+
pid: Number(row.pid),
|
|
1748
|
+
applicationName: row.application_name ?? "",
|
|
1749
|
+
state: row.state ?? "",
|
|
1750
|
+
xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
|
|
1751
|
+
xid: row.xid
|
|
1752
|
+
}));
|
|
1753
|
+
} catch {
|
|
1754
|
+
return [];
|
|
1755
|
+
}
|
|
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
|
+
}
|
|
1787
|
+
/** Bind slots per statement: 16-bit on the wire, read signed in-process. */
|
|
1788
|
+
const MAX_BIND_PARAMETERS = 32767;
|
|
1789
|
+
/** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
|
|
1790
|
+
function chunkRows(rows) {
|
|
1791
|
+
if (rows.length === 0) return [];
|
|
1792
|
+
const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
|
|
1793
|
+
const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
|
|
1794
|
+
if (rows.length <= perChunk) return [rows];
|
|
1795
|
+
const chunks = [];
|
|
1796
|
+
for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
|
|
1797
|
+
return chunks;
|
|
1798
|
+
}
|
|
1799
|
+
/** Per database: the function that assigns the transaction's xid. */
|
|
1800
|
+
const xidFunctions = /* @__PURE__ */ new WeakMap();
|
|
1213
1801
|
var KyselyOperationIndexTxn = class {
|
|
1214
1802
|
collections = [];
|
|
1215
1803
|
collectionMemberships = [];
|
|
@@ -1275,16 +1863,20 @@ var KyselyOperationIndexTxn = class {
|
|
|
1275
1863
|
}
|
|
1276
1864
|
};
|
|
1277
1865
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1866
|
+
db;
|
|
1278
1867
|
trx;
|
|
1868
|
+
liveIds;
|
|
1279
1869
|
constructor(db) {
|
|
1280
1870
|
this.db = db;
|
|
1281
1871
|
}
|
|
1282
1872
|
get queryExecutor() {
|
|
1283
1873
|
return this.trx ?? this.db;
|
|
1284
1874
|
}
|
|
1285
|
-
|
|
1875
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
1876
|
+
withTransaction(trx, liveIds) {
|
|
1286
1877
|
const instance = new KyselyOperationIndex(this.db);
|
|
1287
1878
|
instance.trx = trx;
|
|
1879
|
+
instance.liveIds = liveIds;
|
|
1288
1880
|
return instance;
|
|
1289
1881
|
}
|
|
1290
1882
|
start() {
|
|
@@ -1331,10 +1923,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1331
1923
|
leftOrdinal: null
|
|
1332
1924
|
}));
|
|
1333
1925
|
for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
|
|
1334
|
-
await trx.insertInto("document_collections").values(
|
|
1926
|
+
for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
|
|
1335
1927
|
}
|
|
1336
1928
|
let operationOrdinals = [];
|
|
1337
1929
|
if (operations.length > 0) {
|
|
1930
|
+
await this.refusePurgedOperations(trx, operations);
|
|
1931
|
+
await this.assignXid(trx);
|
|
1338
1932
|
const operationRows = operations.map((op) => ({
|
|
1339
1933
|
opId: op.id || "",
|
|
1340
1934
|
documentId: op.documentId,
|
|
@@ -1349,7 +1943,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1349
1943
|
deniedReason: op.deniedReason ?? null,
|
|
1350
1944
|
sourceRemote: op.sourceRemote
|
|
1351
1945
|
}));
|
|
1352
|
-
|
|
1946
|
+
for (const chunk of chunkRows(operationRows)) {
|
|
1947
|
+
const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
|
|
1948
|
+
operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
|
|
1949
|
+
}
|
|
1353
1950
|
}
|
|
1354
1951
|
if (memberships.length > 0) for (const m of memberships) {
|
|
1355
1952
|
const ordinal = operationOrdinals[m.operationIndex];
|
|
@@ -1382,10 +1979,44 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1382
1979
|
}
|
|
1383
1980
|
return operationOrdinals;
|
|
1384
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
|
+
}
|
|
1989
|
+
async getOrdinalsInRange(after, through, limit, signal) {
|
|
1990
|
+
signal?.throwIfAborted();
|
|
1991
|
+
if (through <= after || limit <= 0) return [];
|
|
1992
|
+
return (await this.queryExecutor.selectFrom("operation_index_operations").select("ordinal").where("ordinal", ">", after).where("ordinal", "<=", through).orderBy("ordinal", "asc").limit(limit).execute()).map((row) => Number(row.ordinal));
|
|
1993
|
+
}
|
|
1994
|
+
async getByOrdinals(ordinals, signal) {
|
|
1995
|
+
signal?.throwIfAborted();
|
|
1996
|
+
if (ordinals.length === 0) return [];
|
|
1997
|
+
const results = [];
|
|
1998
|
+
for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
|
|
1999
|
+
const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
|
|
2000
|
+
const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
|
|
2001
|
+
for (const row of rows) results.push(this.rowToOperationWithContext(row));
|
|
2002
|
+
}
|
|
2003
|
+
results.sort((a, b) => a.context.ordinal - b.context.ordinal);
|
|
2004
|
+
return results;
|
|
2005
|
+
}
|
|
2006
|
+
async getStreamAfter(stream, after, signal, limit) {
|
|
2007
|
+
signal?.throwIfAborted();
|
|
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));
|
|
2009
|
+
}
|
|
1385
2010
|
async getGroupReferencers(groupId, signal) {
|
|
1386
2011
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1387
2012
|
return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1388
2013
|
}
|
|
2014
|
+
async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
|
|
2015
|
+
signal?.throwIfAborted();
|
|
2016
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Map();
|
|
2017
|
+
const rows = await this.queryExecutor.selectFrom("operation_index_operations").select("opId").select((eb) => eb.fn.max("ordinal").as("ordinal")).where("documentId", "=", documentId).where("branch", "=", branch).where("scope", "=", scope).where("opId", "in", opIds).groupBy("opId").execute();
|
|
2018
|
+
return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
|
|
2019
|
+
}
|
|
1389
2020
|
async find(collectionId, cursor, view, paging, signal) {
|
|
1390
2021
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1391
2022
|
const outerCursor = cursor ?? -1;
|
|
@@ -1399,6 +2030,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1399
2030
|
if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
|
|
1400
2031
|
if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
|
|
1401
2032
|
if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
|
|
2033
|
+
if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
|
|
1402
2034
|
return qb;
|
|
1403
2035
|
};
|
|
1404
2036
|
const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
|
|
@@ -1487,6 +2119,23 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1487
2119
|
}, signal) : void 0
|
|
1488
2120
|
};
|
|
1489
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
|
+
}
|
|
2128
|
+
/** Takes the xid before the first ordinal, as the settled watermark needs. */
|
|
2129
|
+
async assignXid(trx) {
|
|
2130
|
+
let fn = xidFunctions.get(this.db);
|
|
2131
|
+
if (fn === void 0) {
|
|
2132
|
+
fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
|
|
2133
|
+
xidFunctions.set(this.db, fn);
|
|
2134
|
+
fn.catch(() => xidFunctions.delete(this.db));
|
|
2135
|
+
}
|
|
2136
|
+
const name = await fn;
|
|
2137
|
+
await sql`select ${sql.raw(name)}()`.execute(trx);
|
|
2138
|
+
}
|
|
1490
2139
|
rowToOperationWithContext(row) {
|
|
1491
2140
|
return {
|
|
1492
2141
|
operation: {
|
|
@@ -1605,21 +2254,6 @@ var RingBuffer = class {
|
|
|
1605
2254
|
}
|
|
1606
2255
|
};
|
|
1607
2256
|
//#endregion
|
|
1608
|
-
//#region src/cache/write-cache-types.ts
|
|
1609
|
-
/**
|
|
1610
|
-
* Where a snapshot sits in its stream.
|
|
1611
|
-
*
|
|
1612
|
-
* - `Head`: the newest revision of the stream when it was stored. Only these
|
|
1613
|
-
* can answer a read that asks for the head.
|
|
1614
|
-
* - `Historical`: state at an earlier revision. Usable as a starting point to
|
|
1615
|
-
* replay forward from, and as an answer to a read for that same revision.
|
|
1616
|
-
*/
|
|
1617
|
-
let SnapshotPosition = /* @__PURE__ */ function(SnapshotPosition) {
|
|
1618
|
-
SnapshotPosition["Head"] = "head";
|
|
1619
|
-
SnapshotPosition["Historical"] = "historical";
|
|
1620
|
-
return SnapshotPosition;
|
|
1621
|
-
}({});
|
|
1622
|
-
//#endregion
|
|
1623
2257
|
//#region src/cache/kysely-write-cache.ts
|
|
1624
2258
|
/**
|
|
1625
2259
|
* The last operation index a keyframe's document reflects for the scope. A
|
|
@@ -1654,42 +2288,6 @@ function copyDocument(document) {
|
|
|
1654
2288
|
operations: { ...document.operations }
|
|
1655
2289
|
};
|
|
1656
2290
|
}
|
|
1657
|
-
/**
|
|
1658
|
-
* In-memory write cache with keyframe persistence for PHDocuments.
|
|
1659
|
-
*
|
|
1660
|
-
* Caches document snapshots in ring buffers with LRU eviction. On cache miss,
|
|
1661
|
-
* rebuilds documents from nearest keyframe or full operation history.
|
|
1662
|
-
*
|
|
1663
|
-
* **Performance Characteristics:**
|
|
1664
|
-
* - Cache hit: O(1) lookup in ring buffer
|
|
1665
|
-
* - Cold miss: O(n) where n is total operation count, or O(k) where k is operations since keyframe
|
|
1666
|
-
* - Warm miss: O(m) where m is operations since cached revision
|
|
1667
|
-
* - Eviction: O(1) for LRU tracking and removal
|
|
1668
|
-
*
|
|
1669
|
-
* **Thread Safety:**
|
|
1670
|
-
* Not thread-safe. Designed for single-threaded job executor environment.
|
|
1671
|
-
* External synchronization required for concurrent access across multiple executors.
|
|
1672
|
-
*
|
|
1673
|
-
* **Example:**
|
|
1674
|
-
* ```typescript
|
|
1675
|
-
* const cache = new KyselyWriteCache(
|
|
1676
|
-
* keyframeStore,
|
|
1677
|
-
* operationStore,
|
|
1678
|
-
* registry,
|
|
1679
|
-
* { maxDocuments: 1000, ringBufferSize: 10, keyframeInterval: 10 }
|
|
1680
|
-
* );
|
|
1681
|
-
*
|
|
1682
|
-
* await cache.startup();
|
|
1683
|
-
*
|
|
1684
|
-
* // Retrieve or rebuild document
|
|
1685
|
-
* const doc = await cache.getState(docId, docType, scope, branch, revision);
|
|
1686
|
-
*
|
|
1687
|
-
* // Cache result after job execution
|
|
1688
|
-
* cache.putState(docId, docType, scope, branch, newRevision, updatedDoc);
|
|
1689
|
-
*
|
|
1690
|
-
* await cache.shutdown();
|
|
1691
|
-
* ```
|
|
1692
|
-
*/
|
|
1693
2291
|
var KyselyWriteCache = class KyselyWriteCache {
|
|
1694
2292
|
streams;
|
|
1695
2293
|
lruTracker;
|
|
@@ -1697,6 +2295,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1697
2295
|
operationStore;
|
|
1698
2296
|
registry;
|
|
1699
2297
|
config;
|
|
2298
|
+
reads = /* @__PURE__ */ new Map();
|
|
1700
2299
|
constructor(keyframeStore, operationStore, registry, config) {
|
|
1701
2300
|
this.keyframeStore = keyframeStore;
|
|
1702
2301
|
this.operationStore = operationStore;
|
|
@@ -1713,6 +2312,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1713
2312
|
const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
|
|
1714
2313
|
scoped.streams = this.streams;
|
|
1715
2314
|
scoped.lruTracker = this.lruTracker;
|
|
2315
|
+
scoped.reads = this.reads;
|
|
1716
2316
|
return scoped;
|
|
1717
2317
|
}
|
|
1718
2318
|
/**
|
|
@@ -1746,11 +2346,27 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1746
2346
|
* @returns The document at the target revision
|
|
1747
2347
|
* @throws {Error} "Operation aborted" if signal is aborted
|
|
1748
2348
|
* @throws {ModuleNotFoundError} If document type not registered in registry
|
|
2349
|
+
* @throws {DocumentPurgedError} If the stream's only row is a purge marker
|
|
1749
2350
|
* @throws {Error} "Failed to rebuild document" if operation store fails
|
|
1750
2351
|
* @throws {Error} If reducer throws during operation application
|
|
1751
2352
|
* @throws {Error} If document serialization fails
|
|
1752
2353
|
*/
|
|
1753
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) {
|
|
1754
2370
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1755
2371
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1756
2372
|
const stream = this.streams.get(streamKey);
|
|
@@ -1758,13 +2374,13 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1758
2374
|
const snapshots = stream.ringBuffer.getAll();
|
|
1759
2375
|
if (targetRevision === void 0) {
|
|
1760
2376
|
const newest = highestRevision(snapshots);
|
|
1761
|
-
if (newest?.position ===
|
|
2377
|
+
if (newest?.position === "head") {
|
|
1762
2378
|
this.lruTracker.touch(streamKey);
|
|
1763
2379
|
return copyDocument(newest.document);
|
|
1764
2380
|
}
|
|
1765
2381
|
if (newest) {
|
|
1766
2382
|
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1767
|
-
this.
|
|
2383
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
1768
2384
|
this.lruTracker.touch(streamKey);
|
|
1769
2385
|
return document;
|
|
1770
2386
|
}
|
|
@@ -1777,7 +2393,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1777
2393
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1778
2394
|
if (newestOlder) {
|
|
1779
2395
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1780
|
-
this.
|
|
2396
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
|
|
1781
2397
|
this.lruTracker.touch(streamKey);
|
|
1782
2398
|
return document;
|
|
1783
2399
|
}
|
|
@@ -1785,7 +2401,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1785
2401
|
}
|
|
1786
2402
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1787
2403
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1788
|
-
this.
|
|
2404
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
1789
2405
|
return document;
|
|
1790
2406
|
}
|
|
1791
2407
|
/**
|
|
@@ -1820,7 +2436,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1820
2436
|
if (run.length === 0) return;
|
|
1821
2437
|
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1822
2438
|
const head = run[run.length - 1];
|
|
1823
|
-
this.store(documentId, scope, branch, head.revision, head.document,
|
|
2439
|
+
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
2440
|
+
}
|
|
2441
|
+
storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
|
|
2442
|
+
if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
|
|
1824
2443
|
}
|
|
1825
2444
|
store(documentId, scope, branch, revision, document, position) {
|
|
1826
2445
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
@@ -1862,6 +2481,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1862
2481
|
* @returns The number of streams evicted
|
|
1863
2482
|
*/
|
|
1864
2483
|
invalidate(documentId, scope, branch) {
|
|
2484
|
+
for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
|
|
1865
2485
|
let evicted = 0;
|
|
1866
2486
|
if (scope === void 0 && branch === void 0) {
|
|
1867
2487
|
for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
|
|
@@ -1890,6 +2510,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1890
2510
|
* Resets LRU tracking state. This operation always succeeds.
|
|
1891
2511
|
*/
|
|
1892
2512
|
clear() {
|
|
2513
|
+
for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
|
|
1893
2514
|
this.streams.clear();
|
|
1894
2515
|
this.lruTracker.clear();
|
|
1895
2516
|
}
|
|
@@ -1924,9 +2545,9 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1924
2545
|
* the last replayed operation are applied at the end. Creation-time 0->N seed
|
|
1925
2546
|
* upgrades carry the initial state, so they still apply immediately.
|
|
1926
2547
|
*/
|
|
1927
|
-
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
2548
|
+
async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
|
|
1928
2549
|
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1929
|
-
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
2550
|
+
const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
|
|
1930
2551
|
const documentScopeBound = scope === "document" ? targetRevision : void 0;
|
|
1931
2552
|
let document;
|
|
1932
2553
|
let startRevision;
|
|
@@ -1969,7 +2590,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1969
2590
|
subsequentDeletes: []
|
|
1970
2591
|
});
|
|
1971
2592
|
}
|
|
1972
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2593
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
1973
2594
|
applyDeleteDocumentAction(document, operation.action);
|
|
1974
2595
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
1975
2596
|
}
|
|
@@ -1982,6 +2603,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1982
2603
|
}, signal);
|
|
1983
2604
|
if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1984
2605
|
const createOp = createOpResult.results[0];
|
|
2606
|
+
if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
|
|
1985
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}`);
|
|
1986
2608
|
const documentCreateAction = createOp.action;
|
|
1987
2609
|
documentType = documentCreateAction.input.model;
|
|
@@ -2021,15 +2643,15 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2021
2643
|
});
|
|
2022
2644
|
} else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
|
|
2023
2645
|
docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
|
|
2024
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2646
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
2025
2647
|
applyDeleteDocumentAction(document, operation.action);
|
|
2026
2648
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2027
2649
|
} else {
|
|
2028
2650
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2029
|
-
document = docModule.reducer(document, operation.action, void 0, {
|
|
2651
|
+
document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
|
|
2030
2652
|
skip: operation.skip,
|
|
2031
2653
|
protocolVersion
|
|
2032
|
-
});
|
|
2654
|
+
}), document.header.protocolVersions);
|
|
2033
2655
|
}
|
|
2034
2656
|
}
|
|
2035
2657
|
}
|
|
@@ -2063,6 +2685,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2063
2685
|
let cursor = void 0;
|
|
2064
2686
|
const pageSize = 100;
|
|
2065
2687
|
let hasMorePages;
|
|
2688
|
+
let needsFullReplay = false;
|
|
2066
2689
|
do {
|
|
2067
2690
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2068
2691
|
const paging = {
|
|
@@ -2073,26 +2696,31 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2073
2696
|
const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
|
|
2074
2697
|
for (const operation of result.results) {
|
|
2075
2698
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2699
|
+
if (keyframe && operation.skip > 0) {
|
|
2700
|
+
needsFullReplay = true;
|
|
2701
|
+
break;
|
|
2702
|
+
}
|
|
2076
2703
|
const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
|
|
2077
2704
|
document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
|
|
2078
2705
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2079
2706
|
else {
|
|
2080
2707
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2081
|
-
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2708
|
+
document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2082
2709
|
skip: operation.skip,
|
|
2083
2710
|
protocolVersion,
|
|
2084
2711
|
replayOptions: { operation },
|
|
2085
2712
|
skipIndexValidation: true
|
|
2086
|
-
});
|
|
2713
|
+
}), document.header.protocolVersions);
|
|
2087
2714
|
}
|
|
2088
2715
|
}
|
|
2089
2716
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
2090
|
-
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
2717
|
+
hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
|
|
2091
2718
|
if (hasMorePages) cursor = result.nextCursor;
|
|
2092
2719
|
} catch (err) {
|
|
2093
2720
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2094
2721
|
}
|
|
2095
2722
|
} while (hasMorePages);
|
|
2723
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2096
2724
|
document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
|
|
2097
2725
|
document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
2098
2726
|
if (pendingUpgrades.length > 0) {
|
|
@@ -2199,26 +2827,32 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2199
2827
|
if ((await this.operationStore.getSince(documentId, "document", branch, docScopeNextIndex - 1, void 0, void 0, signal)).results.length > 0) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
2200
2828
|
const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
|
|
2201
2829
|
let document = copyDocument(baseDocument);
|
|
2830
|
+
let needsFullReplay = false;
|
|
2202
2831
|
try {
|
|
2203
2832
|
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
2204
2833
|
for (const operation of pagedResults.results) {
|
|
2205
2834
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2206
2835
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2836
|
+
if (operation.skip > 0) {
|
|
2837
|
+
needsFullReplay = true;
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2207
2840
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2208
2841
|
else {
|
|
2209
2842
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2210
|
-
document = module.reducer(document, operation.action, void 0, {
|
|
2843
|
+
document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
|
|
2211
2844
|
skip: operation.skip,
|
|
2212
2845
|
protocolVersion,
|
|
2213
2846
|
replayOptions: { operation },
|
|
2214
2847
|
skipIndexValidation: true
|
|
2215
|
-
});
|
|
2848
|
+
}), document.header.protocolVersions);
|
|
2216
2849
|
}
|
|
2217
2850
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2218
2851
|
}
|
|
2219
2852
|
} catch (err) {
|
|
2220
2853
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2221
2854
|
}
|
|
2855
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2222
2856
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
2223
2857
|
document.header.revision = revisions.revision;
|
|
2224
2858
|
if (targetRevision !== void 0) document.header.revision = {
|
|
@@ -2337,13 +2971,165 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2337
2971
|
}
|
|
2338
2972
|
}
|
|
2339
2973
|
//#endregion
|
|
2340
|
-
//#region src/
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
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
|
|
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
|
+
};
|
|
3122
|
+
var DefaultExecutionScope = class {
|
|
3123
|
+
operationStore;
|
|
3124
|
+
operationIndex;
|
|
3125
|
+
writeCache;
|
|
3126
|
+
documentMetaCache;
|
|
3127
|
+
collectionMembershipCache;
|
|
3128
|
+
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
3129
|
+
this.operationStore = operationStore;
|
|
3130
|
+
this.operationIndex = operationIndex;
|
|
3131
|
+
this.writeCache = writeCache;
|
|
3132
|
+
this.documentMetaCache = documentMetaCache;
|
|
2347
3133
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
2348
3134
|
}
|
|
2349
3135
|
async run(fn, signal) {
|
|
@@ -2353,11 +3139,19 @@ var DefaultExecutionScope = class {
|
|
|
2353
3139
|
operationIndex: this.operationIndex,
|
|
2354
3140
|
writeCache: this.writeCache,
|
|
2355
3141
|
documentMetaCache: this.documentMetaCache,
|
|
2356
|
-
collectionMembershipCache: this.collectionMembershipCache
|
|
3142
|
+
collectionMembershipCache: this.collectionMembershipCache,
|
|
3143
|
+
documentLocks: NOOP_DOCUMENT_LOCKS
|
|
2357
3144
|
});
|
|
2358
3145
|
}
|
|
2359
3146
|
};
|
|
2360
3147
|
var KyselyExecutionScope = class {
|
|
3148
|
+
db;
|
|
3149
|
+
operationStore;
|
|
3150
|
+
operationIndex;
|
|
3151
|
+
keyframeStore;
|
|
3152
|
+
writeCache;
|
|
3153
|
+
documentMetaCache;
|
|
3154
|
+
collectionMembershipCache;
|
|
2361
3155
|
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2362
3156
|
this.db = db;
|
|
2363
3157
|
this.operationStore = operationStore;
|
|
@@ -2370,28 +3164,46 @@ var KyselyExecutionScope = class {
|
|
|
2370
3164
|
async run(fn, signal) {
|
|
2371
3165
|
signal?.throwIfAborted();
|
|
2372
3166
|
return this.db.transaction().execute(async (trx) => {
|
|
2373
|
-
const
|
|
2374
|
-
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);
|
|
2375
3171
|
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
2376
3172
|
return fn({
|
|
2377
3173
|
operationStore: scopedOperationStore,
|
|
2378
3174
|
operationIndex: scopedOperationIndex,
|
|
2379
3175
|
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
2380
3176
|
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
2381
|
-
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)
|
|
2382
3193
|
});
|
|
2383
3194
|
});
|
|
2384
3195
|
}
|
|
2385
3196
|
};
|
|
2386
3197
|
//#endregion
|
|
2387
3198
|
//#region src/utils/reshuffle.ts
|
|
2388
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
3199
|
+
const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2389
3200
|
"CREATE_DOCUMENT",
|
|
2390
3201
|
"DELETE_DOCUMENT",
|
|
2391
3202
|
"UPGRADE_DOCUMENT",
|
|
2392
3203
|
"ADD_RELATIONSHIP",
|
|
2393
3204
|
"REMOVE_RELATIONSHIP",
|
|
2394
3205
|
"UPDATE_RELATIONSHIP",
|
|
3206
|
+
"PURGE_DOCUMENT",
|
|
2395
3207
|
"ADD_FOLDER",
|
|
2396
3208
|
"UPDATE_FOLDER",
|
|
2397
3209
|
"REMOVE_FOLDER"
|
|
@@ -2597,6 +3409,15 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2597
3409
|
const derivedSet = derivedReadSet(definition);
|
|
2598
3410
|
if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
|
|
2599
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
|
+
}
|
|
2600
3421
|
const readStreams = await Promise.all(readSet.map(async (stream) => ({
|
|
2601
3422
|
stream,
|
|
2602
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))
|
|
@@ -2609,7 +3430,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2609
3430
|
const histories = [];
|
|
2610
3431
|
for (const read of readStreams) {
|
|
2611
3432
|
const streamOperations = read.stream === writtenProjection ? [...read.operations, ...operations] : read.operations;
|
|
2612
|
-
const before = await
|
|
3433
|
+
const before = await stateBefore(read.stream.query);
|
|
2613
3434
|
walked.push({
|
|
2614
3435
|
streamKey: streamKey(read.stream.query),
|
|
2615
3436
|
scope: read.stream.query.scope,
|
|
@@ -2631,7 +3452,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2631
3452
|
branch: target.branch
|
|
2632
3453
|
};
|
|
2633
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));
|
|
2634
|
-
const before = await
|
|
3455
|
+
const before = await stateBefore(query);
|
|
2635
3456
|
evaluatedStateKey = streamKey(query);
|
|
2636
3457
|
walked.push({
|
|
2637
3458
|
streamKey: evaluatedStateKey,
|
|
@@ -2702,58 +3523,36 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2702
3523
|
return operations.map((operation) => reasons.get(operation.id));
|
|
2703
3524
|
}
|
|
2704
3525
|
//#endregion
|
|
2705
|
-
//#region src/cache/operation-index-types.ts
|
|
2706
|
-
const DRIVE_COLLECTION_PREFIX = "drive.";
|
|
2707
|
-
/**
|
|
2708
|
-
* Identifies the collection a remote synchronizes. Collections are drive-level
|
|
2709
|
-
* abstractions (document-drive and reactor-drive), so a collection id is the
|
|
2710
|
-
* drive document id plus the branch it scopes to rather than an opaque string.
|
|
2711
|
-
*
|
|
2712
|
-
* The canonical string form (`drive.${branch}.${driveId}`) is produced only by
|
|
2713
|
-
* `key` and parsed only by `fromKey`; that string is the wire and storage
|
|
2714
|
-
* representation and is byte-for-byte identical to the legacy
|
|
2715
|
-
* `driveCollectionId(branch, driveId)` output, so existing `document_collections`
|
|
2716
|
-
* rows and persisted remotes remain valid without migration.
|
|
2717
|
-
*/
|
|
2718
|
-
var DriveCollectionId = class DriveCollectionId {
|
|
2719
|
-
constructor(driveId, branch) {
|
|
2720
|
-
this.driveId = driveId;
|
|
2721
|
-
this.branch = branch;
|
|
2722
|
-
}
|
|
2723
|
-
static forDrive(driveId, branch = "main") {
|
|
2724
|
-
return new DriveCollectionId(driveId, branch);
|
|
2725
|
-
}
|
|
2726
|
-
/**
|
|
2727
|
-
* The single deserializer for the wire/storage form. `branch` may contain
|
|
2728
|
-
* dots, while `driveId` is a dot-free document id, so the drive id is the
|
|
2729
|
-
* final dot-delimited segment.
|
|
2730
|
-
*/
|
|
2731
|
-
static fromKey(key) {
|
|
2732
|
-
if (!key.startsWith(DRIVE_COLLECTION_PREFIX)) throw new Error(`Unsupported collection id: ${key}`);
|
|
2733
|
-
const rest = key.slice(6);
|
|
2734
|
-
const lastDot = rest.lastIndexOf(".");
|
|
2735
|
-
if (lastDot === -1 || lastDot === rest.length - 1) throw new Error(`Malformed drive collection id: ${key}`);
|
|
2736
|
-
return new DriveCollectionId(rest.slice(lastDot + 1), rest.slice(0, lastDot));
|
|
2737
|
-
}
|
|
2738
|
-
get key() {
|
|
2739
|
-
return `${DRIVE_COLLECTION_PREFIX}${this.branch}.${this.driveId}`;
|
|
2740
|
-
}
|
|
2741
|
-
toString() {
|
|
2742
|
-
return this.key;
|
|
2743
|
-
}
|
|
2744
|
-
equals(other) {
|
|
2745
|
-
return this.driveId === other.driveId && this.branch === other.branch;
|
|
2746
|
-
}
|
|
2747
|
-
};
|
|
2748
|
-
//#endregion
|
|
2749
3526
|
//#region src/executor/document-action-handler.ts
|
|
2750
3527
|
var DocumentActionHandler = class {
|
|
2751
|
-
|
|
3528
|
+
registry;
|
|
3529
|
+
logger;
|
|
3530
|
+
driveContainerTypes;
|
|
3531
|
+
featureFlags;
|
|
3532
|
+
decisionModel;
|
|
3533
|
+
protocolSupport;
|
|
3534
|
+
loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
|
|
3535
|
+
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
|
|
2752
3536
|
this.registry = registry;
|
|
2753
3537
|
this.logger = logger;
|
|
2754
3538
|
this.driveContainerTypes = driveContainerTypes;
|
|
2755
3539
|
this.featureFlags = featureFlags;
|
|
2756
3540
|
this.decisionModel = decisionModel;
|
|
3541
|
+
this.protocolSupport = protocolSupport;
|
|
3542
|
+
}
|
|
3543
|
+
/** Keys this reactor does not register are admitted: it cannot judge them. */
|
|
3544
|
+
unsupportedProtocol(documentId, protocolVersions) {
|
|
3545
|
+
for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
|
|
3546
|
+
const supported = this.protocolSupport[protocol];
|
|
3547
|
+
if (supported === void 0) {
|
|
3548
|
+
if (!this.loggedUnregisteredProtocols.has(protocol)) {
|
|
3549
|
+
this.loggedUnregisteredProtocols.add(protocol);
|
|
3550
|
+
this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
|
|
3551
|
+
}
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
|
|
3555
|
+
}
|
|
2757
3556
|
}
|
|
2758
3557
|
/** Whether the write arrives with its evaluation already decided. */
|
|
2759
3558
|
alreadyEvaluated(executing) {
|
|
@@ -2771,6 +3570,7 @@ var DocumentActionHandler = class {
|
|
|
2771
3570
|
case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
|
|
2772
3571
|
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
|
|
2773
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);
|
|
2774
3574
|
default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
|
|
2775
3575
|
}
|
|
2776
3576
|
}
|
|
@@ -2840,7 +3640,7 @@ var DocumentActionHandler = class {
|
|
|
2840
3640
|
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2841
3641
|
};
|
|
2842
3642
|
executing.touchedStreams.add(job.documentId, job.scope, job.branch);
|
|
2843
|
-
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing,
|
|
3643
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
|
|
2844
3644
|
indexTxn.write([{
|
|
2845
3645
|
...operation,
|
|
2846
3646
|
documentId: job.documentId,
|
|
@@ -2852,6 +3652,7 @@ var DocumentActionHandler = class {
|
|
|
2852
3652
|
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2853
3653
|
state: standing.state.document,
|
|
2854
3654
|
documentType: standing.header.documentType,
|
|
3655
|
+
protocolVersions: standing.header.protocolVersions,
|
|
2855
3656
|
documentScopeRevision: operation.index + 1
|
|
2856
3657
|
});
|
|
2857
3658
|
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
@@ -2868,6 +3669,9 @@ var DocumentActionHandler = class {
|
|
|
2868
3669
|
error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
|
|
2869
3670
|
duration: Date.now() - startTime
|
|
2870
3671
|
};
|
|
3672
|
+
const input = action.input;
|
|
3673
|
+
const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
|
|
3674
|
+
if (unsupported) return buildErrorResult(job, unsupported, startTime);
|
|
2871
3675
|
const document = createDocumentFromAction(action);
|
|
2872
3676
|
let operation = createOperation(action, 0, skip, {
|
|
2873
3677
|
documentId: document.header.id,
|
|
@@ -2875,8 +3679,8 @@ var DocumentActionHandler = class {
|
|
|
2875
3679
|
branch: job.branch
|
|
2876
3680
|
});
|
|
2877
3681
|
const resultingStateObj = {
|
|
2878
|
-
|
|
2879
|
-
|
|
3682
|
+
...document.state,
|
|
3683
|
+
header: document.header
|
|
2880
3684
|
};
|
|
2881
3685
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
2882
3686
|
const writeResult = await this.writeOperationToStore({
|
|
@@ -2893,7 +3697,7 @@ var DocumentActionHandler = class {
|
|
|
2893
3697
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2894
3698
|
};
|
|
2895
3699
|
executing.touchedStreams.add(document.header.id, job.scope, job.branch);
|
|
2896
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document,
|
|
3700
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
|
|
2897
3701
|
indexTxn.write([{
|
|
2898
3702
|
...operation,
|
|
2899
3703
|
documentId: document.header.id,
|
|
@@ -2910,6 +3714,7 @@ var DocumentActionHandler = class {
|
|
|
2910
3714
|
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
2911
3715
|
state: document.state.document,
|
|
2912
3716
|
documentType: document.header.documentType,
|
|
3717
|
+
protocolVersions: document.header.protocolVersions,
|
|
2913
3718
|
documentScopeRevision: 1
|
|
2914
3719
|
});
|
|
2915
3720
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
@@ -2953,7 +3758,7 @@ var DocumentActionHandler = class {
|
|
|
2953
3758
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2954
3759
|
};
|
|
2955
3760
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
2956
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3761
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
2957
3762
|
indexTxn.write([{
|
|
2958
3763
|
...operation,
|
|
2959
3764
|
documentId,
|
|
@@ -2965,6 +3770,7 @@ var DocumentActionHandler = class {
|
|
|
2965
3770
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
2966
3771
|
state: document.state.document,
|
|
2967
3772
|
documentType: document.header.documentType,
|
|
3773
|
+
protocolVersions: document.header.protocolVersions,
|
|
2968
3774
|
documentScopeRevision: operation.index + 1
|
|
2969
3775
|
});
|
|
2970
3776
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3003,7 +3809,7 @@ var DocumentActionHandler = class {
|
|
|
3003
3809
|
} catch (error) {
|
|
3004
3810
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3005
3811
|
}
|
|
3006
|
-
const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3812
|
+
const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3007
3813
|
for (const revisionScope of revisionScopes) {
|
|
3008
3814
|
const snapshot = input.revision[revisionScope] ?? 0;
|
|
3009
3815
|
const actual = actualRevisions[revisionScope] ?? 0;
|
|
@@ -3045,8 +3851,8 @@ var DocumentActionHandler = class {
|
|
|
3045
3851
|
branch: job.branch
|
|
3046
3852
|
});
|
|
3047
3853
|
const resultingStateObj = {
|
|
3048
|
-
|
|
3049
|
-
|
|
3854
|
+
...document.state,
|
|
3855
|
+
header: document.header
|
|
3050
3856
|
};
|
|
3051
3857
|
if (fromVersion > 0) resultingStateObj.__migrated = true;
|
|
3052
3858
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
@@ -3064,7 +3870,7 @@ var DocumentActionHandler = class {
|
|
|
3064
3870
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3065
3871
|
};
|
|
3066
3872
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3067
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3873
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3068
3874
|
for (const scope of otherScopes) executing.postCommitInvalidations.push({
|
|
3069
3875
|
documentId,
|
|
3070
3876
|
scope,
|
|
@@ -3081,22 +3887,41 @@ var DocumentActionHandler = class {
|
|
|
3081
3887
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3082
3888
|
state: document.state.document,
|
|
3083
3889
|
documentType: document.header.documentType,
|
|
3890
|
+
protocolVersions: document.header.protocolVersions,
|
|
3084
3891
|
documentScopeRevision: operation.index + 1
|
|
3085
3892
|
});
|
|
3086
3893
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
3087
3894
|
}
|
|
3088
|
-
|
|
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
|
+
}
|
|
3089
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 }) => {
|
|
3090
|
-
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3909
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3091
3910
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3092
3911
|
txn.addToCollection(collectionId, input.targetId);
|
|
3093
3912
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
3094
3913
|
}
|
|
3095
3914
|
});
|
|
3096
3915
|
}
|
|
3097
|
-
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
|
+
}
|
|
3098
3923
|
return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
3099
|
-
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3924
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3100
3925
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3101
3926
|
txn.removeFromCollection(collectionId, input.targetId);
|
|
3102
3927
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
@@ -3120,6 +3945,7 @@ var DocumentActionHandler = class {
|
|
|
3120
3945
|
try {
|
|
3121
3946
|
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
3122
3947
|
} catch (error) {
|
|
3948
|
+
if (DocumentPurgedError.isError(error)) return buildErrorResult(job, error, startTime);
|
|
3123
3949
|
if (DocumentNotFoundError.isError(error)) return buildErrorResult(job, new DocumentNotFoundError(input.sourceId, `${actionTypeName}: source document ${input.sourceId} not found`), startTime);
|
|
3124
3950
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3125
3951
|
}
|
|
@@ -3150,7 +3976,7 @@ var DocumentActionHandler = class {
|
|
|
3150
3976
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
3151
3977
|
executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
|
|
3152
3978
|
executing.touchedStreams.add(input.targetId, job.scope, job.branch);
|
|
3153
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc,
|
|
3979
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
|
|
3154
3980
|
indexTxn.write([{
|
|
3155
3981
|
...operation,
|
|
3156
3982
|
documentId: input.sourceId,
|
|
@@ -3169,6 +3995,7 @@ var DocumentActionHandler = class {
|
|
|
3169
3995
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3170
3996
|
state: sourceDoc.state.document,
|
|
3171
3997
|
documentType: sourceDoc.header.documentType,
|
|
3998
|
+
protocolVersions: sourceDoc.header.protocolVersions,
|
|
3172
3999
|
documentScopeRevision: operation.index + 1
|
|
3173
4000
|
});
|
|
3174
4001
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
@@ -3197,55 +4024,669 @@ var DocumentActionHandler = class {
|
|
|
3197
4024
|
}
|
|
3198
4025
|
};
|
|
3199
4026
|
//#endregion
|
|
3200
|
-
//#region src/
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
4027
|
+
//#region src/signer/passthrough-signer.ts
|
|
4028
|
+
/**
|
|
4029
|
+
* A no-op signer that returns empty values for all methods.
|
|
4030
|
+
* Used when signing is not required.
|
|
4031
|
+
*/
|
|
4032
|
+
var PassthroughSigner = class {
|
|
4033
|
+
publicKey = {};
|
|
4034
|
+
sign() {
|
|
4035
|
+
return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
|
|
3204
4036
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
4037
|
+
verify() {
|
|
4038
|
+
return Promise.resolve();
|
|
4039
|
+
}
|
|
4040
|
+
signAction() {
|
|
4041
|
+
return Promise.resolve([
|
|
4042
|
+
"",
|
|
4043
|
+
"",
|
|
4044
|
+
"",
|
|
4045
|
+
"",
|
|
4046
|
+
""
|
|
4047
|
+
]);
|
|
4048
|
+
}
|
|
4049
|
+
};
|
|
4050
|
+
//#endregion
|
|
4051
|
+
//#region src/signer/trust-policy.ts
|
|
4052
|
+
/**
|
|
4053
|
+
* The policy admission asks: the reactor's own key signing as its own user is
|
|
4054
|
+
* accepted, then the host's policy decides, else the default does. `own` is
|
|
4055
|
+
* read on every ask, since its user changes on login.
|
|
4056
|
+
*/
|
|
4057
|
+
function admissionTrustPolicy(own, authEnforcement, policy) {
|
|
4058
|
+
return { authorizeSigner(signer, key, documentId) {
|
|
4059
|
+
const identity = actionSignerIdentity(own);
|
|
4060
|
+
if (identity.app.key !== "" && key === identity.app.key) {
|
|
4061
|
+
if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
|
|
3228
4062
|
}
|
|
4063
|
+
if (policy) return policy.authorizeSigner(signer, key, documentId);
|
|
4064
|
+
return Promise.resolve(!authEnforcement);
|
|
4065
|
+
} };
|
|
4066
|
+
}
|
|
4067
|
+
function sameUser(a, b) {
|
|
4068
|
+
return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
|
|
4069
|
+
}
|
|
4070
|
+
//#endregion
|
|
4071
|
+
//#region src/signer/did-key.ts
|
|
4072
|
+
const DID_KEY_PREFIX = "did:key:z";
|
|
4073
|
+
const P256_MULTICODEC = [128, 36];
|
|
4074
|
+
const MAX_CACHED_KEYS = 1024;
|
|
4075
|
+
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
|
|
4076
|
+
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
|
|
4077
|
+
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
|
|
4078
|
+
/** A did:key that is not a compressed P-256 public key. */
|
|
4079
|
+
var MalformedDidKeyError = class extends Error {
|
|
4080
|
+
constructor(did, detail) {
|
|
4081
|
+
super(`Malformed did:key ${did}: ${detail}`);
|
|
4082
|
+
this.name = "MalformedDidKeyError";
|
|
3229
4083
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
4084
|
+
};
|
|
4085
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
4086
|
+
/** Cached per process: decompressing the point costs a BigInt modPow. */
|
|
4087
|
+
async function importDidKey(did) {
|
|
4088
|
+
const cached = keyCache.get(did);
|
|
4089
|
+
if (cached) {
|
|
4090
|
+
keyCache.delete(did);
|
|
4091
|
+
keyCache.set(did, cached);
|
|
4092
|
+
return cached;
|
|
4093
|
+
}
|
|
4094
|
+
const raw = decompressP256(compressedPointFromDid(did), did);
|
|
4095
|
+
const key = await crypto.subtle.importKey("raw", raw.buffer, {
|
|
4096
|
+
name: "ECDSA",
|
|
4097
|
+
namedCurve: "P-256"
|
|
4098
|
+
}, false, ["verify"]);
|
|
4099
|
+
keyCache.set(did, key);
|
|
4100
|
+
if (keyCache.size > MAX_CACHED_KEYS) {
|
|
4101
|
+
const oldest = keyCache.keys().next().value;
|
|
4102
|
+
if (oldest !== void 0) keyCache.delete(oldest);
|
|
4103
|
+
}
|
|
4104
|
+
return key;
|
|
4105
|
+
}
|
|
4106
|
+
function compressedPointFromDid(did) {
|
|
4107
|
+
if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
|
|
4108
|
+
const decoded = base58Decode(did.slice(9));
|
|
4109
|
+
if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
|
|
4110
|
+
if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
|
|
4111
|
+
return decoded.subarray(2);
|
|
4112
|
+
}
|
|
4113
|
+
function decompressP256(compressed, did) {
|
|
4114
|
+
const prefix = compressed[0];
|
|
4115
|
+
if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
|
|
4116
|
+
let x = BigInt(0);
|
|
4117
|
+
for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
|
|
4118
|
+
if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
|
|
4119
|
+
const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
|
|
4120
|
+
let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
|
|
4121
|
+
if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
|
|
4122
|
+
if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
|
|
4123
|
+
const uncompressed = /* @__PURE__ */ new Uint8Array(65);
|
|
4124
|
+
uncompressed[0] = 4;
|
|
4125
|
+
uncompressed.set(bigIntToBytes(x), 1);
|
|
4126
|
+
uncompressed.set(bigIntToBytes(y), 33);
|
|
4127
|
+
return uncompressed;
|
|
4128
|
+
}
|
|
4129
|
+
function modPow(base, exp, mod) {
|
|
4130
|
+
let result = BigInt(1);
|
|
4131
|
+
let b = base % mod;
|
|
4132
|
+
let e = exp;
|
|
4133
|
+
while (e > BigInt(0)) {
|
|
4134
|
+
if (e & BigInt(1)) result = result * b % mod;
|
|
4135
|
+
e >>= BigInt(1);
|
|
4136
|
+
b = b * b % mod;
|
|
4137
|
+
}
|
|
4138
|
+
return result;
|
|
4139
|
+
}
|
|
4140
|
+
function bigIntToBytes(value) {
|
|
4141
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
4142
|
+
let v = value;
|
|
4143
|
+
for (let i = 31; i >= 0; i--) {
|
|
4144
|
+
bytes[i] = Number(v & BigInt(255));
|
|
4145
|
+
v >>= BigInt(8);
|
|
4146
|
+
}
|
|
4147
|
+
return bytes;
|
|
4148
|
+
}
|
|
4149
|
+
//#endregion
|
|
4150
|
+
//#region src/signer/verify-action-signature.ts
|
|
4151
|
+
const RENOWN_HASH_LENGTH = 44;
|
|
4152
|
+
const SHARED_HASH_LENGTH = 28;
|
|
4153
|
+
const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
|
|
4154
|
+
/**
|
|
4155
|
+
* Integrity only: identity binding and the live-id check sit with the caller.
|
|
4156
|
+
* `operation` is the incoming operation at load admission.
|
|
4157
|
+
*/
|
|
4158
|
+
async function verifyActionSignature(action, target, path, operation) {
|
|
4159
|
+
const policy = target.policy ?? "legacy";
|
|
4160
|
+
const refusal = policyRefusal(action, policy);
|
|
4161
|
+
if (refusal) return refusal;
|
|
4162
|
+
const shape = signerShape(action.context?.signer);
|
|
4163
|
+
if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
|
|
4164
|
+
ok: true,
|
|
4165
|
+
scheme: "unsigned"
|
|
4166
|
+
};
|
|
4167
|
+
if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
|
|
4168
|
+
const { signer, tuple } = shape;
|
|
4169
|
+
const scheme = schemeOf(tuple[2]);
|
|
4170
|
+
if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
|
|
4171
|
+
if (scheme !== "v2" && policy === "v2-required") return refuse(scheme, "SCHEME_BELOW_POLICY", `action ${action.id} carries a legacy tuple but ${target.documentId} requires v2 signatures`);
|
|
4172
|
+
if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
|
|
4173
|
+
if (scheme === "v2") {
|
|
4174
|
+
const refusal = await checkV2(action, tuple, target, operation);
|
|
4175
|
+
if (refusal) return refusal;
|
|
4176
|
+
} else if (path === "mutation") {
|
|
4177
|
+
if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
4178
|
+
}
|
|
4179
|
+
return verifyEcdsa(tuple, scheme, action.id);
|
|
4180
|
+
}
|
|
4181
|
+
/** Refusals a document's policy makes before any tuple is read. */
|
|
4182
|
+
function policyRefusal(action, policy) {
|
|
4183
|
+
if (action.type !== "CREATE_DOCUMENT") return;
|
|
4184
|
+
const scheme = schemeOfAction(action);
|
|
4185
|
+
const input = action.input;
|
|
4186
|
+
const documentId = input?.documentId ?? "";
|
|
4187
|
+
if (policy === "legacy") return isDerivedDocumentId(documentId) ? refuse(scheme, "ID_MISMATCH", `action ${action.id}: a legacy document cannot take the content-addressed id ${documentId}`) : void 0;
|
|
4188
|
+
const expected = derivedCreateId(input);
|
|
4189
|
+
if (expected === documentId) return;
|
|
4190
|
+
return refuse(scheme, "ID_MISMATCH", expected === void 0 ? `action ${action.id} creates v2-required ${documentId} without the header params its id is derived from` : `action ${action.id} creates v2-required ${documentId}, but its header params derive ${expected}`);
|
|
4191
|
+
}
|
|
4192
|
+
function derivedCreateId(input) {
|
|
4193
|
+
if (!input?.signing || !input.protocolVersions) return;
|
|
4194
|
+
try {
|
|
4195
|
+
return deriveDocumentId({
|
|
4196
|
+
documentType: input.model,
|
|
4197
|
+
createdAtUtcIso: input.signing.createdAtUtcIso,
|
|
4198
|
+
nonce: input.signing.nonce,
|
|
4199
|
+
protocolVersions: input.protocolVersions
|
|
4200
|
+
});
|
|
4201
|
+
} catch {
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
function schemeOfAction(action) {
|
|
4206
|
+
const shape = signerShape(action.context?.signer);
|
|
4207
|
+
if (shape.kind === "unsigned") return "unsigned";
|
|
4208
|
+
return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
|
|
4209
|
+
}
|
|
4210
|
+
/** Peers send arbitrary JSON, so nothing past `context` is assumed. */
|
|
4211
|
+
function signerShape(value) {
|
|
4212
|
+
if (value === void 0 || value === null) return { kind: "unsigned" };
|
|
4213
|
+
if (!isRecord(value)) return {
|
|
4214
|
+
kind: "malformed",
|
|
4215
|
+
reason: "has a signer that is not an object"
|
|
4216
|
+
};
|
|
4217
|
+
const { app, user, signatures } = value;
|
|
4218
|
+
if (app !== void 0 && app !== null && !isRecord(app)) return {
|
|
4219
|
+
kind: "malformed",
|
|
4220
|
+
reason: "has a signer.app that is not an object"
|
|
4221
|
+
};
|
|
4222
|
+
const key = isRecord(app) ? app.key : void 0;
|
|
4223
|
+
if (key !== void 0 && key !== null && typeof key !== "string") return {
|
|
4224
|
+
kind: "malformed",
|
|
4225
|
+
reason: "has a signer.app.key that is not a string"
|
|
4226
|
+
};
|
|
4227
|
+
if (!key) return { kind: "unsigned" };
|
|
4228
|
+
if (!isRecord(user) || typeof user.address !== "string") return {
|
|
4229
|
+
kind: "malformed",
|
|
4230
|
+
reason: "has a signer without a user address"
|
|
4231
|
+
};
|
|
4232
|
+
if (!Array.isArray(signatures)) return {
|
|
4233
|
+
kind: "malformed",
|
|
4234
|
+
reason: "has a signer whose signatures are not a list"
|
|
4235
|
+
};
|
|
4236
|
+
const tuple = signatures.at(-1);
|
|
4237
|
+
if (!isTuple(tuple)) return {
|
|
4238
|
+
kind: "malformed",
|
|
4239
|
+
reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
|
|
4240
|
+
};
|
|
4241
|
+
return {
|
|
4242
|
+
kind: "signed",
|
|
4243
|
+
signer: value,
|
|
4244
|
+
tuple
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
function isRecord(value) {
|
|
4248
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4249
|
+
}
|
|
4250
|
+
async function checkV2(action, tuple, target, operation) {
|
|
4251
|
+
const problem = v2TupleProblem(tuple);
|
|
4252
|
+
if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
|
|
4253
|
+
if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
|
|
4254
|
+
let expected;
|
|
4255
|
+
try {
|
|
4256
|
+
expected = await hashActionV2(action, target, action.context.signer);
|
|
4257
|
+
} catch (error) {
|
|
4258
|
+
return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
|
|
4259
|
+
}
|
|
4260
|
+
if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
4261
|
+
}
|
|
4262
|
+
function sameInstant(a, b) {
|
|
4263
|
+
const left = Date.parse(a);
|
|
4264
|
+
return !Number.isNaN(left) && left === Date.parse(b);
|
|
4265
|
+
}
|
|
4266
|
+
function schemeOf(hash) {
|
|
4267
|
+
if (isV2ActionHash(hash)) return "v2";
|
|
4268
|
+
if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
|
|
4269
|
+
if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
|
|
4270
|
+
return "legacy-unknown";
|
|
4271
|
+
}
|
|
4272
|
+
async function legacyHash(scheme, action, target) {
|
|
4273
|
+
if (scheme === "legacy-renown") {
|
|
4274
|
+
const payload = [
|
|
4275
|
+
action.scope,
|
|
4276
|
+
action.type,
|
|
4277
|
+
JSON.stringify(action.input)
|
|
4278
|
+
].join("");
|
|
4279
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
4280
|
+
return bytesToBase64(new Uint8Array(digest));
|
|
4281
|
+
}
|
|
4282
|
+
const signer = action.context.signer;
|
|
4283
|
+
return buildOperationSignatureParams({
|
|
4284
|
+
documentId: target.documentId,
|
|
4285
|
+
signer,
|
|
4286
|
+
action,
|
|
4287
|
+
previousStateHash: ""
|
|
4288
|
+
})[2];
|
|
4289
|
+
}
|
|
4290
|
+
async function verifyEcdsa(tuple, scheme, actionId) {
|
|
4291
|
+
const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
|
|
4292
|
+
if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
|
|
4293
|
+
let cryptoKey;
|
|
4294
|
+
try {
|
|
4295
|
+
cryptoKey = await importDidKey(key);
|
|
4296
|
+
} catch (error) {
|
|
4297
|
+
return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4298
|
+
}
|
|
4299
|
+
const message = buildOperationSignatureMessage([
|
|
4300
|
+
timestamp,
|
|
4301
|
+
key,
|
|
4302
|
+
hash,
|
|
4303
|
+
prevStateHash
|
|
4304
|
+
]);
|
|
4305
|
+
let valid;
|
|
4306
|
+
try {
|
|
4307
|
+
valid = await crypto.subtle.verify({
|
|
4308
|
+
name: "ECDSA",
|
|
4309
|
+
hash: "SHA-256"
|
|
4310
|
+
}, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
|
|
4311
|
+
} catch {
|
|
4312
|
+
valid = false;
|
|
4313
|
+
}
|
|
4314
|
+
if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
|
|
4315
|
+
return {
|
|
4316
|
+
ok: true,
|
|
4317
|
+
scheme
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
function isTuple(value) {
|
|
4321
|
+
return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
|
|
4322
|
+
}
|
|
4323
|
+
function refuse(scheme, code, reason) {
|
|
4324
|
+
return {
|
|
4325
|
+
ok: false,
|
|
4326
|
+
scheme,
|
|
4327
|
+
code,
|
|
4328
|
+
reason
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
function hexToBytes(hex) {
|
|
4332
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
4333
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
4334
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
4335
|
+
return bytes;
|
|
4336
|
+
}
|
|
4337
|
+
function bytesToBase64(bytes) {
|
|
4338
|
+
let binary = "";
|
|
4339
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
4340
|
+
return btoa(binary);
|
|
4341
|
+
}
|
|
4342
|
+
//#endregion
|
|
4343
|
+
//#region src/executor/synthesized-signing.ts
|
|
4344
|
+
/** Submitted types whose stored action the reducer writes in their place. */
|
|
4345
|
+
const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
|
|
4346
|
+
/**
|
|
4347
|
+
* The id of the action synthesized from `submittedId`. Derived, so the stored
|
|
4348
|
+
* operation answers for the submitted id at the live-id check.
|
|
4349
|
+
*/
|
|
4350
|
+
function synthesizedActionId(submittedId) {
|
|
4351
|
+
return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
|
|
4352
|
+
}
|
|
4353
|
+
/**
|
|
4354
|
+
* Whether `operation` stores an action the reducer made from `submitted`
|
|
4355
|
+
* rather than `submitted` itself or an operation already in `history`.
|
|
4356
|
+
*/
|
|
4357
|
+
function isSynthesized(submitted, operation, history) {
|
|
4358
|
+
if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
|
|
4359
|
+
const id = operation.action.id;
|
|
4360
|
+
if (!id) return true;
|
|
4361
|
+
if (id === submitted.id) return false;
|
|
4362
|
+
return !history.some((existing) => existing.action.id === id);
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Signs the synthesized action of `operation` in place as `signer`, under the
|
|
4366
|
+
* id {@link synthesizedActionId} derives from the submitted one. A REDO's
|
|
4367
|
+
* rebuilt action has no timestamp, so it takes the submitted one; the
|
|
4368
|
+
* operation timestamp is set to the action's, which a peer requires of a v2
|
|
4369
|
+
* tuple.
|
|
4370
|
+
*/
|
|
4371
|
+
async function signSynthesized(operation, submitted, target, signer, signal) {
|
|
4372
|
+
const synthesized = operation.action;
|
|
4373
|
+
const id = synthesizedActionId(submitted.id);
|
|
4374
|
+
const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
|
|
4375
|
+
const action = {
|
|
4376
|
+
id,
|
|
4377
|
+
timestampUtcMs,
|
|
4378
|
+
type: synthesized.type,
|
|
4379
|
+
input: synthesized.input,
|
|
4380
|
+
scope: synthesized.scope
|
|
4381
|
+
};
|
|
4382
|
+
const signature = await signer.signAction(action, {
|
|
4383
|
+
documentId: target.documentId,
|
|
4384
|
+
branch: target.branch
|
|
4385
|
+
}, signal);
|
|
4386
|
+
operation.action = {
|
|
4387
|
+
...action,
|
|
4388
|
+
context: { signer: {
|
|
4389
|
+
...actionSignerIdentity(signer),
|
|
4390
|
+
signatures: [signature]
|
|
4391
|
+
} }
|
|
4392
|
+
};
|
|
4393
|
+
operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
|
|
4394
|
+
operation.timestampUtcMs = timestampUtcMs;
|
|
4395
|
+
}
|
|
4396
|
+
//#endregion
|
|
4397
|
+
//#region src/executor/signature-admission.ts
|
|
4398
|
+
const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
|
|
4399
|
+
/** Runs once per write, when this reactor first stores it; never on re-appends. */
|
|
4400
|
+
var SignatureAdmission = class {
|
|
4401
|
+
mode;
|
|
4402
|
+
logger;
|
|
4403
|
+
eventBus;
|
|
4404
|
+
policySource;
|
|
4405
|
+
trustPolicy;
|
|
4406
|
+
trustTimeoutMs;
|
|
4407
|
+
constructor(mode, logger, eventBus, policySource = "meta", trust = {
|
|
4408
|
+
authEnforcement: false,
|
|
4409
|
+
timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
|
|
4410
|
+
}) {
|
|
4411
|
+
this.mode = mode;
|
|
4412
|
+
this.logger = logger;
|
|
4413
|
+
this.eventBus = eventBus;
|
|
4414
|
+
this.policySource = policySource;
|
|
4415
|
+
this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
|
|
4416
|
+
this.trustTimeoutMs = trust.timeoutMs;
|
|
4417
|
+
}
|
|
4418
|
+
/** The first refusal of a mutation's submitted actions, when enforcing. */
|
|
4419
|
+
async admitMutation(job, stores, signal) {
|
|
4420
|
+
const { operationStore } = stores;
|
|
4421
|
+
const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
|
|
4422
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4423
|
+
const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
|
|
4424
|
+
await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4425
|
+
const submitted = /* @__PURE__ */ new Set();
|
|
4426
|
+
for (const entry of candidates) {
|
|
4427
|
+
if (committed.has(entry)) continue;
|
|
4428
|
+
const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
|
|
4429
|
+
submitted.add(entry.opId);
|
|
4430
|
+
if (verdict.ok) continue;
|
|
4431
|
+
const refusal = this.record(job, entry, verdict, "mutation");
|
|
4432
|
+
if (refusal) return {
|
|
4433
|
+
kind: "refused",
|
|
4434
|
+
error: refusal
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
return {
|
|
4438
|
+
kind: "admitted",
|
|
4439
|
+
committed: [...committed.values()]
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
/** The incoming operations a load drops, empty unless enforcing. */
|
|
4443
|
+
async admitLoad(job, operations, stores, signal) {
|
|
4444
|
+
const { operationStore } = stores;
|
|
4445
|
+
const stream = {
|
|
4446
|
+
documentId: job.documentId,
|
|
4447
|
+
scope: job.scope,
|
|
4448
|
+
branch: job.branch
|
|
4449
|
+
};
|
|
4450
|
+
const candidates = operations.map((operation) => ({
|
|
4451
|
+
...candidate(operation.action, stream),
|
|
4452
|
+
operation
|
|
4453
|
+
}));
|
|
4454
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4455
|
+
const redelivered = await this.redelivered(candidates, live, operationStore, signal);
|
|
4456
|
+
await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4457
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
4458
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
4459
|
+
const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
|
|
4460
|
+
if (verdict.ok) continue;
|
|
4461
|
+
if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
|
|
4462
|
+
}
|
|
4463
|
+
return dropped;
|
|
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
|
+
}
|
|
4480
|
+
async verdict(entry, live, submitted, path, signal, known = false) {
|
|
4481
|
+
const verdict = await verifyActionSignature(entry.action, {
|
|
4482
|
+
documentId: entry.stream.documentId,
|
|
4483
|
+
branch: entry.stream.branch,
|
|
4484
|
+
policy: entry.policy
|
|
4485
|
+
}, path, entry.operation);
|
|
4486
|
+
if (!verdict.ok) return verdict;
|
|
4487
|
+
if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
|
|
4488
|
+
ok: false,
|
|
4489
|
+
scheme: verdict.scheme,
|
|
4490
|
+
code: "DUPLICATE_ACTION",
|
|
4491
|
+
reason: `action ${entry.action.id} is already in the stream`
|
|
4492
|
+
};
|
|
4493
|
+
if (verdict.scheme === "unsigned") {
|
|
4494
|
+
const address = entry.action.context?.signer?.user?.address;
|
|
4495
|
+
return address ? {
|
|
4496
|
+
ok: false,
|
|
4497
|
+
scheme: "unsigned",
|
|
4498
|
+
code: "UNSIGNED_IDENTITY",
|
|
4499
|
+
reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
|
|
4500
|
+
} : verdict;
|
|
4501
|
+
}
|
|
4502
|
+
if (known) return verdict;
|
|
4503
|
+
const signer = entry.action.context.signer;
|
|
4504
|
+
if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
|
|
4505
|
+
ok: false,
|
|
4506
|
+
scheme: verdict.scheme,
|
|
4507
|
+
code: "SIGNER_UNAUTHORIZED",
|
|
4508
|
+
reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
|
|
4509
|
+
};
|
|
4510
|
+
return verdict;
|
|
4511
|
+
}
|
|
4512
|
+
/** Throws when the policy does, times out or the job is aborted. */
|
|
4513
|
+
async authorize(signer, documentId, signal) {
|
|
4514
|
+
signal?.throwIfAborted();
|
|
4515
|
+
let timer;
|
|
4516
|
+
let onAbort;
|
|
4517
|
+
const stop = new Promise((_, reject) => {
|
|
4518
|
+
timer = setTimeout(() => {
|
|
4519
|
+
reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
|
|
4520
|
+
}, this.trustTimeoutMs);
|
|
4521
|
+
if (signal) {
|
|
4522
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
4523
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3244
4524
|
}
|
|
3245
|
-
|
|
4525
|
+
});
|
|
4526
|
+
try {
|
|
4527
|
+
return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
|
|
4528
|
+
} finally {
|
|
4529
|
+
clearTimeout(timer);
|
|
4530
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
3246
4531
|
}
|
|
3247
4532
|
}
|
|
4533
|
+
/**
|
|
4534
|
+
* The live candidates stored exactly as submitted and still standing: one
|
|
4535
|
+
* an undo or a reshuffle has since superseded is refused, not re-emitted with
|
|
4536
|
+
* the state it once produced. A synthesized operation
|
|
4537
|
+
* holds another action, so it cannot be compared; only a retry, whose first
|
|
4538
|
+
* attempt this job was, takes it as committed.
|
|
4539
|
+
*/
|
|
4540
|
+
async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
|
|
4541
|
+
const committed = /* @__PURE__ */ new Map();
|
|
4542
|
+
const toCompare = [];
|
|
4543
|
+
for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
|
|
4544
|
+
if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
|
|
4545
|
+
} else if (live.has(entry.opId)) toCompare.push(entry);
|
|
4546
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4547
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4548
|
+
const byId = new Map(stored.map((operation) => [operation.id, operation]));
|
|
4549
|
+
const matching = entries.filter((entry) => {
|
|
4550
|
+
const operation = byId.get(entry.opId);
|
|
4551
|
+
return operation && sameContent(operation.action, entry.action);
|
|
4552
|
+
});
|
|
4553
|
+
if (matching.length === 0) continue;
|
|
4554
|
+
const later = await operationStore.getSince(stream.documentId, stream.scope, stream.branch, Math.min(...matching.map((entry) => byId.get(entry.opId).index)), void 0, void 0, signal);
|
|
4555
|
+
for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
|
|
4556
|
+
}
|
|
4557
|
+
return committed;
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* The load candidates stored exactly as they arrive, timestamp included: a
|
|
4561
|
+
* peer re-sending what this stream holds, not a replay of its action.
|
|
4562
|
+
*/
|
|
4563
|
+
async redelivered(candidates, live, operationStore, signal) {
|
|
4564
|
+
const found = /* @__PURE__ */ new Set();
|
|
4565
|
+
const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
|
|
4566
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4567
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4568
|
+
for (const entry of entries) if (stored.some((operation) => operation.id === entry.opId && Date.parse(operation.timestampUtcMs) === Date.parse(entry.operation?.timestampUtcMs ?? "") && sameContent(operation.action, entry.action))) found.add(entry);
|
|
4569
|
+
}
|
|
4570
|
+
return found;
|
|
4571
|
+
}
|
|
4572
|
+
async liveOperationIds(candidates, operationStore, signal) {
|
|
4573
|
+
const live = /* @__PURE__ */ new Set();
|
|
4574
|
+
for (const [stream, entries] of byStream(candidates)) {
|
|
4575
|
+
const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
|
|
4576
|
+
for (const opId of found) live.add(opId);
|
|
4577
|
+
}
|
|
4578
|
+
return live;
|
|
4579
|
+
}
|
|
4580
|
+
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
4581
|
+
record(job, entry, refusal, path, enforced = this.mode === "enforce") {
|
|
4582
|
+
const event = {
|
|
4583
|
+
jobId: job.id,
|
|
4584
|
+
documentId: entry.stream.documentId,
|
|
4585
|
+
scope: entry.stream.scope,
|
|
4586
|
+
branch: entry.stream.branch,
|
|
4587
|
+
actionId: entry.action.id,
|
|
4588
|
+
code: refusal.code,
|
|
4589
|
+
scheme: refusal.scheme,
|
|
4590
|
+
path,
|
|
4591
|
+
enforced,
|
|
4592
|
+
reason: refusal.reason
|
|
4593
|
+
};
|
|
4594
|
+
this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
|
|
4595
|
+
this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
|
|
4596
|
+
this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
|
|
4597
|
+
});
|
|
4598
|
+
return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
|
|
4599
|
+
}
|
|
3248
4600
|
};
|
|
4601
|
+
/**
|
|
4602
|
+
* A CREATE_DOCUMENT is verified under its own input. Anything else takes the
|
|
4603
|
+
* stored document's policy, or, for a document this job creates, that CREATE's.
|
|
4604
|
+
*/
|
|
4605
|
+
async function resolvePolicies(candidates, jobActions, stored) {
|
|
4606
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4607
|
+
for (const entry of candidates) {
|
|
4608
|
+
if (entry.action.type === "CREATE_DOCUMENT") {
|
|
4609
|
+
entry.policy = createPolicy(entry.action);
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
const { documentId, branch } = entry.stream;
|
|
4613
|
+
const key = `${documentId}\u0000${branch}`;
|
|
4614
|
+
let policy = resolved.get(key);
|
|
4615
|
+
if (policy === void 0) {
|
|
4616
|
+
policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
|
|
4617
|
+
resolved.set(key, policy);
|
|
4618
|
+
}
|
|
4619
|
+
entry.policy = policy;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
async function storedPolicy(stores, source, documentId, branch, signal) {
|
|
4623
|
+
try {
|
|
4624
|
+
if (source === "write-cache") {
|
|
4625
|
+
const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
|
|
4626
|
+
return signaturePolicyOf(document.header);
|
|
4627
|
+
}
|
|
4628
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4629
|
+
return signaturePolicyOf(meta.protocolVersions);
|
|
4630
|
+
} catch (error) {
|
|
4631
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
4632
|
+
throw error;
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
function createdPolicy(actions, documentId) {
|
|
4636
|
+
const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
|
|
4637
|
+
return create ? createPolicy(create) : void 0;
|
|
4638
|
+
}
|
|
4639
|
+
function createPolicy(action) {
|
|
4640
|
+
return signaturePolicyOf(action.input);
|
|
4641
|
+
}
|
|
4642
|
+
function candidate(action, stream) {
|
|
4643
|
+
const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
|
|
4644
|
+
return {
|
|
4645
|
+
action,
|
|
4646
|
+
stream,
|
|
4647
|
+
opId: opId(action.id),
|
|
4648
|
+
synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
function isLive(entry, live) {
|
|
4652
|
+
return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
|
|
4653
|
+
}
|
|
4654
|
+
function mutationStream(action, job) {
|
|
4655
|
+
return {
|
|
4656
|
+
...actionSigningTarget(action, job.documentId, job.branch),
|
|
4657
|
+
scope: job.scope
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
function byStream(candidates) {
|
|
4661
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4662
|
+
for (const entry of candidates) {
|
|
4663
|
+
const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
|
|
4664
|
+
const group = groups.get(key) ?? [entry.stream, []];
|
|
4665
|
+
group[1].push(entry);
|
|
4666
|
+
groups.set(key, group);
|
|
4667
|
+
}
|
|
4668
|
+
return [...groups.values()];
|
|
4669
|
+
}
|
|
4670
|
+
function isRetry(job) {
|
|
4671
|
+
return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
|
|
4672
|
+
}
|
|
4673
|
+
function committedWrite(entry, opId) {
|
|
4674
|
+
return {
|
|
4675
|
+
...entry.stream,
|
|
4676
|
+
actionId: entry.action.id,
|
|
4677
|
+
opId
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
function isSuperseded(operation, later) {
|
|
4681
|
+
return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
|
|
4682
|
+
}
|
|
4683
|
+
function sameContent(stored, submitted) {
|
|
4684
|
+
try {
|
|
4685
|
+
return canonicalJson(stored) === canonicalJson(submitted);
|
|
4686
|
+
} catch {
|
|
4687
|
+
return false;
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
3249
4690
|
//#endregion
|
|
3250
4691
|
//#region src/executor/simple-job-executor.ts
|
|
3251
4692
|
const MAX_SKIP_THRESHOLD = 1e3;
|
|
@@ -3262,7 +4703,32 @@ function isValidISOTimestamp(value) {
|
|
|
3262
4703
|
* returned JobResult there, which is what the queue, the worker protocol and
|
|
3263
4704
|
* every test expect a failed job to look like.
|
|
3264
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
|
+
}
|
|
3265
4730
|
var JobRollbackSignal = class extends Error {
|
|
4731
|
+
result;
|
|
3266
4732
|
constructor(result) {
|
|
3267
4733
|
super("job rolled back");
|
|
3268
4734
|
this.result = result;
|
|
@@ -3270,13 +4736,27 @@ var JobRollbackSignal = class extends Error {
|
|
|
3270
4736
|
}
|
|
3271
4737
|
};
|
|
3272
4738
|
var SimpleJobExecutor = class {
|
|
4739
|
+
logger;
|
|
4740
|
+
registry;
|
|
4741
|
+
operationStore;
|
|
4742
|
+
eventBus;
|
|
4743
|
+
writeCache;
|
|
4744
|
+
operationIndex;
|
|
4745
|
+
documentMetaCache;
|
|
4746
|
+
collectionMembershipCache;
|
|
4747
|
+
driveContainerTypes;
|
|
3273
4748
|
config;
|
|
3274
4749
|
featureFlags;
|
|
3275
4750
|
decisionModel;
|
|
3276
|
-
|
|
4751
|
+
signatureAdmission;
|
|
3277
4752
|
documentActionHandler;
|
|
3278
4753
|
executionScope;
|
|
3279
|
-
|
|
4754
|
+
signer;
|
|
4755
|
+
/**
|
|
4756
|
+
* `signer` signs the operations the reducer synthesizes; unsigned if omitted.
|
|
4757
|
+
* `trustPolicy` decides which keys may sign as which users at admission.
|
|
4758
|
+
*/
|
|
4759
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
|
|
3280
4760
|
this.logger = logger;
|
|
3281
4761
|
this.registry = registry;
|
|
3282
4762
|
this.operationStore = operationStore;
|
|
@@ -3286,6 +4766,8 @@ var SimpleJobExecutor = class {
|
|
|
3286
4766
|
this.documentMetaCache = documentMetaCache;
|
|
3287
4767
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3288
4768
|
this.driveContainerTypes = driveContainerTypes;
|
|
4769
|
+
this.signer = signer ?? new PassthroughSigner();
|
|
4770
|
+
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3289
4771
|
this.config = {
|
|
3290
4772
|
featureFlags: config.featureFlags ?? {},
|
|
3291
4773
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
@@ -3295,12 +4777,19 @@ var SimpleJobExecutor = class {
|
|
|
3295
4777
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3296
4778
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3297
4779
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3298
|
-
batchApplies: config.batchApplies ?? true
|
|
4780
|
+
batchApplies: config.batchApplies ?? true,
|
|
4781
|
+
signatureVerification: config.signatureVerification ?? "enforce",
|
|
4782
|
+
protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols,
|
|
4783
|
+
maxPurgeOperations: config.maxPurgeOperations ?? 2e5
|
|
3299
4784
|
};
|
|
3300
|
-
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3301
4785
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3302
|
-
this.
|
|
3303
|
-
|
|
4786
|
+
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
4787
|
+
signer: this.signer,
|
|
4788
|
+
authEnforcement: this.featureFlags.authEnforcement,
|
|
4789
|
+
policy: trustPolicy,
|
|
4790
|
+
timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
|
|
4791
|
+
});
|
|
4792
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
|
|
3304
4793
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3305
4794
|
}
|
|
3306
4795
|
/**
|
|
@@ -3327,6 +4816,7 @@ var SimpleJobExecutor = class {
|
|
|
3327
4816
|
const touchedStreams = new TouchedStreams();
|
|
3328
4817
|
const postCommitInvalidations = [];
|
|
3329
4818
|
const postCommitMembershipInvalidations = [];
|
|
4819
|
+
const postCommitMetaInvalidations = [];
|
|
3330
4820
|
let outcome;
|
|
3331
4821
|
try {
|
|
3332
4822
|
outcome = await this.executionScope.run(async (stores) => {
|
|
@@ -3337,7 +4827,8 @@ var SimpleJobExecutor = class {
|
|
|
3337
4827
|
signal,
|
|
3338
4828
|
touchedStreams,
|
|
3339
4829
|
postCommitInvalidations,
|
|
3340
|
-
postCommitMembershipInvalidations
|
|
4830
|
+
postCommitMembershipInvalidations,
|
|
4831
|
+
postCommitMetaInvalidations
|
|
3341
4832
|
});
|
|
3342
4833
|
if (!scoped.result.success) throw new JobRollbackSignal(scoped.result);
|
|
3343
4834
|
return scoped;
|
|
@@ -3349,6 +4840,7 @@ var SimpleJobExecutor = class {
|
|
|
3349
4840
|
}
|
|
3350
4841
|
for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
|
|
3351
4842
|
for (const documentId of postCommitMembershipInvalidations) this.collectionMembershipCache.invalidate(documentId);
|
|
4843
|
+
for (const documentId of postCommitMetaInvalidations) this.documentMetaCache.invalidate(documentId);
|
|
3352
4844
|
const { pendingEvent } = outcome;
|
|
3353
4845
|
if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
|
|
3354
4846
|
this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
|
|
@@ -3364,8 +4856,38 @@ var SimpleJobExecutor = class {
|
|
|
3364
4856
|
* has not committed yet has nothing to announce.
|
|
3365
4857
|
*/
|
|
3366
4858
|
async executeInScope(params) {
|
|
3367
|
-
const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
4859
|
+
const { job, startTime, stores: scopeStores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
3368
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
|
+
};
|
|
4889
|
+
const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
|
|
4890
|
+
if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
|
|
3369
4891
|
const indexTxn = stores.operationIndex.start();
|
|
3370
4892
|
if (job.kind === "load") {
|
|
3371
4893
|
const loadResult = await this.executeLoadJob({
|
|
@@ -3378,7 +4900,8 @@ var SimpleJobExecutor = class {
|
|
|
3378
4900
|
evaluatedByPosition: false,
|
|
3379
4901
|
postCommitInvalidations,
|
|
3380
4902
|
postCommitMembershipInvalidations,
|
|
3381
|
-
touchedStreams
|
|
4903
|
+
touchedStreams,
|
|
4904
|
+
purgeFence
|
|
3382
4905
|
});
|
|
3383
4906
|
if (loadResult.success && loadResult.operationsWithContext) {
|
|
3384
4907
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -3408,7 +4931,8 @@ var SimpleJobExecutor = class {
|
|
|
3408
4931
|
evaluatedByPosition: false,
|
|
3409
4932
|
postCommitInvalidations,
|
|
3410
4933
|
postCommitMembershipInvalidations,
|
|
3411
|
-
touchedStreams
|
|
4934
|
+
touchedStreams,
|
|
4935
|
+
purgeFence
|
|
3412
4936
|
});
|
|
3413
4937
|
if (reevalResult.success && reevalResult.operationsWithContext) {
|
|
3414
4938
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -3429,10 +4953,29 @@ var SimpleJobExecutor = class {
|
|
|
3429
4953
|
pendingEvent
|
|
3430
4954
|
};
|
|
3431
4955
|
}
|
|
3432
|
-
|
|
4956
|
+
let admission;
|
|
4957
|
+
try {
|
|
4958
|
+
admission = await this.signatureAdmission.admitMutation(job, stores, signal);
|
|
4959
|
+
} catch (error) {
|
|
4960
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4961
|
+
}
|
|
4962
|
+
if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
|
|
4963
|
+
let committed;
|
|
4964
|
+
try {
|
|
4965
|
+
committed = await this.reloadCommitted(admission.committed, stores, signal);
|
|
4966
|
+
} catch (error) {
|
|
4967
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4968
|
+
}
|
|
4969
|
+
const committedIds = new Set(admission.committed.map((write) => write.actionId));
|
|
4970
|
+
const fresh = committedIds.size === 0 ? job : {
|
|
4971
|
+
...job,
|
|
4972
|
+
actions: job.actions.filter((action) => !committedIds.has(action.id))
|
|
4973
|
+
};
|
|
4974
|
+
if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
|
|
4975
|
+
const positioned = await this.positionByTimestamp(fresh, stores, signal);
|
|
3433
4976
|
if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
|
|
3434
4977
|
const executing = {
|
|
3435
|
-
job,
|
|
4978
|
+
job: fresh,
|
|
3436
4979
|
startTime,
|
|
3437
4980
|
indexTxn,
|
|
3438
4981
|
stores,
|
|
@@ -3441,7 +4984,8 @@ var SimpleJobExecutor = class {
|
|
|
3441
4984
|
evaluatedByPosition: positioned.evaluatedByPosition,
|
|
3442
4985
|
postCommitInvalidations,
|
|
3443
4986
|
postCommitMembershipInvalidations,
|
|
3444
|
-
touchedStreams
|
|
4987
|
+
touchedStreams,
|
|
4988
|
+
purgeFence
|
|
3445
4989
|
};
|
|
3446
4990
|
const actionResult = await this.processActions(positioned.writes, executing);
|
|
3447
4991
|
if (!actionResult.success) return { result: {
|
|
@@ -3462,13 +5006,291 @@ var SimpleJobExecutor = class {
|
|
|
3462
5006
|
} };
|
|
3463
5007
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
3464
5008
|
postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
5009
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
5010
|
+
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
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
|
+
}
|
|
5270
|
+
/** A stored document at a version this reactor does not run is read-only here. */
|
|
5271
|
+
async unsupportedStoredProtocol(job, stores, signal) {
|
|
5272
|
+
let versions;
|
|
5273
|
+
try {
|
|
5274
|
+
versions = this.featureFlags.documentDecisions && job.kind !== "load" ? (await stores.writeCache.getState(job.documentId, "document", job.branch, void 0, signal)).header.protocolVersions : (await stores.documentMetaCache.getDocumentMeta(job.documentId, job.branch, signal)).protocolVersions;
|
|
5275
|
+
} catch {
|
|
5276
|
+
return;
|
|
5277
|
+
}
|
|
5278
|
+
return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
|
|
5279
|
+
}
|
|
5280
|
+
/**
|
|
5281
|
+
* A successful mutation's result and the write-ready event it owes, carrying
|
|
5282
|
+
* what earlier attempts committed ahead of what this one wrote.
|
|
5283
|
+
*/
|
|
5284
|
+
async committedOutcome(job, reloaded, written, generated, stores, startTime) {
|
|
5285
|
+
const operationsWithContext = [...reloaded, ...written];
|
|
5286
|
+
let pendingEvent;
|
|
5287
|
+
if (operationsWithContext.length > 0) {
|
|
5288
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
|
|
3468
5289
|
pendingEvent = {
|
|
3469
5290
|
jobId: job.id,
|
|
3470
|
-
operations:
|
|
5291
|
+
operations: operationsWithContext,
|
|
3471
5292
|
jobMeta: job.meta,
|
|
5293
|
+
submittedActionIds: submittedActionIds(job),
|
|
3472
5294
|
collectionMemberships
|
|
3473
5295
|
};
|
|
3474
5296
|
}
|
|
@@ -3476,14 +5298,62 @@ var SimpleJobExecutor = class {
|
|
|
3476
5298
|
result: {
|
|
3477
5299
|
job,
|
|
3478
5300
|
success: true,
|
|
3479
|
-
operations:
|
|
3480
|
-
operationsWithContext
|
|
5301
|
+
operations: [...reloaded.map((entry) => entry.operation), ...generated],
|
|
5302
|
+
operationsWithContext,
|
|
3481
5303
|
duration: Date.now() - startTime
|
|
3482
5304
|
},
|
|
3483
5305
|
pendingEvent
|
|
3484
5306
|
};
|
|
3485
5307
|
}
|
|
3486
5308
|
/**
|
|
5309
|
+
* The stored operations of writes an earlier attempt committed, as a fresh
|
|
5310
|
+
* write would report them. State is rebuilt from the write cache, and left
|
|
5311
|
+
* out when it cannot be.
|
|
5312
|
+
*/
|
|
5313
|
+
async reloadCommitted(writes, stores, signal) {
|
|
5314
|
+
const streams = /* @__PURE__ */ new Map();
|
|
5315
|
+
for (const write of writes) {
|
|
5316
|
+
const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
|
|
5317
|
+
streams.set(key, [...streams.get(key) ?? [], write]);
|
|
5318
|
+
}
|
|
5319
|
+
const reloaded = [];
|
|
5320
|
+
for (const group of streams.values()) {
|
|
5321
|
+
const { documentId, scope, branch } = group[0];
|
|
5322
|
+
const opIds = [...new Set(group.map((write) => write.opId))];
|
|
5323
|
+
const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
|
|
5324
|
+
const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
|
|
5325
|
+
const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
5326
|
+
for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
|
|
5327
|
+
operation,
|
|
5328
|
+
context: {
|
|
5329
|
+
documentId,
|
|
5330
|
+
scope,
|
|
5331
|
+
branch,
|
|
5332
|
+
documentType,
|
|
5333
|
+
resultingState: await this.rebuiltState(stores, {
|
|
5334
|
+
documentId,
|
|
5335
|
+
scope,
|
|
5336
|
+
branch
|
|
5337
|
+
}, operation, signal),
|
|
5338
|
+
ordinal: ordinals.get(operation.id) ?? 0
|
|
5339
|
+
}
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5342
|
+
return reloaded;
|
|
5343
|
+
}
|
|
5344
|
+
async rebuiltState(stores, stream, operation, signal) {
|
|
5345
|
+
try {
|
|
5346
|
+
const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
|
|
5347
|
+
return JSON.stringify({
|
|
5348
|
+
...document.state,
|
|
5349
|
+
header: document.header
|
|
5350
|
+
});
|
|
5351
|
+
} catch (error) {
|
|
5352
|
+
this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
/**
|
|
3487
5357
|
* Drops the cached state of every stream a job wrote, after its transaction
|
|
3488
5358
|
* did not commit.
|
|
3489
5359
|
*
|
|
@@ -3512,16 +5382,6 @@ var SimpleJobExecutor = class {
|
|
|
3512
5382
|
const actions = writes.map((write) => write.action);
|
|
3513
5383
|
const generatedOperations = [];
|
|
3514
5384
|
const operationsWithContext = [];
|
|
3515
|
-
try {
|
|
3516
|
-
await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
|
|
3517
|
-
} catch (error) {
|
|
3518
|
-
return {
|
|
3519
|
-
success: false,
|
|
3520
|
-
generatedOperations,
|
|
3521
|
-
operationsWithContext,
|
|
3522
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
3523
|
-
};
|
|
3524
|
-
}
|
|
3525
5385
|
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
3526
5386
|
success: false,
|
|
3527
5387
|
generatedOperations,
|
|
@@ -3619,7 +5479,7 @@ var SimpleJobExecutor = class {
|
|
|
3619
5479
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3620
5480
|
documentVersion = docMeta.state.version;
|
|
3621
5481
|
}
|
|
3622
|
-
if (isUndoRedo(action) ||
|
|
5482
|
+
if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3623
5483
|
let document;
|
|
3624
5484
|
if (baseDocument !== void 0) document = baseDocument;
|
|
3625
5485
|
else try {
|
|
@@ -3679,7 +5539,7 @@ var SimpleJobExecutor = class {
|
|
|
3679
5539
|
branch: job.branch,
|
|
3680
5540
|
protocolVersion
|
|
3681
5541
|
};
|
|
3682
|
-
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
5542
|
+
updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
|
|
3683
5543
|
} catch (error) {
|
|
3684
5544
|
const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3685
5545
|
const enhancedError = new Error(contextMessage);
|
|
@@ -3691,6 +5551,15 @@ var SimpleJobExecutor = class {
|
|
|
3691
5551
|
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
3692
5552
|
const newOperation = operations[operations.length - 1];
|
|
3693
5553
|
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
5554
|
+
if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
|
|
5555
|
+
await signSynthesized(newOperation, action, {
|
|
5556
|
+
documentId: job.documentId,
|
|
5557
|
+
scope,
|
|
5558
|
+
branch: job.branch
|
|
5559
|
+
}, this.signer, signal);
|
|
5560
|
+
} catch (error) {
|
|
5561
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
5562
|
+
}
|
|
3694
5563
|
const resultingState = JSON.stringify({
|
|
3695
5564
|
...updatedDocument.state,
|
|
3696
5565
|
header: updatedDocument.header
|
|
@@ -3791,7 +5660,7 @@ var SimpleJobExecutor = class {
|
|
|
3791
5660
|
* apply and its own reasons for it.
|
|
3792
5661
|
* - A positional or replayed run carries skips and re-appended operations,
|
|
3793
5662
|
* whose indices are not a simple ascending run from the head.
|
|
3794
|
-
* - UNDO, REDO
|
|
5663
|
+
* - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
|
|
3795
5664
|
* force a full-history rebuild, so they cannot be reduced against state
|
|
3796
5665
|
* threaded from the write before them.
|
|
3797
5666
|
* - The auth scope decides later writes against the policy earlier ones
|
|
@@ -3809,7 +5678,7 @@ var SimpleJobExecutor = class {
|
|
|
3809
5678
|
if (scope === "auth" || scope === "document") return false;
|
|
3810
5679
|
return writes.every((write) => {
|
|
3811
5680
|
const type = write.action.type;
|
|
3812
|
-
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "
|
|
5681
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
|
|
3813
5682
|
});
|
|
3814
5683
|
}
|
|
3815
5684
|
/**
|
|
@@ -3932,13 +5801,22 @@ var SimpleJobExecutor = class {
|
|
|
3932
5801
|
}
|
|
3933
5802
|
if (!backdated) return plain();
|
|
3934
5803
|
const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
|
|
3935
|
-
|
|
5804
|
+
const nothingToMove = async () => {
|
|
3936
5805
|
if (!this.featureFlags.authEnforcement) return plain();
|
|
3937
5806
|
return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
|
|
3938
|
-
}
|
|
5807
|
+
};
|
|
5808
|
+
if (conflicting.length === 0) return nothingToMove();
|
|
3939
5809
|
const nextIndex = revisions.revision[job.scope] ?? 0;
|
|
3940
5810
|
let firstConflicting = conflicting[0].index;
|
|
3941
5811
|
for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
|
|
5812
|
+
const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
|
|
5813
|
+
const conflictingIds = new Set(conflicting.map((operation) => operation.id));
|
|
5814
|
+
const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
|
|
5815
|
+
const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
|
|
5816
|
+
const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
|
|
5817
|
+
if (moving.length === 0) return nothingToMove();
|
|
5818
|
+
let firstRetracted = moving[0].index;
|
|
5819
|
+
for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
|
|
3942
5820
|
const incoming = job.actions.map((action, i) => ({
|
|
3943
5821
|
id: action.id,
|
|
3944
5822
|
index: nextIndex + i,
|
|
@@ -3949,8 +5827,8 @@ var SimpleJobExecutor = class {
|
|
|
3949
5827
|
}));
|
|
3950
5828
|
const merged = reshuffleByTimestamp({
|
|
3951
5829
|
index: nextIndex,
|
|
3952
|
-
skip: retractionSkip(nextIndex,
|
|
3953
|
-
},
|
|
5830
|
+
skip: retractionSkip(nextIndex, firstRetracted)
|
|
5831
|
+
}, moving, incoming);
|
|
3954
5832
|
stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3955
5833
|
if (!this.featureFlags.authEnforcement) return {
|
|
3956
5834
|
writes: merged.map((operation) => ({
|
|
@@ -4098,7 +5976,7 @@ var SimpleJobExecutor = class {
|
|
|
4098
5976
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
4099
5977
|
const result = await this.processActions(tail.map((operation, i) => ({
|
|
4100
5978
|
action: operation.action,
|
|
4101
|
-
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
|
|
5979
|
+
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
|
|
4102
5980
|
sourceRemote: "",
|
|
4103
5981
|
deniedReason: reevaluated[firstChange + i]
|
|
4104
5982
|
})), {
|
|
@@ -4187,36 +6065,32 @@ var SimpleJobExecutor = class {
|
|
|
4187
6065
|
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
4188
6066
|
duration: Date.now() - startTime
|
|
4189
6067
|
};
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
const
|
|
4195
|
-
if (
|
|
4196
|
-
|
|
4197
|
-
let conflictingOps;
|
|
4198
|
-
try {
|
|
4199
|
-
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
4200
|
-
} catch {
|
|
4201
|
-
conflictingOps = [];
|
|
4202
|
-
}
|
|
4203
|
-
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
4204
|
-
if (conflictingOps.length > 0) {
|
|
4205
|
-
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
6068
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
6069
|
+
const admitted = /* @__PURE__ */ new Set();
|
|
6070
|
+
let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
|
|
6071
|
+
for (;;) {
|
|
6072
|
+
const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
|
|
6073
|
+
if (unadmitted.length === 0) break;
|
|
6074
|
+
let refused;
|
|
4206
6075
|
try {
|
|
4207
|
-
|
|
4208
|
-
} catch {
|
|
4209
|
-
|
|
6076
|
+
refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
|
|
6077
|
+
} catch (error) {
|
|
6078
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4210
6079
|
}
|
|
6080
|
+
for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
|
|
6081
|
+
else admitted.add(operation);
|
|
6082
|
+
if (refused.size === 0) break;
|
|
6083
|
+
const remaining = job.operations.filter((operation) => !dropped.has(operation));
|
|
6084
|
+
if (remaining.length === 0) return {
|
|
6085
|
+
job,
|
|
6086
|
+
success: true,
|
|
6087
|
+
operations: [],
|
|
6088
|
+
operationsWithContext: [],
|
|
6089
|
+
duration: Date.now() - startTime
|
|
6090
|
+
};
|
|
6091
|
+
selection = await this.selectLoadWrites(job, remaining, stores, signal);
|
|
4211
6092
|
}
|
|
4212
|
-
const
|
|
4213
|
-
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
4214
|
-
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
4215
|
-
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
4216
|
-
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
4217
|
-
}
|
|
4218
|
-
return true;
|
|
4219
|
-
});
|
|
6093
|
+
const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
|
|
4220
6094
|
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
4221
6095
|
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
4222
6096
|
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
@@ -4237,14 +6111,6 @@ var SimpleJobExecutor = class {
|
|
|
4237
6111
|
const logicalSkip = latestRevision - minLogicalIndex;
|
|
4238
6112
|
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
4239
6113
|
}
|
|
4240
|
-
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
4241
|
-
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
4242
|
-
const incomingOpsToApply = job.operations.filter((op) => {
|
|
4243
|
-
if (existingActionIds.has(op.action.id)) return false;
|
|
4244
|
-
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
4245
|
-
seenIncomingActionIds.add(op.action.id);
|
|
4246
|
-
return true;
|
|
4247
|
-
});
|
|
4248
6114
|
if (incomingOpsToApply.length === 0) return {
|
|
4249
6115
|
job,
|
|
4250
6116
|
success: true,
|
|
@@ -4272,7 +6138,8 @@ var SimpleJobExecutor = class {
|
|
|
4272
6138
|
...operation,
|
|
4273
6139
|
id: operation.id
|
|
4274
6140
|
})));
|
|
4275
|
-
|
|
6141
|
+
const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
|
|
6142
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
|
|
4276
6143
|
let deniedReasons;
|
|
4277
6144
|
if (this.featureFlags.documentDecisions) try {
|
|
4278
6145
|
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
@@ -4324,6 +6191,71 @@ var SimpleJobExecutor = class {
|
|
|
4324
6191
|
duration: Date.now() - startTime
|
|
4325
6192
|
};
|
|
4326
6193
|
}
|
|
6194
|
+
/** The conflicting window a load opens, and which of its operations are new. */
|
|
6195
|
+
async selectLoadWrites(job, operations, stores, signal) {
|
|
6196
|
+
const scope = job.scope;
|
|
6197
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
6198
|
+
let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
|
|
6199
|
+
for (const operation of operations) {
|
|
6200
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
6201
|
+
const ts = operation.timestampUtcMs || "";
|
|
6202
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
6203
|
+
}
|
|
6204
|
+
let conflictingOps;
|
|
6205
|
+
try {
|
|
6206
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
6207
|
+
} catch {
|
|
6208
|
+
conflictingOps = [];
|
|
6209
|
+
}
|
|
6210
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
6211
|
+
if (conflictingOps.length > 0) {
|
|
6212
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
6213
|
+
try {
|
|
6214
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
6215
|
+
} catch {
|
|
6216
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
const incomingActionIds = new Set(operations.map((op) => op.action.id));
|
|
6220
|
+
const supersededBy = (op) => {
|
|
6221
|
+
let nearest;
|
|
6222
|
+
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0 && laterOp.index - laterOp.skip <= op.index && (nearest === void 0 || laterOp.index < nearest.index)) nearest = laterOp;
|
|
6223
|
+
return nearest;
|
|
6224
|
+
};
|
|
6225
|
+
const undoneActionIds = /* @__PURE__ */ new Set();
|
|
6226
|
+
let predecessorBound = minIncomingIndex;
|
|
6227
|
+
const rowsByActionId = /* @__PURE__ */ new Map();
|
|
6228
|
+
for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
|
|
6229
|
+
const liveOps = /* @__PURE__ */ new Set();
|
|
6230
|
+
for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
|
|
6231
|
+
for (const [actionId, rows] of rowsByActionId) {
|
|
6232
|
+
if (rows.some((row) => liveOps.has(row))) continue;
|
|
6233
|
+
if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
|
|
6234
|
+
else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
|
|
6235
|
+
}
|
|
6236
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
6237
|
+
if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
|
|
6238
|
+
return liveOps.has(op);
|
|
6239
|
+
});
|
|
6240
|
+
let rewoundFrom = Number.POSITIVE_INFINITY;
|
|
6241
|
+
for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
|
|
6242
|
+
const reappended = new Set(nonSupersededOps.map((op) => op.index));
|
|
6243
|
+
for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
|
|
6244
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
6245
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
6246
|
+
const incomingOpsToApply = operations.filter((op) => {
|
|
6247
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
6248
|
+
if (undoneActionIds.has(op.action.id)) return false;
|
|
6249
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
6250
|
+
seenIncomingActionIds.add(op.action.id);
|
|
6251
|
+
return true;
|
|
6252
|
+
});
|
|
6253
|
+
return {
|
|
6254
|
+
nonSupersededOps,
|
|
6255
|
+
allOpsFromMinConflictingIndex,
|
|
6256
|
+
incomingOpsToApply
|
|
6257
|
+
};
|
|
6258
|
+
}
|
|
4327
6259
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
4328
6260
|
if (!result.success) return result;
|
|
4329
6261
|
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
@@ -4470,6 +6402,7 @@ var DocumentModelRegistry = class {
|
|
|
4470
6402
|
//#endregion
|
|
4471
6403
|
//#region src/storage/kysely/keyframe-store.ts
|
|
4472
6404
|
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
6405
|
+
db;
|
|
4473
6406
|
trx;
|
|
4474
6407
|
constructor(db) {
|
|
4475
6408
|
this.db = db;
|
|
@@ -4484,19 +6417,28 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
|
4484
6417
|
}
|
|
4485
6418
|
async putKeyframe(documentId, scope, branch, revision, document, signal) {
|
|
4486
6419
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
4487
|
-
|
|
4488
|
-
documentId,
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
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);
|
|
4500
6442
|
}
|
|
4501
6443
|
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
4502
6444
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -4554,6 +6496,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
4554
6496
|
//#endregion
|
|
4555
6497
|
//#region src/storage/txn.ts
|
|
4556
6498
|
var AtomicTransaction = class {
|
|
6499
|
+
documentId;
|
|
6500
|
+
documentType;
|
|
6501
|
+
scope;
|
|
6502
|
+
branch;
|
|
6503
|
+
baseRevision;
|
|
4557
6504
|
operations = [];
|
|
4558
6505
|
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
4559
6506
|
this.documentId = documentId;
|
|
@@ -4587,6 +6534,11 @@ var AtomicTransaction = class {
|
|
|
4587
6534
|
//#endregion
|
|
4588
6535
|
//#region src/storage/kysely/store.ts
|
|
4589
6536
|
var _UniqueConstraintContext = class extends Error {
|
|
6537
|
+
documentId;
|
|
6538
|
+
scope;
|
|
6539
|
+
branch;
|
|
6540
|
+
revision;
|
|
6541
|
+
stagedOps;
|
|
4590
6542
|
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
4591
6543
|
super("unique constraint");
|
|
4592
6544
|
this.documentId = documentId;
|
|
@@ -4598,16 +6550,20 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
4598
6550
|
}
|
|
4599
6551
|
};
|
|
4600
6552
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
6553
|
+
db;
|
|
4601
6554
|
trx;
|
|
6555
|
+
liveIds;
|
|
4602
6556
|
constructor(db) {
|
|
4603
6557
|
this.db = db;
|
|
4604
6558
|
}
|
|
4605
6559
|
get queryExecutor() {
|
|
4606
6560
|
return this.trx ?? this.db;
|
|
4607
6561
|
}
|
|
4608
|
-
|
|
6562
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
6563
|
+
withTransaction(trx, liveIds) {
|
|
4609
6564
|
const instance = new KyselyOperationStore(this.db);
|
|
4610
6565
|
instance.trx = trx;
|
|
6566
|
+
instance.liveIds = liveIds;
|
|
4611
6567
|
return instance;
|
|
4612
6568
|
}
|
|
4613
6569
|
async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
@@ -4652,6 +6608,8 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4652
6608
|
await fn(atomicTxn);
|
|
4653
6609
|
const operations = atomicTxn.getOperations();
|
|
4654
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;
|
|
4655
6613
|
if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
|
|
4656
6614
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
4657
6615
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
@@ -4697,12 +6655,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4697
6655
|
* otherwise read a snapshot taken before the locks were held.
|
|
4698
6656
|
*/
|
|
4699
6657
|
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
4700
|
-
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
6658
|
+
const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
|
|
4701
6659
|
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
6660
|
+
const sortedKeys = sql.join([...keys].sort());
|
|
4702
6661
|
await sql`
|
|
4703
6662
|
with ordered as materialized (
|
|
4704
6663
|
select key
|
|
4705
|
-
from unnest(array[${
|
|
6664
|
+
from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
|
|
4706
6665
|
order by ord
|
|
4707
6666
|
)
|
|
4708
6667
|
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
@@ -4766,6 +6725,24 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4766
6725
|
}
|
|
4767
6726
|
return storedRows.map((row) => this.rowToOperation(row));
|
|
4768
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
|
+
}
|
|
4769
6746
|
/** True when the staged write creates a document rather than appending to one. */
|
|
4770
6747
|
isCreate(operations) {
|
|
4771
6748
|
for (const operation of operations) {
|
|
@@ -4779,6 +6756,14 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4779
6756
|
}
|
|
4780
6757
|
return false;
|
|
4781
6758
|
}
|
|
6759
|
+
/**
|
|
6760
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
6761
|
+
* last row returned), not the last row's own index. This keeps "0" an
|
|
6762
|
+
* unambiguous start-of-stream sentinel even when a page ends at index 0
|
|
6763
|
+
* (e.g. `limit: 1` on a fresh stream), which would otherwise make
|
|
6764
|
+
* `nextCursor` equal the start cursor and loop a caller that walks pages
|
|
6765
|
+
* forever.
|
|
6766
|
+
*/
|
|
4782
6767
|
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
4783
6768
|
throwIfAborted(signal);
|
|
4784
6769
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
@@ -4793,10 +6778,10 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4793
6778
|
}
|
|
4794
6779
|
if (paging) {
|
|
4795
6780
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4796
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6781
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4797
6782
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4798
6783
|
}
|
|
4799
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
6784
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
4800
6785
|
cursor,
|
|
4801
6786
|
limit
|
|
4802
6787
|
}, signal));
|
|
@@ -4814,26 +6799,28 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4814
6799
|
limit
|
|
4815
6800
|
}, signal));
|
|
4816
6801
|
}
|
|
6802
|
+
/**
|
|
6803
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
6804
|
+
* last row returned), not the last row's own index, for the same reason as
|
|
6805
|
+
* `getSince`: a page ending at index 0 must not produce a cursor that is
|
|
6806
|
+
* indistinguishable from the start-of-stream sentinel.
|
|
6807
|
+
*/
|
|
4817
6808
|
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
4818
6809
|
throwIfAborted(signal);
|
|
4819
6810
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
|
|
4820
6811
|
if (paging) {
|
|
4821
6812
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4822
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6813
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4823
6814
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4824
6815
|
}
|
|
4825
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
6816
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
4826
6817
|
cursor,
|
|
4827
6818
|
limit
|
|
4828
6819
|
}, signal));
|
|
4829
6820
|
}
|
|
4830
6821
|
async getRevisions(documentId, branch, signal) {
|
|
4831
6822
|
throwIfAborted(signal);
|
|
4832
|
-
const scopeRevisions = await this.queryExecutor.selectFrom("Operation as
|
|
4833
|
-
"o1.scope",
|
|
4834
|
-
"o1.index",
|
|
4835
|
-
"o1.timestampUtcMs"
|
|
4836
|
-
]).where("o1.documentId", "=", documentId).where("o1.branch", "=", branch).where((eb) => eb("o1.index", "=", eb.selectFrom("Operation as o2").select((eb2) => eb2.fn.max("o2.index").as("maxIndex")).where("o2.documentId", "=", eb.ref("o1.documentId")).where("o2.branch", "=", eb.ref("o1.branch")).where("o2.scope", "=", eb.ref("o1.scope")))).execute();
|
|
6823
|
+
const scopeRevisions = await this.queryExecutor.selectFrom("Operation").select("scope").select((eb) => eb.fn.max("index").as("index")).where("documentId", "=", documentId).where("branch", "=", branch).groupBy("scope").execute();
|
|
4837
6824
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
|
|
4838
6825
|
const revision = {};
|
|
4839
6826
|
for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
|
|
@@ -4846,6 +6833,20 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4846
6833
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).executeTakeFirst();
|
|
4847
6834
|
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
4848
6835
|
}
|
|
6836
|
+
async findOperationIds(documentId, scope, branch, opIds, signal) {
|
|
6837
|
+
throwIfAborted(signal);
|
|
6838
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Set();
|
|
6839
|
+
const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
|
|
6840
|
+
return new Set(rows.map((row) => row.opId));
|
|
6841
|
+
}
|
|
6842
|
+
async getOperationsByIds(documentId, scope, branch, opIds, signal) {
|
|
6843
|
+
throwIfAborted(signal);
|
|
6844
|
+
if (opIds.length === 0) return [];
|
|
6845
|
+
const rows = await this.queryExecutor.selectFrom("Operation").selectAll().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "asc").execute();
|
|
6846
|
+
const latest = /* @__PURE__ */ new Map();
|
|
6847
|
+
for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
|
|
6848
|
+
return [...latest.values()];
|
|
6849
|
+
}
|
|
4849
6850
|
rowToOperation(row) {
|
|
4850
6851
|
return {
|
|
4851
6852
|
index: row.index,
|
|
@@ -4939,8 +6940,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
4939
6940
|
}
|
|
4940
6941
|
//#endregion
|
|
4941
6942
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
4942
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4943
|
-
async function up$
|
|
6943
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
|
|
6944
|
+
async function up$25(db) {
|
|
4944
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", [
|
|
4945
6946
|
"documentId",
|
|
4946
6947
|
"scope",
|
|
@@ -4965,8 +6966,8 @@ async function up$18(db) {
|
|
|
4965
6966
|
}
|
|
4966
6967
|
//#endregion
|
|
4967
6968
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
4968
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4969
|
-
async function up$
|
|
6969
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
|
|
6970
|
+
async function up$24(db) {
|
|
4970
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", [
|
|
4971
6972
|
"documentId",
|
|
4972
6973
|
"scope",
|
|
@@ -4982,14 +6983,14 @@ async function up$17(db) {
|
|
|
4982
6983
|
}
|
|
4983
6984
|
//#endregion
|
|
4984
6985
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
4985
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4986
|
-
async function up$
|
|
6986
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
|
|
6987
|
+
async function up$23(db) {
|
|
4987
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();
|
|
4988
6989
|
}
|
|
4989
6990
|
//#endregion
|
|
4990
6991
|
//#region src/storage/migrations/004_create_document_relationship_table.ts
|
|
4991
|
-
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4992
|
-
async function up$
|
|
6992
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
|
|
6993
|
+
async function up$22(db) {
|
|
4993
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", [
|
|
4994
6995
|
"sourceId",
|
|
4995
6996
|
"targetId",
|
|
@@ -5001,14 +7002,14 @@ async function up$15(db) {
|
|
|
5001
7002
|
}
|
|
5002
7003
|
//#endregion
|
|
5003
7004
|
//#region src/storage/migrations/005_create_indexer_state_table.ts
|
|
5004
|
-
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5005
|
-
async function up$
|
|
7005
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
|
|
7006
|
+
async function up$21(db) {
|
|
5006
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();
|
|
5007
7008
|
}
|
|
5008
7009
|
//#endregion
|
|
5009
7010
|
//#region src/storage/migrations/006_create_document_snapshot_table.ts
|
|
5010
|
-
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5011
|
-
async function up$
|
|
7011
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
|
|
7012
|
+
async function up$20(db) {
|
|
5012
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", [
|
|
5013
7014
|
"documentId",
|
|
5014
7015
|
"scope",
|
|
@@ -5029,8 +7030,8 @@ async function up$13(db) {
|
|
|
5029
7030
|
}
|
|
5030
7031
|
//#endregion
|
|
5031
7032
|
//#region src/storage/migrations/007_create_slug_mapping_table.ts
|
|
5032
|
-
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5033
|
-
async function up$
|
|
7033
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
|
|
7034
|
+
async function up$19(db) {
|
|
5034
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", [
|
|
5035
7036
|
"documentId",
|
|
5036
7037
|
"scope",
|
|
@@ -5040,14 +7041,14 @@ async function up$12(db) {
|
|
|
5040
7041
|
}
|
|
5041
7042
|
//#endregion
|
|
5042
7043
|
//#region src/storage/migrations/008_create_view_state_table.ts
|
|
5043
|
-
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5044
|
-
async function up$
|
|
7044
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
|
|
7045
|
+
async function up$18(db) {
|
|
5045
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();
|
|
5046
7047
|
}
|
|
5047
7048
|
//#endregion
|
|
5048
7049
|
//#region src/storage/migrations/009_create_operation_index_tables.ts
|
|
5049
|
-
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5050
|
-
async function up$
|
|
7050
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
|
|
7051
|
+
async function up$17(db) {
|
|
5051
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();
|
|
5052
7053
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
5053
7054
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -5061,8 +7062,8 @@ async function up$10(db) {
|
|
|
5061
7062
|
}
|
|
5062
7063
|
//#endregion
|
|
5063
7064
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
5064
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5065
|
-
async function up$
|
|
7065
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
|
|
7066
|
+
async function up$16(db) {
|
|
5066
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();
|
|
5067
7068
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
5068
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();
|
|
@@ -5070,8 +7071,8 @@ async function up$9(db) {
|
|
|
5070
7071
|
}
|
|
5071
7072
|
//#endregion
|
|
5072
7073
|
//#region src/storage/migrations/011_add_cursor_type_column.ts
|
|
5073
|
-
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5074
|
-
async function up$
|
|
7074
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
7075
|
+
async function up$15(db) {
|
|
5075
7076
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
5076
7077
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
5077
7078
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -5080,64 +7081,64 @@ async function up$8(db) {
|
|
|
5080
7081
|
}
|
|
5081
7082
|
//#endregion
|
|
5082
7083
|
//#region src/storage/migrations/012_add_source_remote_column.ts
|
|
5083
|
-
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5084
|
-
async function up$
|
|
7084
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
7085
|
+
async function up$14(db) {
|
|
5085
7086
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
5086
7087
|
}
|
|
5087
7088
|
//#endregion
|
|
5088
7089
|
//#region src/storage/migrations/013_create_sync_dead_letters_table.ts
|
|
5089
|
-
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5090
|
-
async function up$
|
|
7090
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
7091
|
+
async function up$13(db) {
|
|
5091
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();
|
|
5092
7093
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
5093
7094
|
}
|
|
5094
7095
|
//#endregion
|
|
5095
7096
|
//#region src/storage/migrations/014_create_processor_cursor_table.ts
|
|
5096
|
-
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5097
|
-
async function up$
|
|
7097
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
7098
|
+
async function up$12(db) {
|
|
5098
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();
|
|
5099
7100
|
}
|
|
5100
7101
|
//#endregion
|
|
5101
7102
|
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
5102
7103
|
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
5103
|
-
down: () => down$
|
|
5104
|
-
up: () => up$
|
|
7104
|
+
down: () => down$11,
|
|
7105
|
+
up: () => up$11
|
|
5105
7106
|
});
|
|
5106
7107
|
/**
|
|
5107
7108
|
* Records why authorization refused an operation. Separate from `error` so a
|
|
5108
7109
|
* denial is distinguishable from a reducer failure without matching on a
|
|
5109
7110
|
* message. Null for every operation written before decisions were enforced.
|
|
5110
7111
|
*/
|
|
5111
|
-
async function up$
|
|
7112
|
+
async function up$11(db) {
|
|
5112
7113
|
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
5113
7114
|
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
5114
7115
|
}
|
|
5115
|
-
async function down$
|
|
7116
|
+
async function down$11(db) {
|
|
5116
7117
|
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
5117
7118
|
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
5118
7119
|
}
|
|
5119
7120
|
//#endregion
|
|
5120
7121
|
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
5121
7122
|
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
5122
|
-
down: () => down$
|
|
5123
|
-
up: () => up$
|
|
7123
|
+
down: () => down$10,
|
|
7124
|
+
up: () => up$10
|
|
5124
7125
|
});
|
|
5125
7126
|
/**
|
|
5126
7127
|
* The classification a dead letter falls into, stored because it decides whether
|
|
5127
7128
|
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
5128
7129
|
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
5129
7130
|
*/
|
|
5130
|
-
async function up$
|
|
7131
|
+
async function up$10(db) {
|
|
5131
7132
|
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
5132
7133
|
}
|
|
5133
|
-
async function down$
|
|
7134
|
+
async function down$10(db) {
|
|
5134
7135
|
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
5135
7136
|
}
|
|
5136
7137
|
//#endregion
|
|
5137
7138
|
//#region src/storage/migrations/017_create_group_references.ts
|
|
5138
7139
|
var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
5139
|
-
down: () => down$
|
|
5140
|
-
up: () => up$
|
|
7140
|
+
down: () => down$9,
|
|
7141
|
+
up: () => up$9
|
|
5141
7142
|
});
|
|
5142
7143
|
/**
|
|
5143
7144
|
* One row per (document, group) reference ever discovered from an auth
|
|
@@ -5148,18 +7149,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
|
5148
7149
|
* requires (sync), and by groupId for the documents a group change affects
|
|
5149
7150
|
* (re-evaluation).
|
|
5150
7151
|
*/
|
|
5151
|
-
async function up$
|
|
7152
|
+
async function up$9(db) {
|
|
5152
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();
|
|
5153
7154
|
await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
|
|
5154
7155
|
}
|
|
5155
|
-
async function down$
|
|
7156
|
+
async function down$9(db) {
|
|
5156
7157
|
await db.schema.dropTable("group_references").execute();
|
|
5157
7158
|
}
|
|
5158
7159
|
//#endregion
|
|
5159
7160
|
//#region src/storage/migrations/018_add_sync_remote_bound_address.ts
|
|
5160
7161
|
var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
5161
|
-
down: () => down$
|
|
5162
|
-
up: () => up$
|
|
7162
|
+
down: () => down$8,
|
|
7163
|
+
up: () => up$8
|
|
5163
7164
|
});
|
|
5164
7165
|
/**
|
|
5165
7166
|
* The address a sync channel is bound to, so a channel created by one subject
|
|
@@ -5170,17 +7171,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
|
5170
7171
|
* until its first authenticated poll adopts it. A default would claim them all
|
|
5171
7172
|
* for one address.
|
|
5172
7173
|
*/
|
|
5173
|
-
async function up$
|
|
7174
|
+
async function up$8(db) {
|
|
5174
7175
|
await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
|
|
5175
7176
|
}
|
|
5176
|
-
async function down$
|
|
7177
|
+
async function down$8(db) {
|
|
5177
7178
|
await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
|
|
5178
7179
|
}
|
|
5179
7180
|
//#endregion
|
|
5180
7181
|
//#region src/storage/migrations/019_require_action_id.ts
|
|
5181
7182
|
var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
5182
|
-
down: () => down,
|
|
5183
|
-
up: () => up
|
|
7183
|
+
down: () => down$7,
|
|
7184
|
+
up: () => up$7
|
|
5184
7185
|
});
|
|
5185
7186
|
/**
|
|
5186
7187
|
* Makes an operation whose action carries no id physically unstorable.
|
|
@@ -5207,7 +7208,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
|
5207
7208
|
* The empty string is rejected alongside null. It derives the same colliding
|
|
5208
7209
|
* operation id as an absent id, so admitting it would leave the hole open.
|
|
5209
7210
|
*/
|
|
5210
|
-
async function up(db) {
|
|
7211
|
+
async function up$7(db) {
|
|
5211
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();
|
|
5212
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();
|
|
5213
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();
|
|
@@ -5219,11 +7220,118 @@ async function up(db) {
|
|
|
5219
7220
|
* their operations are now known by, and reverting them would reintroduce the
|
|
5220
7221
|
* collision the migration removed.
|
|
5221
7222
|
*/
|
|
5222
|
-
async function down(db) {
|
|
7223
|
+
async function down$7(db) {
|
|
5223
7224
|
await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
|
|
5224
7225
|
await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
|
|
5225
7226
|
}
|
|
5226
7227
|
//#endregion
|
|
7228
|
+
//#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
|
|
7229
|
+
var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
|
|
7230
|
+
down: () => down$6,
|
|
7231
|
+
up: () => up$6
|
|
7232
|
+
});
|
|
7233
|
+
/** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
|
|
7234
|
+
async function up$6(db) {
|
|
7235
|
+
await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
7236
|
+
}
|
|
7237
|
+
async function down$6(db) {
|
|
7238
|
+
await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
|
|
7239
|
+
}
|
|
7240
|
+
//#endregion
|
|
7241
|
+
//#region src/storage/migrations/021_add_sync_remote_peer.ts
|
|
7242
|
+
var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
|
|
7243
|
+
down: () => down$5,
|
|
7244
|
+
up: () => up$5
|
|
7245
|
+
});
|
|
7246
|
+
/**
|
|
7247
|
+
* The peer's manifest as canonical JSON, and when it was heard. Both null: not
|
|
7248
|
+
* yet heard. A time with no manifest: a silent peer, which gets the baselines.
|
|
7249
|
+
*/
|
|
7250
|
+
async function up$5(db) {
|
|
7251
|
+
await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
|
|
7252
|
+
}
|
|
7253
|
+
async function down$5(db) {
|
|
7254
|
+
await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
|
|
7255
|
+
}
|
|
7256
|
+
//#endregion
|
|
7257
|
+
//#region src/storage/migrations/022_create_sync_holds.ts
|
|
7258
|
+
var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
|
|
7259
|
+
down: () => down$4,
|
|
7260
|
+
up: () => up$4
|
|
7261
|
+
});
|
|
7262
|
+
/** Documents held back from a remote whose peer cannot run their versions. */
|
|
7263
|
+
async function up$4(db) {
|
|
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", [
|
|
7265
|
+
"remote_name",
|
|
7266
|
+
"document_id",
|
|
7267
|
+
"branch"
|
|
7268
|
+
]).execute();
|
|
7269
|
+
}
|
|
7270
|
+
async function down$4(db) {
|
|
7271
|
+
await db.schema.dropTable("sync_holds").execute();
|
|
7272
|
+
}
|
|
7273
|
+
//#endregion
|
|
7274
|
+
//#region src/storage/migrations/023_index_created_protocol_versions.ts
|
|
7275
|
+
var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
|
|
7276
|
+
down: () => down$3,
|
|
7277
|
+
up: () => up$3
|
|
7278
|
+
});
|
|
7279
|
+
/**
|
|
7280
|
+
* One entry per document created with protocolVersions, keyed by a hash so an
|
|
7281
|
+
* oversized header cannot make its creation unstorable. Startup reads the
|
|
7282
|
+
* distinct values without visiting every document.
|
|
7283
|
+
*/
|
|
7284
|
+
async function up$3(db) {
|
|
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();
|
|
7286
|
+
}
|
|
7287
|
+
async function down$3(db) {
|
|
7288
|
+
await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
|
|
7289
|
+
}
|
|
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
|
|
5227
7335
|
//#region src/storage/migrations/migrator.ts
|
|
5228
7336
|
const REACTOR_SCHEMA = "reactor";
|
|
5229
7337
|
const migrations = {
|
|
@@ -5245,7 +7353,14 @@ const migrations = {
|
|
|
5245
7353
|
"016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
|
|
5246
7354
|
"017_create_group_references": _017_create_group_references_exports,
|
|
5247
7355
|
"018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
|
|
5248
|
-
"019_require_action_id": _019_require_action_id_exports
|
|
7356
|
+
"019_require_action_id": _019_require_action_id_exports,
|
|
7357
|
+
"020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
|
|
7358
|
+
"021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
|
|
7359
|
+
"022_create_sync_holds": _022_create_sync_holds_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
|
|
5249
7364
|
};
|
|
5250
7365
|
var ProgrammaticMigrationProvider = class {
|
|
5251
7366
|
getMigrations() {
|
|
@@ -5303,8 +7418,155 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
5303
7418
|
}
|
|
5304
7419
|
//#endregion
|
|
5305
7420
|
//#region src/core/drive-container-types.ts
|
|
5306
|
-
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
7421
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
7422
|
+
//#endregion
|
|
7423
|
+
//#region src/executor/worker/error-info.ts
|
|
7424
|
+
const MAX_STACK_LENGTH = 8192;
|
|
7425
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
7426
|
+
function toErrorInfo(err, depth = 0) {
|
|
7427
|
+
if (depth > MAX_CAUSE_DEPTH) return {
|
|
7428
|
+
name: "Error",
|
|
7429
|
+
message: "cause chain truncated"
|
|
7430
|
+
};
|
|
7431
|
+
if (err instanceof Error) {
|
|
7432
|
+
const info = {
|
|
7433
|
+
name: err.name || "Error",
|
|
7434
|
+
message: err.message
|
|
7435
|
+
};
|
|
7436
|
+
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
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;
|
|
7440
|
+
return info;
|
|
7441
|
+
}
|
|
7442
|
+
if (typeof err === "string") return {
|
|
7443
|
+
name: "Error",
|
|
7444
|
+
message: err
|
|
7445
|
+
};
|
|
7446
|
+
if (err && typeof err === "object") {
|
|
7447
|
+
const record = err;
|
|
7448
|
+
return {
|
|
7449
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
7450
|
+
message: typeof record.message === "string" ? record.message : safeStringify(err)
|
|
7451
|
+
};
|
|
7452
|
+
}
|
|
7453
|
+
return {
|
|
7454
|
+
name: "Error",
|
|
7455
|
+
message: safeStringify(err)
|
|
7456
|
+
};
|
|
7457
|
+
}
|
|
7458
|
+
function safeStringify(value) {
|
|
7459
|
+
if (value === null || value === void 0) return String(value);
|
|
7460
|
+
if (typeof value === "string") return value;
|
|
7461
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
7462
|
+
try {
|
|
7463
|
+
return JSON.stringify(value);
|
|
7464
|
+
} catch {
|
|
7465
|
+
return "[unserializable]";
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
function fromErrorInfo(info) {
|
|
7469
|
+
const err = new Error(info.message);
|
|
7470
|
+
Object.defineProperty(err, "name", {
|
|
7471
|
+
value: info.name,
|
|
7472
|
+
configurable: true,
|
|
7473
|
+
writable: true
|
|
7474
|
+
});
|
|
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
|
+
});
|
|
7481
|
+
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
7482
|
+
value: fromErrorInfo(info.cause),
|
|
7483
|
+
configurable: true,
|
|
7484
|
+
writable: true
|
|
7485
|
+
});
|
|
7486
|
+
return err;
|
|
7487
|
+
}
|
|
7488
|
+
//#endregion
|
|
7489
|
+
//#region src/executor/worker/sanitize.ts
|
|
7490
|
+
const MAX_DEPTH = 8;
|
|
7491
|
+
function errorToInfo(err) {
|
|
7492
|
+
return toErrorInfo(err);
|
|
7493
|
+
}
|
|
7494
|
+
function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
7495
|
+
if (depth > MAX_DEPTH) return "[Truncated]";
|
|
7496
|
+
if (value === null || value === void 0) return null;
|
|
7497
|
+
if (typeof value === "boolean") return value;
|
|
7498
|
+
if (typeof value === "number") return value;
|
|
7499
|
+
if (typeof value === "string") return value;
|
|
7500
|
+
if (typeof value === "bigint") return value.toString();
|
|
7501
|
+
if (typeof value === "function" || typeof value === "symbol") return null;
|
|
7502
|
+
if (value instanceof Error) return toErrorInfo(value);
|
|
7503
|
+
if (value instanceof Date) return value.toISOString();
|
|
7504
|
+
if (value instanceof Map) {
|
|
7505
|
+
const obj = {};
|
|
7506
|
+
for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
|
|
7507
|
+
return obj;
|
|
7508
|
+
}
|
|
7509
|
+
if (value instanceof Set) {
|
|
7510
|
+
const arr = [];
|
|
7511
|
+
for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
|
|
7512
|
+
return arr;
|
|
7513
|
+
}
|
|
7514
|
+
if (Array.isArray(value)) {
|
|
7515
|
+
if (seen.has(value)) return "[Circular]";
|
|
7516
|
+
seen.add(value);
|
|
7517
|
+
const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
|
|
7518
|
+
seen.delete(value);
|
|
7519
|
+
return result;
|
|
7520
|
+
}
|
|
7521
|
+
if (typeof value === "object") {
|
|
7522
|
+
const proto = Object.getPrototypeOf(value);
|
|
7523
|
+
if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
|
|
7524
|
+
if (seen.has(value)) return "[Circular]";
|
|
7525
|
+
seen.add(value);
|
|
7526
|
+
const result = {};
|
|
7527
|
+
for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
|
|
7528
|
+
seen.delete(value);
|
|
7529
|
+
return result;
|
|
7530
|
+
}
|
|
7531
|
+
return null;
|
|
7532
|
+
}
|
|
7533
|
+
//#endregion
|
|
7534
|
+
//#region src/executor/worker/forwarding-logger.ts
|
|
7535
|
+
function createForwardingLogger(post, level = "info", tags = []) {
|
|
7536
|
+
function safePost(msg) {
|
|
7537
|
+
try {
|
|
7538
|
+
post(msg);
|
|
7539
|
+
} catch {
|
|
7540
|
+
try {
|
|
7541
|
+
process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
|
|
7542
|
+
} catch {}
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
function emit(wireLevel, message, args) {
|
|
7546
|
+
const sanitizedArgs = args.map((a) => sanitizeArg(a));
|
|
7547
|
+
safePost({
|
|
7548
|
+
type: "log",
|
|
7549
|
+
level: wireLevel,
|
|
7550
|
+
message,
|
|
7551
|
+
args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
|
|
7552
|
+
timestamp: Date.now()
|
|
7553
|
+
});
|
|
7554
|
+
}
|
|
7555
|
+
return {
|
|
7556
|
+
level,
|
|
7557
|
+
verbose: (message, ...args) => emit("debug", message, args),
|
|
7558
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
7559
|
+
info: (message, ...args) => emit("info", message, args),
|
|
7560
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
7561
|
+
error: (message, ...args) => emit("error", message, args),
|
|
7562
|
+
errorHandler: ((...data) => {
|
|
7563
|
+
const first = data[0];
|
|
7564
|
+
emit("error", typeof first === "string" ? first : "", data.slice(1));
|
|
7565
|
+
}),
|
|
7566
|
+
child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
|
|
7567
|
+
};
|
|
7568
|
+
}
|
|
5307
7569
|
//#endregion
|
|
5308
|
-
export { selectDecisionModel as A,
|
|
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 };
|
|
5309
7571
|
|
|
5310
|
-
//# sourceMappingURL=
|
|
7572
|
+
//# sourceMappingURL=forwarding-logger-DMqDs9tE.js.map
|