@powerhousedao/reactor 6.2.3-dev.4 → 6.2.3-dev.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-worker-executor-DnU3ZU4H.js → build-worker-executor-Bonu2jUB.js} +30 -8
- package/dist/build-worker-executor-Bonu2jUB.js.map +1 -0
- package/dist/{document-indexer-C5Gsa1B3.js → document-indexer-B3yKal-y.js} +981 -104
- package/dist/document-indexer-B3yKal-y.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-BKDP4JnD.js} +2650 -367
- package/dist/forwarding-logger-BKDP4JnD.js.map +1 -0
- package/dist/index.d.ts +4819 -3185
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4146 -892
- 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-BZPCQK-4.js} +24 -4
- package/dist/projection-shard-manager-BZPCQK-4.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-CefOwokd.js} +30 -17
- package/dist/worker-handle-CefOwokd.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
|
/**
|
|
@@ -915,6 +1232,19 @@ var ModuleNotFoundError = class extends Error {
|
|
|
915
1232
|
return Error.isError(error) && error.name === "ModuleNotFoundError";
|
|
916
1233
|
}
|
|
917
1234
|
};
|
|
1235
|
+
/** The host has the model only as a live module, which workers cannot import. */
|
|
1236
|
+
var ModelNotWorkerImportableError = class extends Error {
|
|
1237
|
+
documentType;
|
|
1238
|
+
constructor(documentType, version) {
|
|
1239
|
+
const versionSuffix = version !== void 0 ? ` version ${version}` : "";
|
|
1240
|
+
super(`Document model ${documentType}${versionSuffix} is registered on the host only as a live module, so executor workers cannot load it. Register it from a { filePath } or { packageName } source.`);
|
|
1241
|
+
this.name = "ModelNotWorkerImportableError";
|
|
1242
|
+
this.documentType = documentType;
|
|
1243
|
+
}
|
|
1244
|
+
static isError(error) {
|
|
1245
|
+
return Error.isError(error) && error.name === "ModelNotWorkerImportableError";
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
918
1248
|
/**
|
|
919
1249
|
* Error thrown when attempting to register a module that already exists.
|
|
920
1250
|
*/
|
|
@@ -978,6 +1308,8 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
978
1308
|
};
|
|
979
1309
|
//#endregion
|
|
980
1310
|
//#region src/executor/types.ts
|
|
1311
|
+
/** 200k operations purge in 1.6 s on local Postgres; jobs time out at 30 s. */
|
|
1312
|
+
const DEFAULT_MAX_PURGE_OPERATIONS = 2e5;
|
|
981
1313
|
/** How long a deferred job waits for its document before it fails. */
|
|
982
1314
|
const DEFAULT_DEFERRED_JOB_TTL_MS = 3e4;
|
|
983
1315
|
/**
|
|
@@ -993,6 +1325,7 @@ const JobExecutorEventTypes = {
|
|
|
993
1325
|
//#endregion
|
|
994
1326
|
//#region src/cache/collection-membership-cache.ts
|
|
995
1327
|
var CollectionMembershipCache = class CollectionMembershipCache {
|
|
1328
|
+
operationIndex;
|
|
996
1329
|
cache = /* @__PURE__ */ new Map();
|
|
997
1330
|
constructor(operationIndex) {
|
|
998
1331
|
this.operationIndex = operationIndex;
|
|
@@ -1190,26 +1523,294 @@ var DocumentMetaCache = class DocumentMetaCache {
|
|
|
1190
1523
|
const docScopeOps = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, void 0, signal);
|
|
1191
1524
|
if (docScopeOps.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1192
1525
|
const createOp = docScopeOps.results[0];
|
|
1526
|
+
if (isPurgeMarker(createOp)) return purgedMeta(createOp.action);
|
|
1193
1527
|
if (createOp.action.type !== "CREATE_DOCUMENT") throw new Error(`Invalid document: first operation must be CREATE_DOCUMENT, found ${createOp.action.type}`);
|
|
1194
1528
|
const createAction = createOp.action;
|
|
1195
1529
|
const documentType = createAction.input.model;
|
|
1196
1530
|
let document = createDocumentFromAction(createAction);
|
|
1197
1531
|
let documentScopeRevision = 0;
|
|
1532
|
+
let purged = false;
|
|
1198
1533
|
for (const op of docScopeOps.results) {
|
|
1199
1534
|
if (targetRevision !== void 0 && op.index > targetRevision) break;
|
|
1200
1535
|
documentScopeRevision = op.index;
|
|
1201
1536
|
if (op.action.type === "UPGRADE_DOCUMENT") {
|
|
1202
1537
|
const upgradeAction = op.action;
|
|
1203
1538
|
document = applyUpgradeDocumentAction$1(document, upgradeAction);
|
|
1204
|
-
} else if (op.action.type === "DELETE_DOCUMENT"
|
|
1539
|
+
} else if (op.action.type === "DELETE_DOCUMENT" || op.action.type === PURGE_DOCUMENT) {
|
|
1540
|
+
document = applyDeleteDocumentAction$1(document, op.action);
|
|
1541
|
+
purged ||= op.action.type === PURGE_DOCUMENT;
|
|
1542
|
+
}
|
|
1205
1543
|
}
|
|
1206
1544
|
return {
|
|
1207
1545
|
state: document.state.document,
|
|
1208
1546
|
documentType,
|
|
1547
|
+
protocolVersions: purged ? purgedProtocolVersions() : createAction.input.protocolVersions,
|
|
1209
1548
|
documentScopeRevision: documentScopeRevision + 1
|
|
1210
1549
|
};
|
|
1211
1550
|
}
|
|
1212
1551
|
};
|
|
1552
|
+
/** A lone marker has no CREATE_DOCUMENT; the marker describes the stream. */
|
|
1553
|
+
function purgedMeta(action) {
|
|
1554
|
+
return {
|
|
1555
|
+
state: createDocumentState({
|
|
1556
|
+
isDeleted: true,
|
|
1557
|
+
deletedAtUtcIso: action.input.purgedAtUtcIso
|
|
1558
|
+
}),
|
|
1559
|
+
documentType: action.input.documentType,
|
|
1560
|
+
protocolVersions: purgedProtocolVersions(),
|
|
1561
|
+
documentScopeRevision: 1
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
//#endregion
|
|
1565
|
+
//#region src/catch-up/settled-watermark.ts
|
|
1566
|
+
const MAX_PENDING_PROBES = 64;
|
|
1567
|
+
/** pg_sequence_last_value needs 10; pg_current_* replaced txid_* in 13. */
|
|
1568
|
+
function snapshotFunctionsFor(serverVersionNum) {
|
|
1569
|
+
if (serverVersionNum >= 13e4) return {
|
|
1570
|
+
snapshot: "pg_current_snapshot",
|
|
1571
|
+
currentXid: "pg_current_xact_id",
|
|
1572
|
+
xidIfAssigned: "pg_current_xact_id_if_assigned"
|
|
1573
|
+
};
|
|
1574
|
+
if (serverVersionNum >= 1e5) return {
|
|
1575
|
+
snapshot: "txid_current_snapshot",
|
|
1576
|
+
currentXid: "txid_current",
|
|
1577
|
+
xidIfAssigned: "txid_current_if_assigned"
|
|
1578
|
+
};
|
|
1579
|
+
throw new Error(`PostgreSQL server_version_num ${serverVersionNum} is not supported: read-side catch-up needs 10 or later`);
|
|
1580
|
+
}
|
|
1581
|
+
/** Parses the `xmin:xmax:xip,...` text form of a snapshot. */
|
|
1582
|
+
function parseSnapshot(text) {
|
|
1583
|
+
const parts = text.split(":");
|
|
1584
|
+
if (parts.length !== 3) throw new Error(`Malformed snapshot: ${text}`);
|
|
1585
|
+
const [xmin, xmax, xip] = parts;
|
|
1586
|
+
return {
|
|
1587
|
+
xmin: BigInt(xmin),
|
|
1588
|
+
xmax: BigInt(xmax),
|
|
1589
|
+
xip: xip === "" ? [] : xip.split(",").map((xid) => BigInt(xid))
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async function readSnapshotFunctions(db) {
|
|
1593
|
+
const result = await sql`select current_setting('server_version_num') as version`.execute(db);
|
|
1594
|
+
return snapshotFunctionsFor(Number(result.rows[0].version));
|
|
1595
|
+
}
|
|
1596
|
+
/** A probe settles once an xmin passes the xid it took after its head. */
|
|
1597
|
+
var ProbeSettler = class {
|
|
1598
|
+
settled = 0;
|
|
1599
|
+
lastHead = 0;
|
|
1600
|
+
covered = 0;
|
|
1601
|
+
lastXip = [];
|
|
1602
|
+
pending = [];
|
|
1603
|
+
get settledThrough() {
|
|
1604
|
+
return this.settled;
|
|
1605
|
+
}
|
|
1606
|
+
get head() {
|
|
1607
|
+
return this.lastHead;
|
|
1608
|
+
}
|
|
1609
|
+
/** The highest head a settled or pending probe covers. */
|
|
1610
|
+
get probedHead() {
|
|
1611
|
+
return this.covered;
|
|
1612
|
+
}
|
|
1613
|
+
observe(head, xid, snapshot, nowUtcMs) {
|
|
1614
|
+
this.lastHead = Math.max(this.lastHead, head);
|
|
1615
|
+
this.lastXip = snapshot.xip;
|
|
1616
|
+
if (xid !== null && head > this.covered) {
|
|
1617
|
+
this.covered = head;
|
|
1618
|
+
this.pending.push({
|
|
1619
|
+
head,
|
|
1620
|
+
xid,
|
|
1621
|
+
takenAtUtcMs: nowUtcMs
|
|
1622
|
+
});
|
|
1623
|
+
if (this.pending.length > MAX_PENDING_PROBES) this.pending.shift();
|
|
1624
|
+
}
|
|
1625
|
+
const stillPending = [];
|
|
1626
|
+
for (const probe of this.pending) if (snapshot.xmin > probe.xid) this.settled = Math.max(this.settled, probe.head);
|
|
1627
|
+
else stillPending.push(probe);
|
|
1628
|
+
this.pending = stillPending.filter((probe) => probe.head > this.settled);
|
|
1629
|
+
return this.settled;
|
|
1630
|
+
}
|
|
1631
|
+
/** Xids an unsettled probe still waits on. */
|
|
1632
|
+
waitingOn() {
|
|
1633
|
+
if (this.pending.length === 0) return [];
|
|
1634
|
+
const xid = this.pending[0].xid;
|
|
1635
|
+
return this.lastXip.filter((open) => open < xid).map((open) => open.toString());
|
|
1636
|
+
}
|
|
1637
|
+
stalledSinceUtcMs() {
|
|
1638
|
+
return this.pending[0]?.takenAtUtcMs;
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
function operationIndexTableName(db) {
|
|
1642
|
+
const from = db.selectFrom("operation_index_operations").selectAll().toOperationNode().from?.froms[0];
|
|
1643
|
+
const schema = from !== void 0 && TableNode.is(from) ? from.table.schema?.name : void 0;
|
|
1644
|
+
const quote = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
1645
|
+
return schema === void 0 ? quote("operation_index_operations") : `${quote(schema)}.${quote("operation_index_operations")}`;
|
|
1646
|
+
}
|
|
1647
|
+
/** Reads the head, takes and commits an xid, then reads a snapshot, in order. */
|
|
1648
|
+
function createKyselyWatermarkProbe(db) {
|
|
1649
|
+
let resolved = null;
|
|
1650
|
+
const resolve = async () => {
|
|
1651
|
+
const fns = await readSnapshotFunctions(db);
|
|
1652
|
+
const table = operationIndexTableName(db);
|
|
1653
|
+
const sequence = (await sql`select pg_get_serial_sequence(${table}, 'ordinal') as sequence`.execute(db)).rows[0]?.sequence;
|
|
1654
|
+
if (!sequence) throw new Error(`No ordinal sequence found for ${table}`);
|
|
1655
|
+
return {
|
|
1656
|
+
fns,
|
|
1657
|
+
sequence
|
|
1658
|
+
};
|
|
1659
|
+
};
|
|
1660
|
+
return async (probedHead) => {
|
|
1661
|
+
resolved ??= resolve().catch((error) => {
|
|
1662
|
+
resolved = null;
|
|
1663
|
+
throw error;
|
|
1664
|
+
});
|
|
1665
|
+
const { fns, sequence } = await resolved;
|
|
1666
|
+
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];
|
|
1667
|
+
const head = Number(headRow.head);
|
|
1668
|
+
let xid = null;
|
|
1669
|
+
if (headRow.outside_write && head > probedHead) {
|
|
1670
|
+
const xidResult = await sql`select ${sql.raw(fns.currentXid)}()::text as xid`.execute(db);
|
|
1671
|
+
xid = BigInt(xidResult.rows[0].xid);
|
|
1672
|
+
}
|
|
1673
|
+
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];
|
|
1674
|
+
return {
|
|
1675
|
+
head,
|
|
1676
|
+
xid,
|
|
1677
|
+
snapshot: parseSnapshot(row.snapshot),
|
|
1678
|
+
outsideWrite: headRow.outside_write && row.outside_write
|
|
1679
|
+
};
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
/** Knows when an ordinal gap is final by asking which transactions are open. */
|
|
1683
|
+
var SettledWatermark = class {
|
|
1684
|
+
probe;
|
|
1685
|
+
logger;
|
|
1686
|
+
now;
|
|
1687
|
+
settler = new ProbeSettler();
|
|
1688
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1689
|
+
current;
|
|
1690
|
+
next;
|
|
1691
|
+
warnedInsideWrite = false;
|
|
1692
|
+
constructor(probe, logger, now = Date.now) {
|
|
1693
|
+
this.probe = probe;
|
|
1694
|
+
this.logger = logger;
|
|
1695
|
+
this.now = now;
|
|
1696
|
+
}
|
|
1697
|
+
get settledThrough() {
|
|
1698
|
+
return this.settler.settledThrough;
|
|
1699
|
+
}
|
|
1700
|
+
refresh(signal) {
|
|
1701
|
+
signal?.throwIfAborted();
|
|
1702
|
+
if (this.next !== void 0) return this.next;
|
|
1703
|
+
if (this.current === void 0) return this.start();
|
|
1704
|
+
const next = this.current.catch(() => this.settledThrough).then(() => {
|
|
1705
|
+
this.next = void 0;
|
|
1706
|
+
return this.start();
|
|
1707
|
+
});
|
|
1708
|
+
this.next = next;
|
|
1709
|
+
return next;
|
|
1710
|
+
}
|
|
1711
|
+
onAdvance(listener) {
|
|
1712
|
+
this.listeners.add(listener);
|
|
1713
|
+
return () => {
|
|
1714
|
+
this.listeners.delete(listener);
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
status() {
|
|
1718
|
+
const stalledSinceUtcMs = this.settler.head > this.settledThrough ? this.settler.stalledSinceUtcMs() : void 0;
|
|
1719
|
+
return {
|
|
1720
|
+
head: this.settler.head,
|
|
1721
|
+
settledThrough: this.settledThrough,
|
|
1722
|
+
waitingOn: this.settler.waitingOn(),
|
|
1723
|
+
...stalledSinceUtcMs !== void 0 ? { stalledSinceUtcMs } : {}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
start() {
|
|
1727
|
+
const run = this.probeOnce().finally(() => {
|
|
1728
|
+
if (this.current === run) this.current = void 0;
|
|
1729
|
+
});
|
|
1730
|
+
this.current = run;
|
|
1731
|
+
return run;
|
|
1732
|
+
}
|
|
1733
|
+
async probeOnce() {
|
|
1734
|
+
const reading = await this.probe(this.settler.probedHead);
|
|
1735
|
+
if (!reading.outsideWrite) {
|
|
1736
|
+
if (!this.warnedInsideWrite) {
|
|
1737
|
+
this.warnedInsideWrite = true;
|
|
1738
|
+
this.logger.warn("settled watermark probe ran inside another session's write transaction; discarded");
|
|
1739
|
+
}
|
|
1740
|
+
return this.settledThrough;
|
|
1741
|
+
}
|
|
1742
|
+
const before = this.settledThrough;
|
|
1743
|
+
const after = this.settler.observe(reading.head, reading.xid, reading.snapshot, this.now());
|
|
1744
|
+
if (after > before) for (const listener of this.listeners) try {
|
|
1745
|
+
listener(after);
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
this.logger.error("settled watermark listener failed: @error", error);
|
|
1748
|
+
}
|
|
1749
|
+
return after;
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
/** Sessions holding these xids open; empty without pg_stat_activity. */
|
|
1753
|
+
async function describeWaitingSessions(db, xids) {
|
|
1754
|
+
if (xids.length === 0) return [];
|
|
1755
|
+
const epochless = xids.map((xid) => (BigInt(xid) % 2n ** 32n).toString());
|
|
1756
|
+
try {
|
|
1757
|
+
return (await sql`select pid, application_name, state, xact_start, backend_xid::text as xid
|
|
1758
|
+
from pg_stat_activity
|
|
1759
|
+
where backend_xid::text in (${sql.join(epochless)})`.execute(db)).rows.map((row) => ({
|
|
1760
|
+
pid: Number(row.pid),
|
|
1761
|
+
applicationName: row.application_name ?? "",
|
|
1762
|
+
state: row.state ?? "",
|
|
1763
|
+
xactStart: row.xact_start === null ? null : new Date(row.xact_start).toISOString(),
|
|
1764
|
+
xid: row.xid
|
|
1765
|
+
}));
|
|
1766
|
+
} catch {
|
|
1767
|
+
return [];
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/storage/kysely/document-purges.ts
|
|
1772
|
+
/** Class key of every purge lock; the stream locks use the single-key space. */
|
|
1773
|
+
const PURGE_NS = 1347571013;
|
|
1774
|
+
/** Ids share this many lock keys, so a job's lock count stays bounded. */
|
|
1775
|
+
const PURGE_LOCK_BUCKETS = 1024;
|
|
1776
|
+
async function acquirePurgeLocks(trx, ids, mode) {
|
|
1777
|
+
const unique = [...new Set(ids)];
|
|
1778
|
+
if (unique.length === 0) return;
|
|
1779
|
+
const lock = sql.raw(mode === "shared" ? "pg_advisory_xact_lock_shared" : "pg_advisory_xact_lock");
|
|
1780
|
+
await sql`
|
|
1781
|
+
with keys as materialized (
|
|
1782
|
+
select distinct hashtext(id) & ${sql.lit(1023)} as key
|
|
1783
|
+
from unnest(${unique}::text[]) as t(id)
|
|
1784
|
+
order by key
|
|
1785
|
+
)
|
|
1786
|
+
select ${lock}(${sql.lit(PURGE_NS)}, key) from keys order by key
|
|
1787
|
+
`.execute(trx);
|
|
1788
|
+
}
|
|
1789
|
+
/** The ids among `ids` that have a tombstone, read in the handle's schema. */
|
|
1790
|
+
async function findPurged(db, ids) {
|
|
1791
|
+
const unique = [...new Set(ids)];
|
|
1792
|
+
if (unique.length === 0) return /* @__PURE__ */ new Set();
|
|
1793
|
+
const rows = await db.selectFrom("document_purges").select("documentId").where(sql`"documentId" = any(${unique}::text[])`).execute();
|
|
1794
|
+
return new Set(rows.map((row) => row.documentId));
|
|
1795
|
+
}
|
|
1796
|
+
/** Every tombstoned id, read in the handle's schema. */
|
|
1797
|
+
async function listPurged(db) {
|
|
1798
|
+
return (await db.selectFrom("document_purges").select("documentId").orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1799
|
+
}
|
|
1800
|
+
/** Bind slots per statement: 16-bit on the wire, read signed in-process. */
|
|
1801
|
+
const MAX_BIND_PARAMETERS = 32767;
|
|
1802
|
+
/** Splits rows into runs that each bind at most MAX_BIND_PARAMETERS. */
|
|
1803
|
+
function chunkRows(rows) {
|
|
1804
|
+
if (rows.length === 0) return [];
|
|
1805
|
+
const columnsPerRow = Math.max(1, Object.keys(rows[0]).length);
|
|
1806
|
+
const perChunk = Math.max(1, Math.floor(MAX_BIND_PARAMETERS / columnsPerRow));
|
|
1807
|
+
if (rows.length <= perChunk) return [rows];
|
|
1808
|
+
const chunks = [];
|
|
1809
|
+
for (let i = 0; i < rows.length; i += perChunk) chunks.push(rows.slice(i, i + perChunk));
|
|
1810
|
+
return chunks;
|
|
1811
|
+
}
|
|
1812
|
+
/** Per database: the function that assigns the transaction's xid. */
|
|
1813
|
+
const xidFunctions = /* @__PURE__ */ new WeakMap();
|
|
1213
1814
|
var KyselyOperationIndexTxn = class {
|
|
1214
1815
|
collections = [];
|
|
1215
1816
|
collectionMemberships = [];
|
|
@@ -1275,16 +1876,20 @@ var KyselyOperationIndexTxn = class {
|
|
|
1275
1876
|
}
|
|
1276
1877
|
};
|
|
1277
1878
|
var KyselyOperationIndex = class KyselyOperationIndex {
|
|
1879
|
+
db;
|
|
1278
1880
|
trx;
|
|
1881
|
+
liveIds;
|
|
1279
1882
|
constructor(db) {
|
|
1280
1883
|
this.db = db;
|
|
1281
1884
|
}
|
|
1282
1885
|
get queryExecutor() {
|
|
1283
1886
|
return this.trx ?? this.db;
|
|
1284
1887
|
}
|
|
1285
|
-
|
|
1888
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
1889
|
+
withTransaction(trx, liveIds) {
|
|
1286
1890
|
const instance = new KyselyOperationIndex(this.db);
|
|
1287
1891
|
instance.trx = trx;
|
|
1892
|
+
instance.liveIds = liveIds;
|
|
1288
1893
|
return instance;
|
|
1289
1894
|
}
|
|
1290
1895
|
start() {
|
|
@@ -1331,10 +1936,12 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1331
1936
|
leftOrdinal: null
|
|
1332
1937
|
}));
|
|
1333
1938
|
for (const collectionId of collections) kyselyTxn.recordMembershipInvalidation(collectionId);
|
|
1334
|
-
await trx.insertInto("document_collections").values(
|
|
1939
|
+
for (const chunk of chunkRows(collectionRows)) await trx.insertInto("document_collections").values(chunk).onConflict((oc) => oc.doNothing()).execute();
|
|
1335
1940
|
}
|
|
1336
1941
|
let operationOrdinals = [];
|
|
1337
1942
|
if (operations.length > 0) {
|
|
1943
|
+
await this.refusePurgedOperations(trx, operations);
|
|
1944
|
+
await this.assignXid(trx);
|
|
1338
1945
|
const operationRows = operations.map((op) => ({
|
|
1339
1946
|
opId: op.id || "",
|
|
1340
1947
|
documentId: op.documentId,
|
|
@@ -1349,7 +1956,10 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1349
1956
|
deniedReason: op.deniedReason ?? null,
|
|
1350
1957
|
sourceRemote: op.sourceRemote
|
|
1351
1958
|
}));
|
|
1352
|
-
|
|
1959
|
+
for (const chunk of chunkRows(operationRows)) {
|
|
1960
|
+
const insertedOps = await trx.insertInto("operation_index_operations").values(chunk).returning("ordinal").execute();
|
|
1961
|
+
operationOrdinals = operationOrdinals.concat(insertedOps.map((row) => row.ordinal));
|
|
1962
|
+
}
|
|
1353
1963
|
}
|
|
1354
1964
|
if (memberships.length > 0) for (const m of memberships) {
|
|
1355
1965
|
const ordinal = operationOrdinals[m.operationIndex];
|
|
@@ -1382,10 +1992,44 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1382
1992
|
}
|
|
1383
1993
|
return operationOrdinals;
|
|
1384
1994
|
}
|
|
1995
|
+
async getCollectionsInRange(after, through, among, signal) {
|
|
1996
|
+
signal?.throwIfAborted();
|
|
1997
|
+
if (through <= after || among?.length === 0) return [];
|
|
1998
|
+
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);
|
|
1999
|
+
if (among !== void 0) query = query.where("dc.collectionId", "in", [...among]);
|
|
2000
|
+
return (await query.execute()).map((row) => row.collectionId);
|
|
2001
|
+
}
|
|
2002
|
+
async getOrdinalsInRange(after, through, limit, signal) {
|
|
2003
|
+
signal?.throwIfAborted();
|
|
2004
|
+
if (through <= after || limit <= 0) return [];
|
|
2005
|
+
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));
|
|
2006
|
+
}
|
|
2007
|
+
async getByOrdinals(ordinals, signal) {
|
|
2008
|
+
signal?.throwIfAborted();
|
|
2009
|
+
if (ordinals.length === 0) return [];
|
|
2010
|
+
const results = [];
|
|
2011
|
+
for (let i = 0; i < ordinals.length; i += MAX_BIND_PARAMETERS) {
|
|
2012
|
+
const chunk = ordinals.slice(i, i + MAX_BIND_PARAMETERS);
|
|
2013
|
+
const rows = await this.queryExecutor.selectFrom("operation_index_operations").selectAll().where("ordinal", "in", chunk).orderBy("ordinal", "asc").execute();
|
|
2014
|
+
for (const row of rows) results.push(this.rowToOperationWithContext(row));
|
|
2015
|
+
}
|
|
2016
|
+
results.sort((a, b) => a.context.ordinal - b.context.ordinal);
|
|
2017
|
+
return results;
|
|
2018
|
+
}
|
|
2019
|
+
async getStreamAfter(stream, after, signal, limit) {
|
|
2020
|
+
signal?.throwIfAborted();
|
|
2021
|
+
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));
|
|
2022
|
+
}
|
|
1385
2023
|
async getGroupReferencers(groupId, signal) {
|
|
1386
2024
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1387
2025
|
return (await this.queryExecutor.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
1388
2026
|
}
|
|
2027
|
+
async getOrdinalsByOpIds(documentId, scope, branch, opIds, signal) {
|
|
2028
|
+
signal?.throwIfAborted();
|
|
2029
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Map();
|
|
2030
|
+
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();
|
|
2031
|
+
return new Map(rows.map((row) => [row.opId, Number(row.ordinal)]));
|
|
2032
|
+
}
|
|
1389
2033
|
async find(collectionId, cursor, view, paging, signal) {
|
|
1390
2034
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1391
2035
|
const outerCursor = cursor ?? -1;
|
|
@@ -1399,6 +2043,7 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1399
2043
|
if (view?.branch) qb = qb.where("oi.branch", "=", view.branch);
|
|
1400
2044
|
if (view?.scopes && view.scopes.length > 0) qb = qb.where("oi.scope", "in", view.scopes);
|
|
1401
2045
|
if (view?.excludeSourceRemote) qb = qb.where("oi.sourceRemote", "!=", view.excludeSourceRemote);
|
|
2046
|
+
if (view?.throughOrdinal !== void 0) qb = qb.where("oi.ordinal", "<=", view.throughOrdinal).where("dc.joinedOrdinal", "<=", BigInt(view.throughOrdinal));
|
|
1402
2047
|
return qb;
|
|
1403
2048
|
};
|
|
1404
2049
|
const rows = await buildBranch("joiner").unionAll(buildBranch("newOps")).orderBy("ordinal", "asc").limit(limit + 1).execute();
|
|
@@ -1487,6 +2132,23 @@ var KyselyOperationIndex = class KyselyOperationIndex {
|
|
|
1487
2132
|
}, signal) : void 0
|
|
1488
2133
|
};
|
|
1489
2134
|
}
|
|
2135
|
+
/** Backstop to the store's refusal: only a marker indexes a purged id. */
|
|
2136
|
+
async refusePurgedOperations(trx, operations) {
|
|
2137
|
+
const ids = new Set(operations.filter((operation) => !isPurgeMarker(operation)).map((operation) => operation.documentId).filter((id) => !this.liveIds?.has(id)));
|
|
2138
|
+
const purged = await findPurged(trx, ids);
|
|
2139
|
+
for (const id of ids) if (purged.has(id)) throw new DocumentPurgedError(id);
|
|
2140
|
+
}
|
|
2141
|
+
/** Takes the xid before the first ordinal, as the settled watermark needs. */
|
|
2142
|
+
async assignXid(trx) {
|
|
2143
|
+
let fn = xidFunctions.get(this.db);
|
|
2144
|
+
if (fn === void 0) {
|
|
2145
|
+
fn = readSnapshotFunctions(trx).then((fns) => fns.currentXid);
|
|
2146
|
+
xidFunctions.set(this.db, fn);
|
|
2147
|
+
fn.catch(() => xidFunctions.delete(this.db));
|
|
2148
|
+
}
|
|
2149
|
+
const name = await fn;
|
|
2150
|
+
await sql`select ${sql.raw(name)}()`.execute(trx);
|
|
2151
|
+
}
|
|
1490
2152
|
rowToOperationWithContext(row) {
|
|
1491
2153
|
return {
|
|
1492
2154
|
operation: {
|
|
@@ -1605,21 +2267,6 @@ var RingBuffer = class {
|
|
|
1605
2267
|
}
|
|
1606
2268
|
};
|
|
1607
2269
|
//#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
2270
|
//#region src/cache/kysely-write-cache.ts
|
|
1624
2271
|
/**
|
|
1625
2272
|
* The last operation index a keyframe's document reflects for the scope. A
|
|
@@ -1654,42 +2301,6 @@ function copyDocument(document) {
|
|
|
1654
2301
|
operations: { ...document.operations }
|
|
1655
2302
|
};
|
|
1656
2303
|
}
|
|
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
2304
|
var KyselyWriteCache = class KyselyWriteCache {
|
|
1694
2305
|
streams;
|
|
1695
2306
|
lruTracker;
|
|
@@ -1697,6 +2308,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1697
2308
|
operationStore;
|
|
1698
2309
|
registry;
|
|
1699
2310
|
config;
|
|
2311
|
+
reads = /* @__PURE__ */ new Map();
|
|
1700
2312
|
constructor(keyframeStore, operationStore, registry, config) {
|
|
1701
2313
|
this.keyframeStore = keyframeStore;
|
|
1702
2314
|
this.operationStore = operationStore;
|
|
@@ -1713,6 +2325,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1713
2325
|
const scoped = new KyselyWriteCache(keyframeStore, operationStore, this.registry, this.config);
|
|
1714
2326
|
scoped.streams = this.streams;
|
|
1715
2327
|
scoped.lruTracker = this.lruTracker;
|
|
2328
|
+
scoped.reads = this.reads;
|
|
1716
2329
|
return scoped;
|
|
1717
2330
|
}
|
|
1718
2331
|
/**
|
|
@@ -1746,11 +2359,27 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1746
2359
|
* @returns The document at the target revision
|
|
1747
2360
|
* @throws {Error} "Operation aborted" if signal is aborted
|
|
1748
2361
|
* @throws {ModuleNotFoundError} If document type not registered in registry
|
|
2362
|
+
* @throws {DocumentPurgedError} If the stream's only row is a purge marker
|
|
1749
2363
|
* @throws {Error} "Failed to rebuild document" if operation store fails
|
|
1750
2364
|
* @throws {Error} If reducer throws during operation application
|
|
1751
2365
|
* @throws {Error} If document serialization fails
|
|
1752
2366
|
*/
|
|
1753
2367
|
async getState(documentId, scope, branch, targetRevision, signal) {
|
|
2368
|
+
const read = { evicted: false };
|
|
2369
|
+
let pending = this.reads.get(documentId);
|
|
2370
|
+
if (!pending) {
|
|
2371
|
+
pending = /* @__PURE__ */ new Set();
|
|
2372
|
+
this.reads.set(documentId, pending);
|
|
2373
|
+
}
|
|
2374
|
+
pending.add(read);
|
|
2375
|
+
try {
|
|
2376
|
+
return await this.readState(documentId, scope, branch, targetRevision, signal, read);
|
|
2377
|
+
} finally {
|
|
2378
|
+
pending.delete(read);
|
|
2379
|
+
if (pending.size === 0) this.reads.delete(documentId);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
async readState(documentId, scope, branch, targetRevision, signal, read) {
|
|
1754
2383
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1755
2384
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
1756
2385
|
const stream = this.streams.get(streamKey);
|
|
@@ -1758,13 +2387,13 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1758
2387
|
const snapshots = stream.ringBuffer.getAll();
|
|
1759
2388
|
if (targetRevision === void 0) {
|
|
1760
2389
|
const newest = highestRevision(snapshots);
|
|
1761
|
-
if (newest?.position ===
|
|
2390
|
+
if (newest?.position === "head") {
|
|
1762
2391
|
this.lruTracker.touch(streamKey);
|
|
1763
2392
|
return copyDocument(newest.document);
|
|
1764
2393
|
}
|
|
1765
2394
|
if (newest) {
|
|
1766
2395
|
const document = await this.warmMissRebuild(newest.document, newest.revision, documentId, scope, branch, void 0, signal);
|
|
1767
|
-
this.
|
|
2396
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, (document.header.revision[scope] ?? 0) - 1, document, "head");
|
|
1768
2397
|
this.lruTracker.touch(streamKey);
|
|
1769
2398
|
return document;
|
|
1770
2399
|
}
|
|
@@ -1777,7 +2406,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1777
2406
|
const newestOlder = this.findNearestOlderSnapshot(snapshots, targetRevision);
|
|
1778
2407
|
if (newestOlder) {
|
|
1779
2408
|
const document = await this.warmMissRebuild(newestOlder.document, newestOlder.revision, documentId, scope, branch, targetRevision, signal);
|
|
1780
|
-
this.
|
|
2409
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, targetRevision, document, "historical");
|
|
1781
2410
|
this.lruTracker.touch(streamKey);
|
|
1782
2411
|
return document;
|
|
1783
2412
|
}
|
|
@@ -1785,7 +2414,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1785
2414
|
}
|
|
1786
2415
|
const document = await this.coldMissRebuild(documentId, scope, branch, targetRevision, signal);
|
|
1787
2416
|
const revision = targetRevision ?? (document.header.revision[scope] ?? 0) - 1;
|
|
1788
|
-
this.
|
|
2417
|
+
this.storeUnlessEvicted(read, documentId, scope, branch, revision, document, targetRevision === void 0 ? "head" : "historical");
|
|
1789
2418
|
return document;
|
|
1790
2419
|
}
|
|
1791
2420
|
/**
|
|
@@ -1820,7 +2449,10 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1820
2449
|
if (run.length === 0) return;
|
|
1821
2450
|
for (const entry of run.slice(0, -1)) this.persistKeyframe(documentId, scope, branch, entry.revision, entry.document);
|
|
1822
2451
|
const head = run[run.length - 1];
|
|
1823
|
-
this.store(documentId, scope, branch, head.revision, head.document,
|
|
2452
|
+
this.store(documentId, scope, branch, head.revision, head.document, "head");
|
|
2453
|
+
}
|
|
2454
|
+
storeUnlessEvicted(read, documentId, scope, branch, revision, document, position) {
|
|
2455
|
+
if (!read.evicted) this.store(documentId, scope, branch, revision, document, position);
|
|
1824
2456
|
}
|
|
1825
2457
|
store(documentId, scope, branch, revision, document, position) {
|
|
1826
2458
|
const streamKey = this.makeStreamKey(documentId, scope, branch);
|
|
@@ -1862,6 +2494,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1862
2494
|
* @returns The number of streams evicted
|
|
1863
2495
|
*/
|
|
1864
2496
|
invalidate(documentId, scope, branch) {
|
|
2497
|
+
for (const read of this.reads.get(documentId) ?? []) read.evicted = true;
|
|
1865
2498
|
let evicted = 0;
|
|
1866
2499
|
if (scope === void 0 && branch === void 0) {
|
|
1867
2500
|
for (const [key] of this.streams.entries()) if (key.startsWith(`${documentId}:`)) {
|
|
@@ -1890,6 +2523,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1890
2523
|
* Resets LRU tracking state. This operation always succeeds.
|
|
1891
2524
|
*/
|
|
1892
2525
|
clear() {
|
|
2526
|
+
for (const pending of this.reads.values()) for (const read of pending) read.evicted = true;
|
|
1893
2527
|
this.streams.clear();
|
|
1894
2528
|
this.lruTracker.clear();
|
|
1895
2529
|
}
|
|
@@ -1924,9 +2558,9 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1924
2558
|
* the last replayed operation are applied at the end. Creation-time 0->N seed
|
|
1925
2559
|
* upgrades carry the initial state, so they still apply immediately.
|
|
1926
2560
|
*/
|
|
1927
|
-
async coldMissRebuild(documentId, scope, branch, targetRevision, signal) {
|
|
2561
|
+
async coldMissRebuild(documentId, scope, branch, targetRevision, signal, fromKeyframe = true) {
|
|
1928
2562
|
const effectiveTargetRevision = targetRevision || Number.MAX_SAFE_INTEGER;
|
|
1929
|
-
const keyframe = await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal);
|
|
2563
|
+
const keyframe = fromKeyframe ? await this.findNearestKeyframe(documentId, scope, branch, effectiveTargetRevision, signal) : void 0;
|
|
1930
2564
|
const documentScopeBound = scope === "document" ? targetRevision : void 0;
|
|
1931
2565
|
let document;
|
|
1932
2566
|
let startRevision;
|
|
@@ -1969,7 +2603,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1969
2603
|
subsequentDeletes: []
|
|
1970
2604
|
});
|
|
1971
2605
|
}
|
|
1972
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2606
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
1973
2607
|
applyDeleteDocumentAction(document, operation.action);
|
|
1974
2608
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
1975
2609
|
}
|
|
@@ -1982,6 +2616,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
1982
2616
|
}, signal);
|
|
1983
2617
|
if (createOpResult.results.length === 0) throw new DocumentNotFoundError(documentId);
|
|
1984
2618
|
const createOp = createOpResult.results[0];
|
|
2619
|
+
if (isPurgeMarker(createOp)) throw new DocumentPurgedError(documentId);
|
|
1985
2620
|
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
2621
|
const documentCreateAction = createOp.action;
|
|
1987
2622
|
documentType = documentCreateAction.input.model;
|
|
@@ -2021,15 +2656,15 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2021
2656
|
});
|
|
2022
2657
|
} else document = applyUpgradeDocumentAction(document, upgradeAction, void 0);
|
|
2023
2658
|
docModule = this.registry.getModule(documentType, normalizeDocumentModelVersion(toVersion));
|
|
2024
|
-
} else if (operation.action.type === "DELETE_DOCUMENT") {
|
|
2659
|
+
} else if (operation.action.type === "DELETE_DOCUMENT" || operation.action.type === PURGE_DOCUMENT) {
|
|
2025
2660
|
applyDeleteDocumentAction(document, operation.action);
|
|
2026
2661
|
for (const pending of pendingUpgrades) pending.subsequentDeletes.push(operation.action);
|
|
2027
2662
|
} else {
|
|
2028
2663
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2029
|
-
document = docModule.reducer(document, operation.action, void 0, {
|
|
2664
|
+
document = withProtocolVersions(docModule.reducer(document, operation.action, void 0, {
|
|
2030
2665
|
skip: operation.skip,
|
|
2031
2666
|
protocolVersion
|
|
2032
|
-
});
|
|
2667
|
+
}), document.header.protocolVersions);
|
|
2033
2668
|
}
|
|
2034
2669
|
}
|
|
2035
2670
|
}
|
|
@@ -2063,6 +2698,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2063
2698
|
let cursor = void 0;
|
|
2064
2699
|
const pageSize = 100;
|
|
2065
2700
|
let hasMorePages;
|
|
2701
|
+
let needsFullReplay = false;
|
|
2066
2702
|
do {
|
|
2067
2703
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2068
2704
|
const paging = {
|
|
@@ -2073,26 +2709,31 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2073
2709
|
const result = await this.operationStore.getSince(documentId, scope, branch, startRevision, void 0, paging, signal);
|
|
2074
2710
|
for (const operation of result.results) {
|
|
2075
2711
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2712
|
+
if (keyframe && operation.skip > 0) {
|
|
2713
|
+
needsFullReplay = true;
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2076
2716
|
const moduleVersion = this.resolveModuleVersionForOp(operation.index, operation.timestampUtcMs, scope, validatedUpgrades, finalVersion);
|
|
2077
2717
|
document = this.applyPendingUpgrades(document, pendingUpgrades, moduleVersion ?? Number.MAX_SAFE_INTEGER);
|
|
2078
2718
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2079
2719
|
else {
|
|
2080
2720
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2081
|
-
document = getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2721
|
+
document = withProtocolVersions(getModuleCached(moduleVersion).reducer(document, operation.action, void 0, {
|
|
2082
2722
|
skip: operation.skip,
|
|
2083
2723
|
protocolVersion,
|
|
2084
2724
|
replayOptions: { operation },
|
|
2085
2725
|
skipIndexValidation: true
|
|
2086
|
-
});
|
|
2726
|
+
}), document.header.protocolVersions);
|
|
2087
2727
|
}
|
|
2088
2728
|
}
|
|
2089
2729
|
const reachedTarget = targetRevision !== void 0 && result.results.some((op) => op.index >= targetRevision);
|
|
2090
|
-
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
2730
|
+
hasMorePages = Boolean(result.nextCursor) && !reachedTarget && !needsFullReplay;
|
|
2091
2731
|
if (hasMorePages) cursor = result.nextCursor;
|
|
2092
2732
|
} catch (err) {
|
|
2093
2733
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2094
2734
|
}
|
|
2095
2735
|
} while (hasMorePages);
|
|
2736
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2096
2737
|
document = this.applyTailPendingUpgrades(document, pendingUpgrades, scope, targetRevision);
|
|
2097
2738
|
document = await this.stampRevisions(document, documentId, scope, branch, targetRevision, signal);
|
|
2098
2739
|
if (pendingUpgrades.length > 0) {
|
|
@@ -2199,26 +2840,32 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2199
2840
|
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
2841
|
const module = this.registry.getModule(documentType, extractModuleVersion(baseDocument));
|
|
2201
2842
|
let document = copyDocument(baseDocument);
|
|
2843
|
+
let needsFullReplay = false;
|
|
2202
2844
|
try {
|
|
2203
2845
|
const pagedResults = await this.operationStore.getSince(documentId, scope, branch, baseRevision, void 0, void 0, signal);
|
|
2204
2846
|
for (const operation of pagedResults.results) {
|
|
2205
2847
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
2206
2848
|
if (targetRevision !== void 0 && operation.index > targetRevision) break;
|
|
2849
|
+
if (operation.skip > 0) {
|
|
2850
|
+
needsFullReplay = true;
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2207
2853
|
if (isDenied(operation)) document = appendWithoutApplying(document, operation, scope);
|
|
2208
2854
|
else {
|
|
2209
2855
|
const protocolVersion = baseReducerVersion(document.header);
|
|
2210
|
-
document = module.reducer(document, operation.action, void 0, {
|
|
2856
|
+
document = withProtocolVersions(module.reducer(document, operation.action, void 0, {
|
|
2211
2857
|
skip: operation.skip,
|
|
2212
2858
|
protocolVersion,
|
|
2213
2859
|
replayOptions: { operation },
|
|
2214
2860
|
skipIndexValidation: true
|
|
2215
|
-
});
|
|
2861
|
+
}), document.header.protocolVersions);
|
|
2216
2862
|
}
|
|
2217
2863
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2218
2864
|
}
|
|
2219
2865
|
} catch (err) {
|
|
2220
2866
|
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2221
2867
|
}
|
|
2868
|
+
if (needsFullReplay) return this.coldMissRebuild(documentId, scope, branch, targetRevision, signal, false);
|
|
2222
2869
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
2223
2870
|
document.header.revision = revisions.revision;
|
|
2224
2871
|
if (targetRevision !== void 0) document.header.revision = {
|
|
@@ -2337,8 +2984,160 @@ function validateFeatureFlags(flags, prerequisites) {
|
|
|
2337
2984
|
}
|
|
2338
2985
|
}
|
|
2339
2986
|
//#endregion
|
|
2987
|
+
//#region src/storage/kysely/document-purger.ts
|
|
2988
|
+
/** An `Operation` row whose DELETE_DOCUMENT applied: not errored, not denied. */
|
|
2989
|
+
const appliedDelete = sql`action->>'type' = 'DELETE_DOCUMENT' and coalesce(error, '') = '' and coalesce("deniedReason", '') = ''`;
|
|
2990
|
+
/** Rows removed per statement from the tables a document can fill. */
|
|
2991
|
+
const DEFAULT_PURGE_DELETE_BATCH = 5e3;
|
|
2992
|
+
/** A purge transaction's statements; bound to the job's transaction. */
|
|
2993
|
+
var KyselyDocumentPurger = class {
|
|
2994
|
+
batchSize;
|
|
2995
|
+
db;
|
|
2996
|
+
constructor(db, batchSize = DEFAULT_PURGE_DELETE_BATCH) {
|
|
2997
|
+
this.batchSize = batchSize;
|
|
2998
|
+
this.db = db;
|
|
2999
|
+
}
|
|
3000
|
+
/** Every stream the document holds operations in. */
|
|
3001
|
+
async streams(documentId) {
|
|
3002
|
+
return this.db.selectFrom("Operation").select(["scope", "branch"]).distinct().where("documentId", "=", documentId).orderBy("scope").orderBy("branch").execute();
|
|
3003
|
+
}
|
|
3004
|
+
async operationCount(documentId) {
|
|
3005
|
+
const row = await this.db.selectFrom("operation_index_operations").select((eb) => eb.fn.countAll().as("count")).where("documentId", "=", documentId).executeTakeFirst();
|
|
3006
|
+
return Number(row?.count ?? 0);
|
|
3007
|
+
}
|
|
3008
|
+
/** Untombstoned documents group_references records as naming the group. */
|
|
3009
|
+
async groupReferencers(groupId) {
|
|
3010
|
+
const ids = (await this.db.selectFrom("group_references").select("documentId").where("groupId", "=", groupId).where("documentId", "<>", groupId).orderBy("documentId").execute()).map((row) => row.documentId);
|
|
3011
|
+
const purged = await findPurged(this.db, ids);
|
|
3012
|
+
return ids.filter((id) => !purged.has(id));
|
|
3013
|
+
}
|
|
3014
|
+
/** Survivors whose accepted auth history names the group; refusals do not. */
|
|
3015
|
+
async groupReferencersInHistory(groupId) {
|
|
3016
|
+
const referencing = [];
|
|
3017
|
+
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);
|
|
3018
|
+
return referencing;
|
|
3019
|
+
}
|
|
3020
|
+
/** The document's auth-scope operations, per branch, in index order. */
|
|
3021
|
+
async authOperations(documentId) {
|
|
3022
|
+
const rows = await this.db.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", "auth").orderBy("branch").orderBy("index").execute();
|
|
3023
|
+
const byBranch = /* @__PURE__ */ new Map();
|
|
3024
|
+
for (const row of rows) {
|
|
3025
|
+
const operations = byBranch.get(row.branch) ?? [];
|
|
3026
|
+
operations.push({
|
|
3027
|
+
index: row.index,
|
|
3028
|
+
timestampUtcMs: row.timestampUtcMs.toISOString(),
|
|
3029
|
+
hash: row.hash,
|
|
3030
|
+
skip: row.skip,
|
|
3031
|
+
error: row.error || void 0,
|
|
3032
|
+
deniedReason: row.deniedReason || void 0,
|
|
3033
|
+
id: row.opId,
|
|
3034
|
+
action: row.action
|
|
3035
|
+
});
|
|
3036
|
+
byBranch.set(row.branch, operations);
|
|
3037
|
+
}
|
|
3038
|
+
return byBranch;
|
|
3039
|
+
}
|
|
3040
|
+
/** Every document ever in the collection, but its owner and its own row. */
|
|
3041
|
+
async collectionMembers(collectionId, ownerId, branch) {
|
|
3042
|
+
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) => {
|
|
3043
|
+
const openElsewhere = Boolean(row.openElsewhere);
|
|
3044
|
+
return {
|
|
3045
|
+
documentId: row.documentId,
|
|
3046
|
+
openElsewhere,
|
|
3047
|
+
required: !openElsewhere && (Boolean(row.openHere) || Boolean(row.deleted))
|
|
3048
|
+
};
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
async remoteCollection(remoteName) {
|
|
3052
|
+
return (await this.db.selectFrom("sync_remotes").select("collection_id").where("name", "=", remoteName).executeTakeFirst())?.collection_id;
|
|
3053
|
+
}
|
|
3054
|
+
/** Deletes every row about the document; the counts are per table. */
|
|
3055
|
+
async deleteRows(documentId) {
|
|
3056
|
+
const removed = {};
|
|
3057
|
+
removed.Operation = await this.deleteBatched(async () => {
|
|
3058
|
+
const result = await this.db.deleteFrom("Operation").where("id", "in", (eb) => eb.selectFrom("Operation").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3059
|
+
return Number(result.numDeletedRows);
|
|
3060
|
+
});
|
|
3061
|
+
removed.operation_index_operations = await this.deleteBatched(async () => {
|
|
3062
|
+
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();
|
|
3063
|
+
return Number(result.numDeletedRows);
|
|
3064
|
+
});
|
|
3065
|
+
removed.Keyframe = await this.deleteBatched(async () => {
|
|
3066
|
+
const result = await this.db.deleteFrom("Keyframe").where("id", "in", (eb) => eb.selectFrom("Keyframe").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3067
|
+
return Number(result.numDeletedRows);
|
|
3068
|
+
});
|
|
3069
|
+
removed.DocumentSnapshot = await this.deleteBatched(async () => {
|
|
3070
|
+
const result = await this.db.deleteFrom("DocumentSnapshot").where("id", "in", (eb) => eb.selectFrom("DocumentSnapshot").select("id").where("documentId", "=", documentId).limit(this.batchSize)).executeTakeFirst();
|
|
3071
|
+
return Number(result.numDeletedRows);
|
|
3072
|
+
});
|
|
3073
|
+
removed.SlugMapping = await this.deleteWhere("SlugMapping", "documentId", documentId);
|
|
3074
|
+
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();
|
|
3075
|
+
removed.DocumentRelationship = Number(relationships?.count ?? 0);
|
|
3076
|
+
removed.Document = await this.deleteWhere("Document", "id", documentId);
|
|
3077
|
+
removed.group_references = await this.deleteWhere("group_references", "documentId", documentId);
|
|
3078
|
+
removed.sync_dead_letters = await this.deleteWhere("sync_dead_letters", "document_id", documentId);
|
|
3079
|
+
removed.sync_holds = await this.deleteWhere("sync_holds", "document_id", documentId);
|
|
3080
|
+
return removed;
|
|
3081
|
+
}
|
|
3082
|
+
async writeTombstone(tombstone) {
|
|
3083
|
+
const values = {
|
|
3084
|
+
documentId: tombstone.documentId,
|
|
3085
|
+
ordinal: tombstone.ordinal,
|
|
3086
|
+
removedRows: JSON.stringify(tombstone.removedRows),
|
|
3087
|
+
purgedAtUtc: tombstone.purgedAtUtc,
|
|
3088
|
+
requestId: tombstone.requestId
|
|
3089
|
+
};
|
|
3090
|
+
await this.db.insertInto("document_purges").values(values).onConflict((oc) => oc.column("documentId").doUpdateSet({
|
|
3091
|
+
ordinal: values.ordinal,
|
|
3092
|
+
removedRows: values.removedRows,
|
|
3093
|
+
purgedAtUtc: values.purgedAtUtc,
|
|
3094
|
+
requestId: values.requestId
|
|
3095
|
+
})).execute();
|
|
3096
|
+
}
|
|
3097
|
+
/** Reopens every membership at the marker ordinal, plus `collectionIds`. */
|
|
3098
|
+
async reopenMemberships(documentId, ordinal, collectionIds = []) {
|
|
3099
|
+
await this.db.updateTable("document_collections").set({
|
|
3100
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3101
|
+
leftOrdinal: null
|
|
3102
|
+
}).where("documentId", "=", documentId).execute();
|
|
3103
|
+
for (const collectionId of new Set(collectionIds)) await this.db.insertInto("document_collections").values({
|
|
3104
|
+
documentId,
|
|
3105
|
+
collectionId,
|
|
3106
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3107
|
+
leftOrdinal: null
|
|
3108
|
+
}).onConflict((oc) => oc.columns(["documentId", "collectionId"]).doUpdateSet({
|
|
3109
|
+
joinedOrdinal: BigInt(ordinal),
|
|
3110
|
+
leftOrdinal: null
|
|
3111
|
+
})).execute();
|
|
3112
|
+
return (await this.db.selectFrom("document_collections").select("collectionId").where("documentId", "=", documentId).orderBy("collectionId").execute()).map((row) => row.collectionId);
|
|
3113
|
+
}
|
|
3114
|
+
async deleteBatched(step) {
|
|
3115
|
+
let total = 0;
|
|
3116
|
+
for (;;) {
|
|
3117
|
+
const deleted = await step();
|
|
3118
|
+
total += deleted;
|
|
3119
|
+
if (deleted < this.batchSize) return total;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
async deleteWhere(table, column, documentId) {
|
|
3123
|
+
const result = await this.db.deleteFrom(table).where(column, "=", documentId).executeTakeFirst();
|
|
3124
|
+
return Number(result.numDeletedRows);
|
|
3125
|
+
}
|
|
3126
|
+
};
|
|
3127
|
+
//#endregion
|
|
2340
3128
|
//#region src/executor/execution-scope.ts
|
|
3129
|
+
/** For scopes without a transaction to hold a lock in. */
|
|
3130
|
+
const NOOP_DOCUMENT_LOCKS = {
|
|
3131
|
+
shared: () => Promise.resolve(),
|
|
3132
|
+
exclusive: () => Promise.resolve(),
|
|
3133
|
+
purged: () => Promise.resolve(/* @__PURE__ */ new Set())
|
|
3134
|
+
};
|
|
2341
3135
|
var DefaultExecutionScope = class {
|
|
3136
|
+
operationStore;
|
|
3137
|
+
operationIndex;
|
|
3138
|
+
writeCache;
|
|
3139
|
+
documentMetaCache;
|
|
3140
|
+
collectionMembershipCache;
|
|
2342
3141
|
constructor(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2343
3142
|
this.operationStore = operationStore;
|
|
2344
3143
|
this.operationIndex = operationIndex;
|
|
@@ -2353,11 +3152,19 @@ var DefaultExecutionScope = class {
|
|
|
2353
3152
|
operationIndex: this.operationIndex,
|
|
2354
3153
|
writeCache: this.writeCache,
|
|
2355
3154
|
documentMetaCache: this.documentMetaCache,
|
|
2356
|
-
collectionMembershipCache: this.collectionMembershipCache
|
|
3155
|
+
collectionMembershipCache: this.collectionMembershipCache,
|
|
3156
|
+
documentLocks: NOOP_DOCUMENT_LOCKS
|
|
2357
3157
|
});
|
|
2358
3158
|
}
|
|
2359
3159
|
};
|
|
2360
3160
|
var KyselyExecutionScope = class {
|
|
3161
|
+
db;
|
|
3162
|
+
operationStore;
|
|
3163
|
+
operationIndex;
|
|
3164
|
+
keyframeStore;
|
|
3165
|
+
writeCache;
|
|
3166
|
+
documentMetaCache;
|
|
3167
|
+
collectionMembershipCache;
|
|
2361
3168
|
constructor(db, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache) {
|
|
2362
3169
|
this.db = db;
|
|
2363
3170
|
this.operationStore = operationStore;
|
|
@@ -2370,28 +3177,46 @@ var KyselyExecutionScope = class {
|
|
|
2370
3177
|
async run(fn, signal) {
|
|
2371
3178
|
signal?.throwIfAborted();
|
|
2372
3179
|
return this.db.transaction().execute(async (trx) => {
|
|
2373
|
-
const
|
|
2374
|
-
const
|
|
3180
|
+
const sharedLocked = /* @__PURE__ */ new Set();
|
|
3181
|
+
const liveIds = /* @__PURE__ */ new Set();
|
|
3182
|
+
const scopedOperationStore = this.operationStore.withTransaction(trx, liveIds);
|
|
3183
|
+
const scopedOperationIndex = this.operationIndex.withTransaction(trx, liveIds);
|
|
2375
3184
|
const scopedKeyframeStore = this.keyframeStore.withTransaction(trx);
|
|
2376
3185
|
return fn({
|
|
2377
3186
|
operationStore: scopedOperationStore,
|
|
2378
3187
|
operationIndex: scopedOperationIndex,
|
|
2379
3188
|
writeCache: this.writeCache.withScopedStores(scopedOperationStore, scopedKeyframeStore),
|
|
2380
3189
|
documentMetaCache: this.documentMetaCache.withScopedStore(scopedOperationStore),
|
|
2381
|
-
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex)
|
|
3190
|
+
collectionMembershipCache: this.collectionMembershipCache.withScopedIndex(scopedOperationIndex),
|
|
3191
|
+
documentLocks: {
|
|
3192
|
+
shared: async (ids) => {
|
|
3193
|
+
const list = [...ids];
|
|
3194
|
+
await acquirePurgeLocks(trx, list, "shared");
|
|
3195
|
+
for (const id of list) sharedLocked.add(id);
|
|
3196
|
+
},
|
|
3197
|
+
exclusive: (id) => acquirePurgeLocks(trx, [id], "exclusive"),
|
|
3198
|
+
purged: async (ids) => {
|
|
3199
|
+
const list = [...ids];
|
|
3200
|
+
const found = await findPurged(trx, list);
|
|
3201
|
+
for (const id of list) if (sharedLocked.has(id) && !found.has(id)) liveIds.add(id);
|
|
3202
|
+
return found;
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
purger: new KyselyDocumentPurger(trx)
|
|
2382
3206
|
});
|
|
2383
3207
|
});
|
|
2384
3208
|
}
|
|
2385
3209
|
};
|
|
2386
3210
|
//#endregion
|
|
2387
3211
|
//#region src/utils/reshuffle.ts
|
|
2388
|
-
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
3212
|
+
const STRICT_ORDER_ACTION_TYPES = /* @__PURE__ */ new Set([
|
|
2389
3213
|
"CREATE_DOCUMENT",
|
|
2390
3214
|
"DELETE_DOCUMENT",
|
|
2391
3215
|
"UPGRADE_DOCUMENT",
|
|
2392
3216
|
"ADD_RELATIONSHIP",
|
|
2393
3217
|
"REMOVE_RELATIONSHIP",
|
|
2394
3218
|
"UPDATE_RELATIONSHIP",
|
|
3219
|
+
"PURGE_DOCUMENT",
|
|
2395
3220
|
"ADD_FOLDER",
|
|
2396
3221
|
"UPDATE_FOLDER",
|
|
2397
3222
|
"REMOVE_FOLDER"
|
|
@@ -2597,6 +3422,15 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2597
3422
|
const derivedSet = derivedReadSet(definition);
|
|
2598
3423
|
if (!definition.evaluatesScope(scope)) return operations.map(() => void 0);
|
|
2599
3424
|
const evaluating = new Set(operations.map((operation) => operation.id));
|
|
3425
|
+
const creation = operations.find((operation) => operation.action.type === "CREATE_DOCUMENT" && operation.action.input.documentId === target.documentId);
|
|
3426
|
+
async function stateBefore(query) {
|
|
3427
|
+
try {
|
|
3428
|
+
return await writeCache.getState(query.documentId, query.scope, query.branch, -1, signal);
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
if (creation !== void 0 && query.documentId === target.documentId && error instanceof DocumentNotFoundError && !(error instanceof DocumentPurgedError)) return createDocumentFromAction(creation.action);
|
|
3431
|
+
throw error;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
2600
3434
|
const readStreams = await Promise.all(readSet.map(async (stream) => ({
|
|
2601
3435
|
stream,
|
|
2602
3436
|
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 +3443,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2609
3443
|
const histories = [];
|
|
2610
3444
|
for (const read of readStreams) {
|
|
2611
3445
|
const streamOperations = read.stream === writtenProjection ? [...read.operations, ...operations] : read.operations;
|
|
2612
|
-
const before = await
|
|
3446
|
+
const before = await stateBefore(read.stream.query);
|
|
2613
3447
|
walked.push({
|
|
2614
3448
|
streamKey: streamKey(read.stream.query),
|
|
2615
3449
|
scope: read.stream.query.scope,
|
|
@@ -2631,7 +3465,7 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2631
3465
|
branch: target.branch
|
|
2632
3466
|
};
|
|
2633
3467
|
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
|
|
3468
|
+
const before = await stateBefore(query);
|
|
2635
3469
|
evaluatedStateKey = streamKey(query);
|
|
2636
3470
|
walked.push({
|
|
2637
3471
|
streamKey: evaluatedStateKey,
|
|
@@ -2702,58 +3536,36 @@ async function evaluateByPosition(model, target, subject, stores, signal) {
|
|
|
2702
3536
|
return operations.map((operation) => reasons.get(operation.id));
|
|
2703
3537
|
}
|
|
2704
3538
|
//#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
3539
|
//#region src/executor/document-action-handler.ts
|
|
2750
3540
|
var DocumentActionHandler = class {
|
|
2751
|
-
|
|
3541
|
+
registry;
|
|
3542
|
+
logger;
|
|
3543
|
+
driveContainerTypes;
|
|
3544
|
+
featureFlags;
|
|
3545
|
+
decisionModel;
|
|
3546
|
+
protocolSupport;
|
|
3547
|
+
loggedUnregisteredProtocols = /* @__PURE__ */ new Set();
|
|
3548
|
+
constructor(registry, logger, driveContainerTypes, featureFlags, decisionModel, protocolSupport) {
|
|
2752
3549
|
this.registry = registry;
|
|
2753
3550
|
this.logger = logger;
|
|
2754
3551
|
this.driveContainerTypes = driveContainerTypes;
|
|
2755
3552
|
this.featureFlags = featureFlags;
|
|
2756
3553
|
this.decisionModel = decisionModel;
|
|
3554
|
+
this.protocolSupport = protocolSupport;
|
|
3555
|
+
}
|
|
3556
|
+
/** Keys this reactor does not register are admitted: it cannot judge them. */
|
|
3557
|
+
unsupportedProtocol(documentId, protocolVersions) {
|
|
3558
|
+
for (const [protocol, version] of Object.entries(protocolVersions ?? {})) {
|
|
3559
|
+
const supported = this.protocolSupport[protocol];
|
|
3560
|
+
if (supported === void 0) {
|
|
3561
|
+
if (!this.loggedUnregisteredProtocols.has(protocol)) {
|
|
3562
|
+
this.loggedUnregisteredProtocols.add(protocol);
|
|
3563
|
+
this.logger.info("Admitting document @documentId with unregistered protocol @protocol @version", documentId, protocol, version);
|
|
3564
|
+
}
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
if (!supported.includes(version)) return new UnsupportedProtocolVersionError(documentId, protocol, version);
|
|
3568
|
+
}
|
|
2757
3569
|
}
|
|
2758
3570
|
/** Whether the write arrives with its evaluation already decided. */
|
|
2759
3571
|
alreadyEvaluated(executing) {
|
|
@@ -2771,6 +3583,7 @@ var DocumentActionHandler = class {
|
|
|
2771
3583
|
case "ADD_RELATIONSHIP": return this.executeAddRelationship(write, executing);
|
|
2772
3584
|
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(write, executing);
|
|
2773
3585
|
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(write, executing);
|
|
3586
|
+
case "PURGE_DOCUMENT": return buildErrorResult(executing.job, new ReservedActionError(executing.job.documentId, action.type), executing.startTime);
|
|
2774
3587
|
default: return buildErrorResult(executing.job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), executing.startTime);
|
|
2775
3588
|
}
|
|
2776
3589
|
}
|
|
@@ -2840,7 +3653,7 @@ var DocumentActionHandler = class {
|
|
|
2840
3653
|
[job.scope]: [...standing.operations[job.scope] ?? [], operation]
|
|
2841
3654
|
};
|
|
2842
3655
|
executing.touchedStreams.add(job.documentId, job.scope, job.branch);
|
|
2843
|
-
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing,
|
|
3656
|
+
stores.writeCache.putState(job.documentId, job.scope, job.branch, operation.index, standing, "head");
|
|
2844
3657
|
indexTxn.write([{
|
|
2845
3658
|
...operation,
|
|
2846
3659
|
documentId: job.documentId,
|
|
@@ -2852,6 +3665,7 @@ var DocumentActionHandler = class {
|
|
|
2852
3665
|
stores.documentMetaCache.putDocumentMeta(job.documentId, job.branch, {
|
|
2853
3666
|
state: standing.state.document,
|
|
2854
3667
|
documentType: standing.header.documentType,
|
|
3668
|
+
protocolVersions: standing.header.protocolVersions,
|
|
2855
3669
|
documentScopeRevision: operation.index + 1
|
|
2856
3670
|
});
|
|
2857
3671
|
return buildSuccessResult(job, operation, job.documentId, standing.header.documentType, JSON.stringify({
|
|
@@ -2868,6 +3682,9 @@ var DocumentActionHandler = class {
|
|
|
2868
3682
|
error: /* @__PURE__ */ new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
|
|
2869
3683
|
duration: Date.now() - startTime
|
|
2870
3684
|
};
|
|
3685
|
+
const input = action.input;
|
|
3686
|
+
const unsupported = this.unsupportedProtocol(input.documentId, input.protocolVersions);
|
|
3687
|
+
if (unsupported) return buildErrorResult(job, unsupported, startTime);
|
|
2871
3688
|
const document = createDocumentFromAction(action);
|
|
2872
3689
|
let operation = createOperation(action, 0, skip, {
|
|
2873
3690
|
documentId: document.header.id,
|
|
@@ -2875,8 +3692,8 @@ var DocumentActionHandler = class {
|
|
|
2875
3692
|
branch: job.branch
|
|
2876
3693
|
});
|
|
2877
3694
|
const resultingStateObj = {
|
|
2878
|
-
|
|
2879
|
-
|
|
3695
|
+
...document.state,
|
|
3696
|
+
header: document.header
|
|
2880
3697
|
};
|
|
2881
3698
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
2882
3699
|
const writeResult = await this.writeOperationToStore({
|
|
@@ -2893,7 +3710,7 @@ var DocumentActionHandler = class {
|
|
|
2893
3710
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2894
3711
|
};
|
|
2895
3712
|
executing.touchedStreams.add(document.header.id, job.scope, job.branch);
|
|
2896
|
-
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document,
|
|
3713
|
+
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document, "head");
|
|
2897
3714
|
indexTxn.write([{
|
|
2898
3715
|
...operation,
|
|
2899
3716
|
documentId: document.header.id,
|
|
@@ -2910,6 +3727,7 @@ var DocumentActionHandler = class {
|
|
|
2910
3727
|
stores.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
2911
3728
|
state: document.state.document,
|
|
2912
3729
|
documentType: document.header.documentType,
|
|
3730
|
+
protocolVersions: document.header.protocolVersions,
|
|
2913
3731
|
documentScopeRevision: 1
|
|
2914
3732
|
});
|
|
2915
3733
|
return buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
@@ -2953,7 +3771,7 @@ var DocumentActionHandler = class {
|
|
|
2953
3771
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2954
3772
|
};
|
|
2955
3773
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
2956
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3774
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
2957
3775
|
indexTxn.write([{
|
|
2958
3776
|
...operation,
|
|
2959
3777
|
documentId,
|
|
@@ -2965,6 +3783,7 @@ var DocumentActionHandler = class {
|
|
|
2965
3783
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
2966
3784
|
state: document.state.document,
|
|
2967
3785
|
documentType: document.header.documentType,
|
|
3786
|
+
protocolVersions: document.header.protocolVersions,
|
|
2968
3787
|
documentScopeRevision: operation.index + 1
|
|
2969
3788
|
});
|
|
2970
3789
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
@@ -3003,7 +3822,7 @@ var DocumentActionHandler = class {
|
|
|
3003
3822
|
} catch (error) {
|
|
3004
3823
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`Failed to fetch revisions for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3005
3824
|
}
|
|
3006
|
-
const revisionScopes = new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3825
|
+
const revisionScopes = /* @__PURE__ */ new Set([...Object.keys(input.revision), ...Object.keys(actualRevisions)]);
|
|
3007
3826
|
for (const revisionScope of revisionScopes) {
|
|
3008
3827
|
const snapshot = input.revision[revisionScope] ?? 0;
|
|
3009
3828
|
const actual = actualRevisions[revisionScope] ?? 0;
|
|
@@ -3045,8 +3864,8 @@ var DocumentActionHandler = class {
|
|
|
3045
3864
|
branch: job.branch
|
|
3046
3865
|
});
|
|
3047
3866
|
const resultingStateObj = {
|
|
3048
|
-
|
|
3049
|
-
|
|
3867
|
+
...document.state,
|
|
3868
|
+
header: document.header
|
|
3050
3869
|
};
|
|
3051
3870
|
if (fromVersion > 0) resultingStateObj.__migrated = true;
|
|
3052
3871
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
@@ -3064,7 +3883,7 @@ var DocumentActionHandler = class {
|
|
|
3064
3883
|
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3065
3884
|
};
|
|
3066
3885
|
executing.touchedStreams.add(documentId, job.scope, job.branch);
|
|
3067
|
-
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document,
|
|
3886
|
+
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document, "head");
|
|
3068
3887
|
for (const scope of otherScopes) executing.postCommitInvalidations.push({
|
|
3069
3888
|
documentId,
|
|
3070
3889
|
scope,
|
|
@@ -3081,22 +3900,41 @@ var DocumentActionHandler = class {
|
|
|
3081
3900
|
stores.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
3082
3901
|
state: document.state.document,
|
|
3083
3902
|
documentType: document.header.documentType,
|
|
3903
|
+
protocolVersions: document.header.protocolVersions,
|
|
3084
3904
|
documentScopeRevision: operation.index + 1
|
|
3085
3905
|
});
|
|
3086
3906
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
3087
3907
|
}
|
|
3088
|
-
|
|
3908
|
+
/** A submitted write naming a purged target was refused at job start. */
|
|
3909
|
+
async isTargetPurged(write, executing) {
|
|
3910
|
+
const target = relationshipTarget(write.action);
|
|
3911
|
+
if (target === void 0 || !executing.purgeFence) return false;
|
|
3912
|
+
return executing.purgeFence.isPurged(target);
|
|
3913
|
+
}
|
|
3914
|
+
async executeAddRelationship(write, executing) {
|
|
3915
|
+
let targetPurged;
|
|
3916
|
+
try {
|
|
3917
|
+
targetPurged = await this.isTargetPurged(write, executing);
|
|
3918
|
+
} catch (error) {
|
|
3919
|
+
return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
|
|
3920
|
+
}
|
|
3089
3921
|
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)) {
|
|
3922
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3091
3923
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3092
3924
|
txn.addToCollection(collectionId, input.targetId);
|
|
3093
3925
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
3094
3926
|
}
|
|
3095
3927
|
});
|
|
3096
3928
|
}
|
|
3097
|
-
executeRemoveRelationship(write, executing) {
|
|
3929
|
+
async executeRemoveRelationship(write, executing) {
|
|
3930
|
+
let targetPurged;
|
|
3931
|
+
try {
|
|
3932
|
+
targetPurged = await this.isTargetPurged(write, executing);
|
|
3933
|
+
} catch (error) {
|
|
3934
|
+
return buildErrorResult(executing.job, error instanceof Error ? error : new Error(String(error)), executing.startTime);
|
|
3935
|
+
}
|
|
3098
3936
|
return this.withRelationshipAction("REMOVE_RELATIONSHIP", write, executing, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
3099
|
-
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3937
|
+
if (!targetPurged && this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3100
3938
|
const collectionId = DriveCollectionId.forDrive(input.sourceId, j.branch).key;
|
|
3101
3939
|
txn.removeFromCollection(collectionId, input.targetId);
|
|
3102
3940
|
s.collectionMembershipCache.invalidate(input.targetId);
|
|
@@ -3120,6 +3958,7 @@ var DocumentActionHandler = class {
|
|
|
3120
3958
|
try {
|
|
3121
3959
|
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
3122
3960
|
} catch (error) {
|
|
3961
|
+
if (DocumentPurgedError.isError(error)) return buildErrorResult(job, error, startTime);
|
|
3123
3962
|
if (DocumentNotFoundError.isError(error)) return buildErrorResult(job, new DocumentNotFoundError(input.sourceId, `${actionTypeName}: source document ${input.sourceId} not found`), startTime);
|
|
3124
3963
|
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3125
3964
|
}
|
|
@@ -3150,7 +3989,7 @@ var DocumentActionHandler = class {
|
|
|
3150
3989
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
3151
3990
|
executing.touchedStreams.add(input.sourceId, job.scope, job.branch);
|
|
3152
3991
|
executing.touchedStreams.add(input.targetId, job.scope, job.branch);
|
|
3153
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc,
|
|
3992
|
+
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc, "head");
|
|
3154
3993
|
indexTxn.write([{
|
|
3155
3994
|
...operation,
|
|
3156
3995
|
documentId: input.sourceId,
|
|
@@ -3169,6 +4008,7 @@ var DocumentActionHandler = class {
|
|
|
3169
4008
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3170
4009
|
state: sourceDoc.state.document,
|
|
3171
4010
|
documentType: sourceDoc.header.documentType,
|
|
4011
|
+
protocolVersions: sourceDoc.header.protocolVersions,
|
|
3172
4012
|
documentScopeRevision: operation.index + 1
|
|
3173
4013
|
});
|
|
3174
4014
|
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
@@ -3197,55 +4037,669 @@ var DocumentActionHandler = class {
|
|
|
3197
4037
|
}
|
|
3198
4038
|
};
|
|
3199
4039
|
//#endregion
|
|
3200
|
-
//#region src/
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
4040
|
+
//#region src/signer/passthrough-signer.ts
|
|
4041
|
+
/**
|
|
4042
|
+
* A no-op signer that returns empty values for all methods.
|
|
4043
|
+
* Used when signing is not required.
|
|
4044
|
+
*/
|
|
4045
|
+
var PassthroughSigner = class {
|
|
4046
|
+
publicKey = {};
|
|
4047
|
+
sign() {
|
|
4048
|
+
return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
|
|
3204
4049
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
4050
|
+
verify() {
|
|
4051
|
+
return Promise.resolve();
|
|
4052
|
+
}
|
|
4053
|
+
signAction() {
|
|
4054
|
+
return Promise.resolve([
|
|
4055
|
+
"",
|
|
4056
|
+
"",
|
|
4057
|
+
"",
|
|
4058
|
+
"",
|
|
4059
|
+
""
|
|
4060
|
+
]);
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
//#endregion
|
|
4064
|
+
//#region src/signer/trust-policy.ts
|
|
4065
|
+
/**
|
|
4066
|
+
* The policy admission asks: the reactor's own key signing as its own user is
|
|
4067
|
+
* accepted, then the host's policy decides, else the default does. `own` is
|
|
4068
|
+
* read on every ask, since its user changes on login.
|
|
4069
|
+
*/
|
|
4070
|
+
function admissionTrustPolicy(own, authEnforcement, policy) {
|
|
4071
|
+
return { authorizeSigner(signer, key, documentId) {
|
|
4072
|
+
const identity = actionSignerIdentity(own);
|
|
4073
|
+
if (identity.app.key !== "" && key === identity.app.key) {
|
|
4074
|
+
if (sameUser(actionSignerIdentity(signer).user, identity.user)) return Promise.resolve(true);
|
|
3228
4075
|
}
|
|
4076
|
+
if (policy) return policy.authorizeSigner(signer, key, documentId);
|
|
4077
|
+
return Promise.resolve(!authEnforcement);
|
|
4078
|
+
} };
|
|
4079
|
+
}
|
|
4080
|
+
function sameUser(a, b) {
|
|
4081
|
+
return a.address.toLowerCase() === b.address.toLowerCase() && a.networkId === b.networkId && a.chainId === b.chainId;
|
|
4082
|
+
}
|
|
4083
|
+
//#endregion
|
|
4084
|
+
//#region src/signer/did-key.ts
|
|
4085
|
+
const DID_KEY_PREFIX = "did:key:z";
|
|
4086
|
+
const P256_MULTICODEC = [128, 36];
|
|
4087
|
+
const MAX_CACHED_KEYS = 1024;
|
|
4088
|
+
const P = BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
|
|
4089
|
+
const A = BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc");
|
|
4090
|
+
const B = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
|
|
4091
|
+
/** A did:key that is not a compressed P-256 public key. */
|
|
4092
|
+
var MalformedDidKeyError = class extends Error {
|
|
4093
|
+
constructor(did, detail) {
|
|
4094
|
+
super(`Malformed did:key ${did}: ${detail}`);
|
|
4095
|
+
this.name = "MalformedDidKeyError";
|
|
3229
4096
|
}
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
4097
|
+
};
|
|
4098
|
+
const keyCache = /* @__PURE__ */ new Map();
|
|
4099
|
+
/** Cached per process: decompressing the point costs a BigInt modPow. */
|
|
4100
|
+
async function importDidKey(did) {
|
|
4101
|
+
const cached = keyCache.get(did);
|
|
4102
|
+
if (cached) {
|
|
4103
|
+
keyCache.delete(did);
|
|
4104
|
+
keyCache.set(did, cached);
|
|
4105
|
+
return cached;
|
|
4106
|
+
}
|
|
4107
|
+
const raw = decompressP256(compressedPointFromDid(did), did);
|
|
4108
|
+
const key = await crypto.subtle.importKey("raw", raw.buffer, {
|
|
4109
|
+
name: "ECDSA",
|
|
4110
|
+
namedCurve: "P-256"
|
|
4111
|
+
}, false, ["verify"]);
|
|
4112
|
+
keyCache.set(did, key);
|
|
4113
|
+
if (keyCache.size > MAX_CACHED_KEYS) {
|
|
4114
|
+
const oldest = keyCache.keys().next().value;
|
|
4115
|
+
if (oldest !== void 0) keyCache.delete(oldest);
|
|
4116
|
+
}
|
|
4117
|
+
return key;
|
|
4118
|
+
}
|
|
4119
|
+
function compressedPointFromDid(did) {
|
|
4120
|
+
if (!did.startsWith(DID_KEY_PREFIX)) throw new MalformedDidKeyError(did, "not a base58btc did:key");
|
|
4121
|
+
const decoded = base58Decode(did.slice(9));
|
|
4122
|
+
if (!decoded || decoded.length !== 35) throw new MalformedDidKeyError(did, "not a compressed P-256 key");
|
|
4123
|
+
if (decoded[0] !== P256_MULTICODEC[0] || decoded[1] !== P256_MULTICODEC[1]) throw new MalformedDidKeyError(did, "not a P-256 multicodec");
|
|
4124
|
+
return decoded.subarray(2);
|
|
4125
|
+
}
|
|
4126
|
+
function decompressP256(compressed, did) {
|
|
4127
|
+
const prefix = compressed[0];
|
|
4128
|
+
if (prefix !== 2 && prefix !== 3) throw new MalformedDidKeyError(did, `compression prefix ${prefix}`);
|
|
4129
|
+
let x = BigInt(0);
|
|
4130
|
+
for (let i = 1; i < compressed.length; i++) x = x << BigInt(8) | BigInt(compressed[i]);
|
|
4131
|
+
if (x >= P) throw new MalformedDidKeyError(did, "x outside the field");
|
|
4132
|
+
const ySquared = (modPow(x, BigInt(3), P) + A * x + B) % P;
|
|
4133
|
+
let y = modPow(ySquared, (P + BigInt(1)) / BigInt(4), P);
|
|
4134
|
+
if (y * y % P !== ySquared) throw new MalformedDidKeyError(did, "x is not on the curve");
|
|
4135
|
+
if (y % BigInt(2) === BigInt(0) !== (prefix === 2)) y = P - y;
|
|
4136
|
+
const uncompressed = /* @__PURE__ */ new Uint8Array(65);
|
|
4137
|
+
uncompressed[0] = 4;
|
|
4138
|
+
uncompressed.set(bigIntToBytes(x), 1);
|
|
4139
|
+
uncompressed.set(bigIntToBytes(y), 33);
|
|
4140
|
+
return uncompressed;
|
|
4141
|
+
}
|
|
4142
|
+
function modPow(base, exp, mod) {
|
|
4143
|
+
let result = BigInt(1);
|
|
4144
|
+
let b = base % mod;
|
|
4145
|
+
let e = exp;
|
|
4146
|
+
while (e > BigInt(0)) {
|
|
4147
|
+
if (e & BigInt(1)) result = result * b % mod;
|
|
4148
|
+
e >>= BigInt(1);
|
|
4149
|
+
b = b * b % mod;
|
|
4150
|
+
}
|
|
4151
|
+
return result;
|
|
4152
|
+
}
|
|
4153
|
+
function bigIntToBytes(value) {
|
|
4154
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
4155
|
+
let v = value;
|
|
4156
|
+
for (let i = 31; i >= 0; i--) {
|
|
4157
|
+
bytes[i] = Number(v & BigInt(255));
|
|
4158
|
+
v >>= BigInt(8);
|
|
4159
|
+
}
|
|
4160
|
+
return bytes;
|
|
4161
|
+
}
|
|
4162
|
+
//#endregion
|
|
4163
|
+
//#region src/signer/verify-action-signature.ts
|
|
4164
|
+
const RENOWN_HASH_LENGTH = 44;
|
|
4165
|
+
const SHARED_HASH_LENGTH = 28;
|
|
4166
|
+
const HEX_SIGNATURE = /^(0x)?([0-9a-fA-F]{2})+$/;
|
|
4167
|
+
/**
|
|
4168
|
+
* Integrity only: identity binding and the live-id check sit with the caller.
|
|
4169
|
+
* `operation` is the incoming operation at load admission.
|
|
4170
|
+
*/
|
|
4171
|
+
async function verifyActionSignature(action, target, path, operation) {
|
|
4172
|
+
const policy = target.policy ?? "legacy";
|
|
4173
|
+
const refusal = policyRefusal(action, policy);
|
|
4174
|
+
if (refusal) return refusal;
|
|
4175
|
+
const shape = signerShape(action.context?.signer);
|
|
4176
|
+
if (shape.kind === "unsigned") return policy === "v2-required" ? refuse("unsigned", "UNSIGNED_REQUIRED", `action ${action.id} is unsigned but ${target.documentId} requires v2 signatures`) : {
|
|
4177
|
+
ok: true,
|
|
4178
|
+
scheme: "unsigned"
|
|
4179
|
+
};
|
|
4180
|
+
if (shape.kind === "malformed") return refuse("legacy-unknown", "MALFORMED_TUPLE", `action ${action.id} ${shape.reason}`);
|
|
4181
|
+
const { signer, tuple } = shape;
|
|
4182
|
+
const scheme = schemeOf(tuple[2]);
|
|
4183
|
+
if (tuple[1] !== signer.app.key) return refuse(scheme, "KEY_MISMATCH", `action ${action.id} tuple key does not match signer.app.key`);
|
|
4184
|
+
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`);
|
|
4185
|
+
if (scheme === "legacy-unknown" && path === "mutation") return refuse(scheme, "MALFORMED_TUPLE", `action ${action.id} carries a hash of unknown length ${tuple[2].length}`);
|
|
4186
|
+
if (scheme === "v2") {
|
|
4187
|
+
const refusal = await checkV2(action, tuple, target, operation);
|
|
4188
|
+
if (refusal) return refusal;
|
|
4189
|
+
} else if (path === "mutation") {
|
|
4190
|
+
if (await legacyHash(scheme, action, target) !== tuple[2]) return refuse(scheme, "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
4191
|
+
}
|
|
4192
|
+
return verifyEcdsa(tuple, scheme, action.id);
|
|
4193
|
+
}
|
|
4194
|
+
/** Refusals a document's policy makes before any tuple is read. */
|
|
4195
|
+
function policyRefusal(action, policy) {
|
|
4196
|
+
if (action.type !== "CREATE_DOCUMENT") return;
|
|
4197
|
+
const scheme = schemeOfAction(action);
|
|
4198
|
+
const input = action.input;
|
|
4199
|
+
const documentId = input?.documentId ?? "";
|
|
4200
|
+
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;
|
|
4201
|
+
const expected = derivedCreateId(input);
|
|
4202
|
+
if (expected === documentId) return;
|
|
4203
|
+
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}`);
|
|
4204
|
+
}
|
|
4205
|
+
function derivedCreateId(input) {
|
|
4206
|
+
if (!input?.signing || !input.protocolVersions) return;
|
|
4207
|
+
try {
|
|
4208
|
+
return deriveDocumentId({
|
|
4209
|
+
documentType: input.model,
|
|
4210
|
+
createdAtUtcIso: input.signing.createdAtUtcIso,
|
|
4211
|
+
nonce: input.signing.nonce,
|
|
4212
|
+
protocolVersions: input.protocolVersions
|
|
4213
|
+
});
|
|
4214
|
+
} catch {
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
function schemeOfAction(action) {
|
|
4219
|
+
const shape = signerShape(action.context?.signer);
|
|
4220
|
+
if (shape.kind === "unsigned") return "unsigned";
|
|
4221
|
+
return shape.kind === "signed" ? schemeOf(shape.tuple[2]) : "legacy-unknown";
|
|
4222
|
+
}
|
|
4223
|
+
/** Peers send arbitrary JSON, so nothing past `context` is assumed. */
|
|
4224
|
+
function signerShape(value) {
|
|
4225
|
+
if (value === void 0 || value === null) return { kind: "unsigned" };
|
|
4226
|
+
if (!isRecord(value)) return {
|
|
4227
|
+
kind: "malformed",
|
|
4228
|
+
reason: "has a signer that is not an object"
|
|
4229
|
+
};
|
|
4230
|
+
const { app, user, signatures } = value;
|
|
4231
|
+
if (app !== void 0 && app !== null && !isRecord(app)) return {
|
|
4232
|
+
kind: "malformed",
|
|
4233
|
+
reason: "has a signer.app that is not an object"
|
|
4234
|
+
};
|
|
4235
|
+
const key = isRecord(app) ? app.key : void 0;
|
|
4236
|
+
if (key !== void 0 && key !== null && typeof key !== "string") return {
|
|
4237
|
+
kind: "malformed",
|
|
4238
|
+
reason: "has a signer.app.key that is not a string"
|
|
4239
|
+
};
|
|
4240
|
+
if (!key) return { kind: "unsigned" };
|
|
4241
|
+
if (!isRecord(user) || typeof user.address !== "string") return {
|
|
4242
|
+
kind: "malformed",
|
|
4243
|
+
reason: "has a signer without a user address"
|
|
4244
|
+
};
|
|
4245
|
+
if (!Array.isArray(signatures)) return {
|
|
4246
|
+
kind: "malformed",
|
|
4247
|
+
reason: "has a signer whose signatures are not a list"
|
|
4248
|
+
};
|
|
4249
|
+
const tuple = signatures.at(-1);
|
|
4250
|
+
if (!isTuple(tuple)) return {
|
|
4251
|
+
kind: "malformed",
|
|
4252
|
+
reason: signatures.length === 0 ? "has a signer but no signatures" : "carries a malformed signature tuple"
|
|
4253
|
+
};
|
|
4254
|
+
return {
|
|
4255
|
+
kind: "signed",
|
|
4256
|
+
signer: value,
|
|
4257
|
+
tuple
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
function isRecord(value) {
|
|
4261
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4262
|
+
}
|
|
4263
|
+
async function checkV2(action, tuple, target, operation) {
|
|
4264
|
+
const problem = v2TupleProblem(tuple);
|
|
4265
|
+
if (problem) return refuse("v2", "MALFORMED_TUPLE", `action ${action.id}: ${problem}`);
|
|
4266
|
+
if (operation && !sameInstant(operation.timestampUtcMs, action.timestampUtcMs)) return refuse("v2", "TIMESTAMP_MISMATCH", `action ${action.id} is stamped ${action.timestampUtcMs} but its operation ${operation.timestampUtcMs}`);
|
|
4267
|
+
let expected;
|
|
4268
|
+
try {
|
|
4269
|
+
expected = await hashActionV2(action, target, action.context.signer);
|
|
4270
|
+
} catch (error) {
|
|
4271
|
+
return refuse("v2", "HASH_MISMATCH", `action ${action.id} has no v2 hash: ${error instanceof Error ? error.message : String(error)}`);
|
|
4272
|
+
}
|
|
4273
|
+
if (expected !== tuple[2]) return refuse("v2", "HASH_MISMATCH", `action ${action.id} does not match the hash its signature covers`);
|
|
4274
|
+
}
|
|
4275
|
+
function sameInstant(a, b) {
|
|
4276
|
+
const left = Date.parse(a);
|
|
4277
|
+
return !Number.isNaN(left) && left === Date.parse(b);
|
|
4278
|
+
}
|
|
4279
|
+
function schemeOf(hash) {
|
|
4280
|
+
if (isV2ActionHash(hash)) return "v2";
|
|
4281
|
+
if (hash.length === RENOWN_HASH_LENGTH) return "legacy-renown";
|
|
4282
|
+
if (hash.length === SHARED_HASH_LENGTH) return "legacy-shared";
|
|
4283
|
+
return "legacy-unknown";
|
|
4284
|
+
}
|
|
4285
|
+
async function legacyHash(scheme, action, target) {
|
|
4286
|
+
if (scheme === "legacy-renown") {
|
|
4287
|
+
const payload = [
|
|
4288
|
+
action.scope,
|
|
4289
|
+
action.type,
|
|
4290
|
+
JSON.stringify(action.input)
|
|
4291
|
+
].join("");
|
|
4292
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
|
|
4293
|
+
return bytesToBase64(new Uint8Array(digest));
|
|
4294
|
+
}
|
|
4295
|
+
const signer = action.context.signer;
|
|
4296
|
+
return buildOperationSignatureParams({
|
|
4297
|
+
documentId: target.documentId,
|
|
4298
|
+
signer,
|
|
4299
|
+
action,
|
|
4300
|
+
previousStateHash: ""
|
|
4301
|
+
})[2];
|
|
4302
|
+
}
|
|
4303
|
+
async function verifyEcdsa(tuple, scheme, actionId) {
|
|
4304
|
+
const [timestamp, key, hash, prevStateHash, signatureHex] = tuple;
|
|
4305
|
+
if (!HEX_SIGNATURE.test(signatureHex)) return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId} signature is not hex`);
|
|
4306
|
+
let cryptoKey;
|
|
4307
|
+
try {
|
|
4308
|
+
cryptoKey = await importDidKey(key);
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
return refuse(scheme, "MALFORMED_TUPLE", `action ${actionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4311
|
+
}
|
|
4312
|
+
const message = buildOperationSignatureMessage([
|
|
4313
|
+
timestamp,
|
|
4314
|
+
key,
|
|
4315
|
+
hash,
|
|
4316
|
+
prevStateHash
|
|
4317
|
+
]);
|
|
4318
|
+
let valid;
|
|
4319
|
+
try {
|
|
4320
|
+
valid = await crypto.subtle.verify({
|
|
4321
|
+
name: "ECDSA",
|
|
4322
|
+
hash: "SHA-256"
|
|
4323
|
+
}, cryptoKey, hexToBytes(signatureHex).buffer, message.buffer);
|
|
4324
|
+
} catch {
|
|
4325
|
+
valid = false;
|
|
4326
|
+
}
|
|
4327
|
+
if (!valid) return refuse(scheme, "BAD_SIGNATURE", `action ${actionId} signature does not verify under its key`);
|
|
4328
|
+
return {
|
|
4329
|
+
ok: true,
|
|
4330
|
+
scheme
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
function isTuple(value) {
|
|
4334
|
+
return Array.isArray(value) && value.length === 5 && value.every((element) => typeof element === "string");
|
|
4335
|
+
}
|
|
4336
|
+
function refuse(scheme, code, reason) {
|
|
4337
|
+
return {
|
|
4338
|
+
ok: false,
|
|
4339
|
+
scheme,
|
|
4340
|
+
code,
|
|
4341
|
+
reason
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
function hexToBytes(hex) {
|
|
4345
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
4346
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
4347
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
|
|
4348
|
+
return bytes;
|
|
4349
|
+
}
|
|
4350
|
+
function bytesToBase64(bytes) {
|
|
4351
|
+
let binary = "";
|
|
4352
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
4353
|
+
return btoa(binary);
|
|
4354
|
+
}
|
|
4355
|
+
//#endregion
|
|
4356
|
+
//#region src/executor/synthesized-signing.ts
|
|
4357
|
+
/** Submitted types whose stored action the reducer writes in their place. */
|
|
4358
|
+
const SYNTHESIZING_TYPES = /* @__PURE__ */ new Set(["UNDO", "REDO"]);
|
|
4359
|
+
/**
|
|
4360
|
+
* The id of the action synthesized from `submittedId`. Derived, so the stored
|
|
4361
|
+
* operation answers for the submitted id at the live-id check.
|
|
4362
|
+
*/
|
|
4363
|
+
function synthesizedActionId(submittedId) {
|
|
4364
|
+
return hashBrowser(`synthesized:${submittedId}`, "sha1", "hex").slice(0, 32);
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* Whether `operation` stores an action the reducer made from `submitted`
|
|
4368
|
+
* rather than `submitted` itself or an operation already in `history`.
|
|
4369
|
+
*/
|
|
4370
|
+
function isSynthesized(submitted, operation, history) {
|
|
4371
|
+
if (!SYNTHESIZING_TYPES.has(submitted.type)) return false;
|
|
4372
|
+
const id = operation.action.id;
|
|
4373
|
+
if (!id) return true;
|
|
4374
|
+
if (id === submitted.id) return false;
|
|
4375
|
+
return !history.some((existing) => existing.action.id === id);
|
|
4376
|
+
}
|
|
4377
|
+
/**
|
|
4378
|
+
* Signs the synthesized action of `operation` in place as `signer`, under the
|
|
4379
|
+
* id {@link synthesizedActionId} derives from the submitted one. A REDO's
|
|
4380
|
+
* rebuilt action has no timestamp, so it takes the submitted one; the
|
|
4381
|
+
* operation timestamp is set to the action's, which a peer requires of a v2
|
|
4382
|
+
* tuple.
|
|
4383
|
+
*/
|
|
4384
|
+
async function signSynthesized(operation, submitted, target, signer, signal) {
|
|
4385
|
+
const synthesized = operation.action;
|
|
4386
|
+
const id = synthesizedActionId(submitted.id);
|
|
4387
|
+
const timestampUtcMs = operation.timestampUtcMs || synthesized.timestampUtcMs || submitted.timestampUtcMs;
|
|
4388
|
+
const action = {
|
|
4389
|
+
id,
|
|
4390
|
+
timestampUtcMs,
|
|
4391
|
+
type: synthesized.type,
|
|
4392
|
+
input: synthesized.input,
|
|
4393
|
+
scope: synthesized.scope
|
|
4394
|
+
};
|
|
4395
|
+
const signature = await signer.signAction(action, {
|
|
4396
|
+
documentId: target.documentId,
|
|
4397
|
+
branch: target.branch
|
|
4398
|
+
}, signal);
|
|
4399
|
+
operation.action = {
|
|
4400
|
+
...action,
|
|
4401
|
+
context: { signer: {
|
|
4402
|
+
...actionSignerIdentity(signer),
|
|
4403
|
+
signatures: [signature]
|
|
4404
|
+
} }
|
|
4405
|
+
};
|
|
4406
|
+
operation.id = deriveOperationId(target.documentId, target.scope, target.branch, id);
|
|
4407
|
+
operation.timestampUtcMs = timestampUtcMs;
|
|
4408
|
+
}
|
|
4409
|
+
//#endregion
|
|
4410
|
+
//#region src/executor/signature-admission.ts
|
|
4411
|
+
const DEFAULT_TRUST_TIMEOUT_MS = 1e4;
|
|
4412
|
+
/** Runs once per write, when this reactor first stores it; never on re-appends. */
|
|
4413
|
+
var SignatureAdmission = class {
|
|
4414
|
+
mode;
|
|
4415
|
+
logger;
|
|
4416
|
+
eventBus;
|
|
4417
|
+
policySource;
|
|
4418
|
+
trustPolicy;
|
|
4419
|
+
trustTimeoutMs;
|
|
4420
|
+
constructor(mode, logger, eventBus, policySource = "meta", trust = {
|
|
4421
|
+
authEnforcement: false,
|
|
4422
|
+
timeoutMs: DEFAULT_TRUST_TIMEOUT_MS
|
|
4423
|
+
}) {
|
|
4424
|
+
this.mode = mode;
|
|
4425
|
+
this.logger = logger;
|
|
4426
|
+
this.eventBus = eventBus;
|
|
4427
|
+
this.policySource = policySource;
|
|
4428
|
+
this.trustPolicy = admissionTrustPolicy(trust.signer ?? new PassthroughSigner(), trust.authEnforcement, trust.policy);
|
|
4429
|
+
this.trustTimeoutMs = trust.timeoutMs;
|
|
4430
|
+
}
|
|
4431
|
+
/** The first refusal of a mutation's submitted actions, when enforcing. */
|
|
4432
|
+
async admitMutation(job, stores, signal) {
|
|
4433
|
+
const { operationStore } = stores;
|
|
4434
|
+
const candidates = job.actions.map((action) => candidate(action, mutationStream(action, job)));
|
|
4435
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4436
|
+
const committed = await this.storedAsSubmitted(candidates, live, isRetry(job), operationStore, signal);
|
|
4437
|
+
await resolvePolicies(candidates, job.actions, (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4438
|
+
const submitted = /* @__PURE__ */ new Set();
|
|
4439
|
+
for (const entry of candidates) {
|
|
4440
|
+
if (committed.has(entry)) continue;
|
|
4441
|
+
const verdict = await this.verdict(entry, live, submitted, "mutation", signal);
|
|
4442
|
+
submitted.add(entry.opId);
|
|
4443
|
+
if (verdict.ok) continue;
|
|
4444
|
+
const refusal = this.record(job, entry, verdict, "mutation");
|
|
4445
|
+
if (refusal) return {
|
|
4446
|
+
kind: "refused",
|
|
4447
|
+
error: refusal
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
return {
|
|
4451
|
+
kind: "admitted",
|
|
4452
|
+
committed: [...committed.values()]
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
/** The incoming operations a load drops, empty unless enforcing. */
|
|
4456
|
+
async admitLoad(job, operations, stores, signal) {
|
|
4457
|
+
const { operationStore } = stores;
|
|
4458
|
+
const stream = {
|
|
4459
|
+
documentId: job.documentId,
|
|
4460
|
+
scope: job.scope,
|
|
4461
|
+
branch: job.branch
|
|
4462
|
+
};
|
|
4463
|
+
const candidates = operations.map((operation) => ({
|
|
4464
|
+
...candidate(operation.action, stream),
|
|
4465
|
+
operation
|
|
4466
|
+
}));
|
|
4467
|
+
const live = await this.liveOperationIds(candidates, operationStore, signal);
|
|
4468
|
+
const redelivered = await this.redelivered(candidates, live, operationStore, signal);
|
|
4469
|
+
await resolvePolicies(candidates, job.operations.map((operation) => operation.action), (documentId, branch) => storedPolicy(stores, this.policySource, documentId, branch, signal));
|
|
4470
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
4471
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
4472
|
+
const verdict = await this.verdict(candidates[i], live, void 0, "load", signal, redelivered.has(candidates[i]));
|
|
4473
|
+
if (verdict.ok) continue;
|
|
4474
|
+
if (this.record(job, candidates[i], verdict, "load")) dropped.add(operations[i]);
|
|
4475
|
+
}
|
|
4476
|
+
return dropped;
|
|
4477
|
+
}
|
|
4478
|
+
/** A marker is held to v2 and enforced whatever the policy and mode. */
|
|
4479
|
+
async admitMarker(job, marker, signal) {
|
|
4480
|
+
const entry = {
|
|
4481
|
+
...candidate(marker.action, {
|
|
4482
|
+
documentId: job.documentId,
|
|
4483
|
+
scope: "document",
|
|
4484
|
+
branch: job.branch
|
|
4485
|
+
}),
|
|
4486
|
+
operation: marker,
|
|
4487
|
+
policy: "v2-required"
|
|
4488
|
+
};
|
|
4489
|
+
const verdict = await this.verdict(entry, /* @__PURE__ */ new Set(), void 0, "load", signal);
|
|
4490
|
+
if (verdict.ok) return;
|
|
4491
|
+
return this.record(job, entry, verdict, "load", true);
|
|
4492
|
+
}
|
|
4493
|
+
async verdict(entry, live, submitted, path, signal, known = false) {
|
|
4494
|
+
const verdict = await verifyActionSignature(entry.action, {
|
|
4495
|
+
documentId: entry.stream.documentId,
|
|
4496
|
+
branch: entry.stream.branch,
|
|
4497
|
+
policy: entry.policy
|
|
4498
|
+
}, path, entry.operation);
|
|
4499
|
+
if (!verdict.ok) return verdict;
|
|
4500
|
+
if (!known && (isLive(entry, live) || submitted?.has(entry.opId))) return {
|
|
4501
|
+
ok: false,
|
|
4502
|
+
scheme: verdict.scheme,
|
|
4503
|
+
code: "DUPLICATE_ACTION",
|
|
4504
|
+
reason: `action ${entry.action.id} is already in the stream`
|
|
4505
|
+
};
|
|
4506
|
+
if (verdict.scheme === "unsigned") {
|
|
4507
|
+
const address = entry.action.context?.signer?.user?.address;
|
|
4508
|
+
return address ? {
|
|
4509
|
+
ok: false,
|
|
4510
|
+
scheme: "unsigned",
|
|
4511
|
+
code: "UNSIGNED_IDENTITY",
|
|
4512
|
+
reason: `action ${entry.action.id} is unsigned but claims to act as ${address}`
|
|
4513
|
+
} : verdict;
|
|
4514
|
+
}
|
|
4515
|
+
if (known) return verdict;
|
|
4516
|
+
const signer = entry.action.context.signer;
|
|
4517
|
+
if (!await this.authorize(signer, entry.stream.documentId, signal)) return {
|
|
4518
|
+
ok: false,
|
|
4519
|
+
scheme: verdict.scheme,
|
|
4520
|
+
code: "SIGNER_UNAUTHORIZED",
|
|
4521
|
+
reason: `action ${entry.action.id}: key ${signer.app.key} may not sign as ${signer.user.address}`
|
|
4522
|
+
};
|
|
4523
|
+
return verdict;
|
|
4524
|
+
}
|
|
4525
|
+
/** Throws when the policy does, times out or the job is aborted. */
|
|
4526
|
+
async authorize(signer, documentId, signal) {
|
|
4527
|
+
signal?.throwIfAborted();
|
|
4528
|
+
let timer;
|
|
4529
|
+
let onAbort;
|
|
4530
|
+
const stop = new Promise((_, reject) => {
|
|
4531
|
+
timer = setTimeout(() => {
|
|
4532
|
+
reject(/* @__PURE__ */ new Error(`signature trust policy did not answer for ${signer.app.key} within ${this.trustTimeoutMs}ms`));
|
|
4533
|
+
}, this.trustTimeoutMs);
|
|
4534
|
+
if (signal) {
|
|
4535
|
+
onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
|
|
4536
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3244
4537
|
}
|
|
3245
|
-
|
|
4538
|
+
});
|
|
4539
|
+
try {
|
|
4540
|
+
return await Promise.race([this.trustPolicy.authorizeSigner(signer, signer.app.key, documentId), stop]);
|
|
4541
|
+
} finally {
|
|
4542
|
+
clearTimeout(timer);
|
|
4543
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
/**
|
|
4547
|
+
* The live candidates stored exactly as submitted and still standing: one
|
|
4548
|
+
* an undo or a reshuffle has since superseded is refused, not re-emitted with
|
|
4549
|
+
* the state it once produced. A synthesized operation
|
|
4550
|
+
* holds another action, so it cannot be compared; only a retry, whose first
|
|
4551
|
+
* attempt this job was, takes it as committed.
|
|
4552
|
+
*/
|
|
4553
|
+
async storedAsSubmitted(candidates, live, retry, operationStore, signal) {
|
|
4554
|
+
const committed = /* @__PURE__ */ new Map();
|
|
4555
|
+
const toCompare = [];
|
|
4556
|
+
for (const entry of candidates) if (entry.synthesizedOpId && live.has(entry.synthesizedOpId)) {
|
|
4557
|
+
if (retry) committed.set(entry, committedWrite(entry, entry.synthesizedOpId));
|
|
4558
|
+
} else if (live.has(entry.opId)) toCompare.push(entry);
|
|
4559
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4560
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4561
|
+
const byId = new Map(stored.map((operation) => [operation.id, operation]));
|
|
4562
|
+
const matching = entries.filter((entry) => {
|
|
4563
|
+
const operation = byId.get(entry.opId);
|
|
4564
|
+
return operation && sameContent(operation.action, entry.action);
|
|
4565
|
+
});
|
|
4566
|
+
if (matching.length === 0) continue;
|
|
4567
|
+
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);
|
|
4568
|
+
for (const entry of matching) if (!isSuperseded(byId.get(entry.opId), later.results)) committed.set(entry, committedWrite(entry, entry.opId));
|
|
3246
4569
|
}
|
|
4570
|
+
return committed;
|
|
4571
|
+
}
|
|
4572
|
+
/**
|
|
4573
|
+
* The load candidates stored exactly as they arrive, timestamp included: a
|
|
4574
|
+
* peer re-sending what this stream holds, not a replay of its action.
|
|
4575
|
+
*/
|
|
4576
|
+
async redelivered(candidates, live, operationStore, signal) {
|
|
4577
|
+
const found = /* @__PURE__ */ new Set();
|
|
4578
|
+
const toCompare = candidates.filter((entry) => live.has(entry.opId) && !(entry.synthesizedOpId && live.has(entry.synthesizedOpId)));
|
|
4579
|
+
for (const [stream, entries] of byStream(toCompare)) {
|
|
4580
|
+
const stored = await operationStore.getOperationsByIds(stream.documentId, stream.scope, stream.branch, entries.map((entry) => entry.opId), signal);
|
|
4581
|
+
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);
|
|
4582
|
+
}
|
|
4583
|
+
return found;
|
|
4584
|
+
}
|
|
4585
|
+
async liveOperationIds(candidates, operationStore, signal) {
|
|
4586
|
+
const live = /* @__PURE__ */ new Set();
|
|
4587
|
+
for (const [stream, entries] of byStream(candidates)) {
|
|
4588
|
+
const found = await operationStore.findOperationIds(stream.documentId, stream.scope, stream.branch, entries.flatMap((entry) => entry.synthesizedOpId ? [entry.opId, entry.synthesizedOpId] : [entry.opId]), signal);
|
|
4589
|
+
for (const opId of found) live.add(opId);
|
|
4590
|
+
}
|
|
4591
|
+
return live;
|
|
4592
|
+
}
|
|
4593
|
+
/** Logs and counts a refusal; returns the error only when enforcing. */
|
|
4594
|
+
record(job, entry, refusal, path, enforced = this.mode === "enforce") {
|
|
4595
|
+
const event = {
|
|
4596
|
+
jobId: job.id,
|
|
4597
|
+
documentId: entry.stream.documentId,
|
|
4598
|
+
scope: entry.stream.scope,
|
|
4599
|
+
branch: entry.stream.branch,
|
|
4600
|
+
actionId: entry.action.id,
|
|
4601
|
+
code: refusal.code,
|
|
4602
|
+
scheme: refusal.scheme,
|
|
4603
|
+
path,
|
|
4604
|
+
enforced,
|
|
4605
|
+
reason: refusal.reason
|
|
4606
|
+
};
|
|
4607
|
+
this.logger.warn(enforced ? "Signature refused: @Refusal" : "Signature refusal logged, write admitted: @Refusal", event);
|
|
4608
|
+
this.eventBus.emit(ReactorEventTypes.SIGNATURE_REFUSED, event).catch((error) => {
|
|
4609
|
+
this.logger.error("Failed to emit SIGNATURE_REFUSED: @Error", error);
|
|
4610
|
+
});
|
|
4611
|
+
return enforced ? new InvalidSignatureError(entry.stream.documentId, refusal.code, refusal.reason) : void 0;
|
|
3247
4612
|
}
|
|
3248
4613
|
};
|
|
4614
|
+
/**
|
|
4615
|
+
* A CREATE_DOCUMENT is verified under its own input. Anything else takes the
|
|
4616
|
+
* stored document's policy, or, for a document this job creates, that CREATE's.
|
|
4617
|
+
*/
|
|
4618
|
+
async function resolvePolicies(candidates, jobActions, stored) {
|
|
4619
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4620
|
+
for (const entry of candidates) {
|
|
4621
|
+
if (entry.action.type === "CREATE_DOCUMENT") {
|
|
4622
|
+
entry.policy = createPolicy(entry.action);
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
const { documentId, branch } = entry.stream;
|
|
4626
|
+
const key = `${documentId}\u0000${branch}`;
|
|
4627
|
+
let policy = resolved.get(key);
|
|
4628
|
+
if (policy === void 0) {
|
|
4629
|
+
policy = await stored(documentId, branch) ?? createdPolicy(jobActions, documentId) ?? "legacy";
|
|
4630
|
+
resolved.set(key, policy);
|
|
4631
|
+
}
|
|
4632
|
+
entry.policy = policy;
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
async function storedPolicy(stores, source, documentId, branch, signal) {
|
|
4636
|
+
try {
|
|
4637
|
+
if (source === "write-cache") {
|
|
4638
|
+
const document = await stores.writeCache.getState(documentId, "document", branch, void 0, signal);
|
|
4639
|
+
return signaturePolicyOf(document.header);
|
|
4640
|
+
}
|
|
4641
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
4642
|
+
return signaturePolicyOf(meta.protocolVersions);
|
|
4643
|
+
} catch (error) {
|
|
4644
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
4645
|
+
throw error;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
function createdPolicy(actions, documentId) {
|
|
4649
|
+
const create = actions.find((action) => action.type === "CREATE_DOCUMENT" && action.input?.documentId === documentId);
|
|
4650
|
+
return create ? createPolicy(create) : void 0;
|
|
4651
|
+
}
|
|
4652
|
+
function createPolicy(action) {
|
|
4653
|
+
return signaturePolicyOf(action.input);
|
|
4654
|
+
}
|
|
4655
|
+
function candidate(action, stream) {
|
|
4656
|
+
const opId = (actionId) => deriveOperationId(stream.documentId, stream.scope, stream.branch, actionId);
|
|
4657
|
+
return {
|
|
4658
|
+
action,
|
|
4659
|
+
stream,
|
|
4660
|
+
opId: opId(action.id),
|
|
4661
|
+
synthesizedOpId: SYNTHESIZING_TYPES.has(action.type) ? opId(synthesizedActionId(action.id)) : void 0
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
4664
|
+
function isLive(entry, live) {
|
|
4665
|
+
return live.has(entry.opId) || entry.synthesizedOpId !== void 0 && live.has(entry.synthesizedOpId);
|
|
4666
|
+
}
|
|
4667
|
+
function mutationStream(action, job) {
|
|
4668
|
+
return {
|
|
4669
|
+
...actionSigningTarget(action, job.documentId, job.branch),
|
|
4670
|
+
scope: job.scope
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
function byStream(candidates) {
|
|
4674
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4675
|
+
for (const entry of candidates) {
|
|
4676
|
+
const key = `${entry.stream.documentId}\u0000${entry.stream.scope}\u0000${entry.stream.branch}`;
|
|
4677
|
+
const group = groups.get(key) ?? [entry.stream, []];
|
|
4678
|
+
group[1].push(entry);
|
|
4679
|
+
groups.set(key, group);
|
|
4680
|
+
}
|
|
4681
|
+
return [...groups.values()];
|
|
4682
|
+
}
|
|
4683
|
+
function isRetry(job) {
|
|
4684
|
+
return job.errorHistory.length > 0 || (job.retryCount ?? 0) > 0;
|
|
4685
|
+
}
|
|
4686
|
+
function committedWrite(entry, opId) {
|
|
4687
|
+
return {
|
|
4688
|
+
...entry.stream,
|
|
4689
|
+
actionId: entry.action.id,
|
|
4690
|
+
opId
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
function isSuperseded(operation, later) {
|
|
4694
|
+
return later.some((next) => next.index > operation.index && next.skip > 0 && next.index - next.skip <= operation.index);
|
|
4695
|
+
}
|
|
4696
|
+
function sameContent(stored, submitted) {
|
|
4697
|
+
try {
|
|
4698
|
+
return canonicalJson(stored) === canonicalJson(submitted);
|
|
4699
|
+
} catch {
|
|
4700
|
+
return false;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
3249
4703
|
//#endregion
|
|
3250
4704
|
//#region src/executor/simple-job-executor.ts
|
|
3251
4705
|
const MAX_SKIP_THRESHOLD = 1e3;
|
|
@@ -3262,7 +4716,32 @@ function isValidISOTimestamp(value) {
|
|
|
3262
4716
|
* returned JobResult there, which is what the queue, the worker protocol and
|
|
3263
4717
|
* every test expect a failed job to look like.
|
|
3264
4718
|
*/
|
|
4719
|
+
/** The ids of a purge job's request, which a drive purge may precede. */
|
|
4720
|
+
function purgeRequestDocumentIds(job) {
|
|
4721
|
+
const ids = job.meta.purgeRequestDocumentIds;
|
|
4722
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
4723
|
+
}
|
|
4724
|
+
const MARKER_INPUT_KEYS = /* @__PURE__ */ new Set([
|
|
4725
|
+
"documentId",
|
|
4726
|
+
"documentType",
|
|
4727
|
+
"purgedAtUtcIso",
|
|
4728
|
+
"requestId"
|
|
4729
|
+
]);
|
|
4730
|
+
/** A peer's marker must name this job's id on main and carry nothing else. */
|
|
4731
|
+
function malformedMarker(job) {
|
|
4732
|
+
const action = job.operations.find((operation) => isPurgeMarker(operation))?.action;
|
|
4733
|
+
const input = action?.input;
|
|
4734
|
+
const refuse = (reason) => new InvalidSignatureError(job.documentId, "ID_MISMATCH", `marker ${action?.id ?? "?"} ${reason}`);
|
|
4735
|
+
if (typeof input !== "object" || input === null) return refuse("carries no input");
|
|
4736
|
+
const extra = Object.keys(input).filter((key) => !MARKER_INPUT_KEYS.has(key));
|
|
4737
|
+
if (extra.length > 0) return refuse(`carries unexpected input ${extra.join(", ")}`);
|
|
4738
|
+
for (const key of MARKER_INPUT_KEYS) if (typeof input[key] !== "string" || input[key] === "") return refuse(`input.${key} is not a non-empty string`);
|
|
4739
|
+
const purgedAt = Date.parse(input.purgedAtUtcIso);
|
|
4740
|
+
if (Number.isNaN(purgedAt) || new Date(purgedAt).toISOString() !== input.purgedAtUtcIso) return refuse("input.purgedAtUtcIso is not an ISO timestamp");
|
|
4741
|
+
if (input.documentId !== job.documentId || job.branch !== "main" || action?.scope !== "document") return refuse(`does not purge ${job.documentId} on document/main`);
|
|
4742
|
+
}
|
|
3265
4743
|
var JobRollbackSignal = class extends Error {
|
|
4744
|
+
result;
|
|
3266
4745
|
constructor(result) {
|
|
3267
4746
|
super("job rolled back");
|
|
3268
4747
|
this.result = result;
|
|
@@ -3270,13 +4749,27 @@ var JobRollbackSignal = class extends Error {
|
|
|
3270
4749
|
}
|
|
3271
4750
|
};
|
|
3272
4751
|
var SimpleJobExecutor = class {
|
|
4752
|
+
logger;
|
|
4753
|
+
registry;
|
|
4754
|
+
operationStore;
|
|
4755
|
+
eventBus;
|
|
4756
|
+
writeCache;
|
|
4757
|
+
operationIndex;
|
|
4758
|
+
documentMetaCache;
|
|
4759
|
+
collectionMembershipCache;
|
|
4760
|
+
driveContainerTypes;
|
|
3273
4761
|
config;
|
|
3274
4762
|
featureFlags;
|
|
3275
4763
|
decisionModel;
|
|
3276
|
-
|
|
4764
|
+
signatureAdmission;
|
|
3277
4765
|
documentActionHandler;
|
|
3278
4766
|
executionScope;
|
|
3279
|
-
|
|
4767
|
+
signer;
|
|
4768
|
+
/**
|
|
4769
|
+
* `signer` signs the operations the reducer synthesizes; unsigned if omitted.
|
|
4770
|
+
* `trustPolicy` decides which keys may sign as which users at admission.
|
|
4771
|
+
*/
|
|
4772
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, executionScope, signer, trustPolicy) {
|
|
3280
4773
|
this.logger = logger;
|
|
3281
4774
|
this.registry = registry;
|
|
3282
4775
|
this.operationStore = operationStore;
|
|
@@ -3286,6 +4779,8 @@ var SimpleJobExecutor = class {
|
|
|
3286
4779
|
this.documentMetaCache = documentMetaCache;
|
|
3287
4780
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3288
4781
|
this.driveContainerTypes = driveContainerTypes;
|
|
4782
|
+
this.signer = signer ?? new PassthroughSigner();
|
|
4783
|
+
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3289
4784
|
this.config = {
|
|
3290
4785
|
featureFlags: config.featureFlags ?? {},
|
|
3291
4786
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
@@ -3295,12 +4790,19 @@ var SimpleJobExecutor = class {
|
|
|
3295
4790
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
3296
4791
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5e3,
|
|
3297
4792
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50,
|
|
3298
|
-
batchApplies: config.batchApplies ?? true
|
|
4793
|
+
batchApplies: config.batchApplies ?? true,
|
|
4794
|
+
signatureVerification: config.signatureVerification ?? "enforce",
|
|
4795
|
+
protocolSupport: config.protocolSupport ?? localSupports(PEER_CAPABILITIES, this.featureFlags).protocols,
|
|
4796
|
+
maxPurgeOperations: config.maxPurgeOperations ?? 2e5
|
|
3299
4797
|
};
|
|
3300
|
-
this.featureFlags = resolveFeatureFlags(config.featureFlags);
|
|
3301
4798
|
this.decisionModel = selectDecisionModel(this.featureFlags, registry);
|
|
3302
|
-
this.
|
|
3303
|
-
|
|
4799
|
+
this.signatureAdmission = new SignatureAdmission(this.config.signatureVerification, logger, eventBus, this.featureFlags.documentDecisions ? "write-cache" : "meta", {
|
|
4800
|
+
signer: this.signer,
|
|
4801
|
+
authEnforcement: this.featureFlags.authEnforcement,
|
|
4802
|
+
policy: trustPolicy,
|
|
4803
|
+
timeoutMs: Math.min(DEFAULT_TRUST_TIMEOUT_MS, this.config.jobTimeoutMs / 2)
|
|
4804
|
+
});
|
|
4805
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes, this.featureFlags, this.decisionModel, this.config.protocolSupport);
|
|
3304
4806
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3305
4807
|
}
|
|
3306
4808
|
/**
|
|
@@ -3327,6 +4829,7 @@ var SimpleJobExecutor = class {
|
|
|
3327
4829
|
const touchedStreams = new TouchedStreams();
|
|
3328
4830
|
const postCommitInvalidations = [];
|
|
3329
4831
|
const postCommitMembershipInvalidations = [];
|
|
4832
|
+
const postCommitMetaInvalidations = [];
|
|
3330
4833
|
let outcome;
|
|
3331
4834
|
try {
|
|
3332
4835
|
outcome = await this.executionScope.run(async (stores) => {
|
|
@@ -3337,7 +4840,8 @@ var SimpleJobExecutor = class {
|
|
|
3337
4840
|
signal,
|
|
3338
4841
|
touchedStreams,
|
|
3339
4842
|
postCommitInvalidations,
|
|
3340
|
-
postCommitMembershipInvalidations
|
|
4843
|
+
postCommitMembershipInvalidations,
|
|
4844
|
+
postCommitMetaInvalidations
|
|
3341
4845
|
});
|
|
3342
4846
|
if (!scoped.result.success) throw new JobRollbackSignal(scoped.result);
|
|
3343
4847
|
return scoped;
|
|
@@ -3349,6 +4853,7 @@ var SimpleJobExecutor = class {
|
|
|
3349
4853
|
}
|
|
3350
4854
|
for (const entry of postCommitInvalidations) this.writeCache.invalidate(entry.documentId, entry.scope, entry.branch);
|
|
3351
4855
|
for (const documentId of postCommitMembershipInvalidations) this.collectionMembershipCache.invalidate(documentId);
|
|
4856
|
+
for (const documentId of postCommitMetaInvalidations) this.documentMetaCache.invalidate(documentId);
|
|
3352
4857
|
const { pendingEvent } = outcome;
|
|
3353
4858
|
if (pendingEvent) this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, pendingEvent).catch((error) => {
|
|
3354
4859
|
this.logger.error("Failed to emit JOB_WRITE_READY event: @Event : @Error", pendingEvent, error);
|
|
@@ -3364,8 +4869,38 @@ var SimpleJobExecutor = class {
|
|
|
3364
4869
|
* has not committed yet has nothing to announce.
|
|
3365
4870
|
*/
|
|
3366
4871
|
async executeInScope(params) {
|
|
3367
|
-
const { job, startTime, stores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
4872
|
+
const { job, startTime, stores: scopeStores, signal, touchedStreams, postCommitInvalidations, postCommitMembershipInvalidations } = params;
|
|
3368
4873
|
let pendingEvent;
|
|
4874
|
+
if (job.kind === "mutation") {
|
|
4875
|
+
const reserved = job.actions.find((action) => isPurgeMarker(action));
|
|
4876
|
+
if (reserved) return { result: buildErrorResult(job, new ReservedActionError(job.documentId, reserved.type), startTime) };
|
|
4877
|
+
}
|
|
4878
|
+
if (job.kind === "purge" || job.kind === "load" && job.operations.some((op) => isPurgeMarker(op))) return this.executePurge(params);
|
|
4879
|
+
const lockedIds = jobWriteIds(job);
|
|
4880
|
+
const lockFailure = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
|
|
4881
|
+
try {
|
|
4882
|
+
await scopeStores.documentLocks.shared(lockedIds);
|
|
4883
|
+
} catch (error) {
|
|
4884
|
+
return lockFailure(error);
|
|
4885
|
+
}
|
|
4886
|
+
let purged;
|
|
4887
|
+
try {
|
|
4888
|
+
purged = await scopeStores.documentLocks.purged(lockedIds);
|
|
4889
|
+
} catch (error) {
|
|
4890
|
+
return lockFailure(error);
|
|
4891
|
+
}
|
|
4892
|
+
const purgedRefusal = this.purgedRefusal(job, purged);
|
|
4893
|
+
if (purgedRefusal) return { result: buildErrorResult(job, purgedRefusal, startTime) };
|
|
4894
|
+
const purgeFence = new PurgeFence(scopeStores.documentLocks, new Set(lockedIds), purged);
|
|
4895
|
+
const stores = {
|
|
4896
|
+
...scopeStores,
|
|
4897
|
+
writeCache: new FencedWriteCache(scopeStores.writeCache, purgeFence, (documentId) => {
|
|
4898
|
+
scopeStores.writeCache.invalidate(documentId);
|
|
4899
|
+
scopeStores.documentMetaCache.invalidate(documentId);
|
|
4900
|
+
})
|
|
4901
|
+
};
|
|
4902
|
+
const unsupported = await this.unsupportedStoredProtocol(job, stores, signal);
|
|
4903
|
+
if (unsupported) return { result: buildErrorResult(job, unsupported, startTime) };
|
|
3369
4904
|
const indexTxn = stores.operationIndex.start();
|
|
3370
4905
|
if (job.kind === "load") {
|
|
3371
4906
|
const loadResult = await this.executeLoadJob({
|
|
@@ -3378,7 +4913,8 @@ var SimpleJobExecutor = class {
|
|
|
3378
4913
|
evaluatedByPosition: false,
|
|
3379
4914
|
postCommitInvalidations,
|
|
3380
4915
|
postCommitMembershipInvalidations,
|
|
3381
|
-
touchedStreams
|
|
4916
|
+
touchedStreams,
|
|
4917
|
+
purgeFence
|
|
3382
4918
|
});
|
|
3383
4919
|
if (loadResult.success && loadResult.operationsWithContext) {
|
|
3384
4920
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -3408,7 +4944,8 @@ var SimpleJobExecutor = class {
|
|
|
3408
4944
|
evaluatedByPosition: false,
|
|
3409
4945
|
postCommitInvalidations,
|
|
3410
4946
|
postCommitMembershipInvalidations,
|
|
3411
|
-
touchedStreams
|
|
4947
|
+
touchedStreams,
|
|
4948
|
+
purgeFence
|
|
3412
4949
|
});
|
|
3413
4950
|
if (reevalResult.success && reevalResult.operationsWithContext) {
|
|
3414
4951
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
@@ -3429,10 +4966,29 @@ var SimpleJobExecutor = class {
|
|
|
3429
4966
|
pendingEvent
|
|
3430
4967
|
};
|
|
3431
4968
|
}
|
|
3432
|
-
|
|
4969
|
+
let admission;
|
|
4970
|
+
try {
|
|
4971
|
+
admission = await this.signatureAdmission.admitMutation(job, stores, signal);
|
|
4972
|
+
} catch (error) {
|
|
4973
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4974
|
+
}
|
|
4975
|
+
if (admission.kind === "refused") return { result: buildErrorResult(job, admission.error, startTime) };
|
|
4976
|
+
let committed;
|
|
4977
|
+
try {
|
|
4978
|
+
committed = await this.reloadCommitted(admission.committed, stores, signal);
|
|
4979
|
+
} catch (error) {
|
|
4980
|
+
return { result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) };
|
|
4981
|
+
}
|
|
4982
|
+
const committedIds = new Set(admission.committed.map((write) => write.actionId));
|
|
4983
|
+
const fresh = committedIds.size === 0 ? job : {
|
|
4984
|
+
...job,
|
|
4985
|
+
actions: job.actions.filter((action) => !committedIds.has(action.id))
|
|
4986
|
+
};
|
|
4987
|
+
if (fresh.actions.length === 0) return this.committedOutcome(job, committed, [], [], stores, startTime);
|
|
4988
|
+
const positioned = await this.positionByTimestamp(fresh, stores, signal);
|
|
3433
4989
|
if (positioned.error) return { result: buildErrorResult(job, positioned.error, startTime) };
|
|
3434
4990
|
const executing = {
|
|
3435
|
-
job,
|
|
4991
|
+
job: fresh,
|
|
3436
4992
|
startTime,
|
|
3437
4993
|
indexTxn,
|
|
3438
4994
|
stores,
|
|
@@ -3441,7 +4997,8 @@ var SimpleJobExecutor = class {
|
|
|
3441
4997
|
evaluatedByPosition: positioned.evaluatedByPosition,
|
|
3442
4998
|
postCommitInvalidations,
|
|
3443
4999
|
postCommitMembershipInvalidations,
|
|
3444
|
-
touchedStreams
|
|
5000
|
+
touchedStreams,
|
|
5001
|
+
purgeFence
|
|
3445
5002
|
};
|
|
3446
5003
|
const actionResult = await this.processActions(positioned.writes, executing);
|
|
3447
5004
|
if (!actionResult.success) return { result: {
|
|
@@ -3462,13 +5019,291 @@ var SimpleJobExecutor = class {
|
|
|
3462
5019
|
} };
|
|
3463
5020
|
const ordinals = await stores.operationIndex.commit(indexTxn, signal);
|
|
3464
5021
|
postCommitMembershipInvalidations.push(...indexTxn.getMembershipInvalidations());
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
5022
|
+
for (let i = 0; i < actionResult.operationsWithContext.length; i++) actionResult.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
5023
|
+
return this.committedOutcome(job, committed, actionResult.operationsWithContext, actionResult.generatedOperations, stores, startTime);
|
|
5024
|
+
}
|
|
5025
|
+
/** A load names only its own id purged; a foreign one is ID_MISMATCH. */
|
|
5026
|
+
purgedRefusal(job, purged) {
|
|
5027
|
+
if (purged.size === 0) return;
|
|
5028
|
+
if (purged.has(job.documentId)) return new DocumentPurgedError(job.documentId);
|
|
5029
|
+
const actions = [...job.actions, ...job.operations.map((operation) => operation.action)];
|
|
5030
|
+
for (const action of actions) {
|
|
5031
|
+
if (!DOCUMENT_SCOPE_ACTIONS.has(action.type)) continue;
|
|
5032
|
+
const target = targetDocumentId(action, job.documentId);
|
|
5033
|
+
if (!purged.has(target)) continue;
|
|
5034
|
+
if (job.kind === "mutation") return new DocumentPurgedError(target);
|
|
5035
|
+
return new InvalidSignatureError(job.documentId, "ID_MISMATCH", `${action.type} ${action.id} in ${job.documentId} writes purged ${target}`);
|
|
5036
|
+
}
|
|
5037
|
+
if (job.kind !== "mutation") return;
|
|
5038
|
+
for (const action of job.actions) {
|
|
5039
|
+
const target = relationshipTarget(action);
|
|
5040
|
+
if (action.type === "ADD_RELATIONSHIP" && target !== void 0 && purged.has(target)) return new DocumentPurgedError(target, `${action.type} target ${target} was purged`);
|
|
5041
|
+
if (action.scope !== "auth") continue;
|
|
5042
|
+
const group = mentionedGroupIds(action).find((id) => purged.has(id));
|
|
5043
|
+
if (group !== void 0) return new DocumentPurgedError(group, `${action.type} names purged group ${group}`);
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
/** A purge job or a marker load: the exclusive lock is the job's only lock. */
|
|
5047
|
+
async executePurge(params) {
|
|
5048
|
+
const { job, startTime, stores } = params;
|
|
5049
|
+
const fail = (error) => ({ result: buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime) });
|
|
5050
|
+
if (job.kind === "load") {
|
|
5051
|
+
const malformed = malformedMarker(job);
|
|
5052
|
+
if (malformed) return fail(malformed);
|
|
5053
|
+
}
|
|
5054
|
+
const purger = stores.purger;
|
|
5055
|
+
if (!purger) return fail(/* @__PURE__ */ new Error(`Purge of ${job.documentId} needs a transactional scope`));
|
|
5056
|
+
try {
|
|
5057
|
+
await stores.documentLocks.exclusive(job.documentId);
|
|
5058
|
+
} catch (error) {
|
|
5059
|
+
return fail(error);
|
|
5060
|
+
}
|
|
5061
|
+
let alreadyPurged;
|
|
5062
|
+
try {
|
|
5063
|
+
alreadyPurged = (await stores.documentLocks.purged([job.documentId])).has(job.documentId);
|
|
5064
|
+
} catch (error) {
|
|
5065
|
+
return fail(error);
|
|
5066
|
+
}
|
|
5067
|
+
if (alreadyPurged) return {
|
|
5068
|
+
result: {
|
|
5069
|
+
job,
|
|
5070
|
+
success: true,
|
|
5071
|
+
operations: [],
|
|
5072
|
+
operationsWithContext: [],
|
|
5073
|
+
duration: Date.now() - startTime
|
|
5074
|
+
},
|
|
5075
|
+
pendingEvent: {
|
|
5076
|
+
jobId: job.id,
|
|
5077
|
+
operations: [],
|
|
5078
|
+
jobMeta: job.meta,
|
|
5079
|
+
collectionMemberships: {}
|
|
5080
|
+
}
|
|
5081
|
+
};
|
|
5082
|
+
let commit;
|
|
5083
|
+
try {
|
|
5084
|
+
commit = job.kind === "purge" ? await this.preparePurgeJob(params, purger) : await this.preparePurgeLoad(params, purger);
|
|
5085
|
+
} catch (error) {
|
|
5086
|
+
return fail(error);
|
|
5087
|
+
}
|
|
5088
|
+
if (commit instanceof Error) return fail(commit);
|
|
5089
|
+
try {
|
|
5090
|
+
return await this.commitPurge(params, commit);
|
|
5091
|
+
} catch (error) {
|
|
5092
|
+
return fail(error);
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
/** Preconditions 1-5, then the marker this host signs. */
|
|
5096
|
+
async preparePurgeJob(params, purger) {
|
|
5097
|
+
const { job, stores, signal } = params;
|
|
5098
|
+
const documentId = job.documentId;
|
|
5099
|
+
if (!job.purge) return /* @__PURE__ */ new Error(`Purge job ${job.id} carries no purge options`);
|
|
5100
|
+
const held = await this.heldStreams(purger, documentId);
|
|
5101
|
+
if (held.branches.length === 0) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not held here`);
|
|
5102
|
+
let documentType;
|
|
5103
|
+
for (const branch of held.branches) {
|
|
5104
|
+
let meta;
|
|
5105
|
+
try {
|
|
5106
|
+
meta = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
5107
|
+
} catch (error) {
|
|
5108
|
+
return new DocumentNotDeletedError(documentId, `Document ${documentId} has no readable state on ${branch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5109
|
+
}
|
|
5110
|
+
if (!meta.state.isDeleted) return new DocumentNotDeletedError(documentId, `Document ${documentId} is not deleted on branch ${branch}`);
|
|
5111
|
+
documentType ??= meta.documentType;
|
|
5112
|
+
}
|
|
5113
|
+
if (documentType === void 0) return new DocumentNotDeletedError(documentId);
|
|
5114
|
+
if (documentType === groupDocumentType) {
|
|
5115
|
+
const referencers = await purger.groupReferencersInHistory(documentId);
|
|
5116
|
+
if (referencers.length > 0) return new GroupInUseError(documentId, referencers);
|
|
5117
|
+
}
|
|
5118
|
+
let survivors = [];
|
|
5119
|
+
if (this.driveContainerTypes.has(documentType)) {
|
|
5120
|
+
const requested = new Set(purgeRequestDocumentIds(job));
|
|
5121
|
+
const members = await this.driveMembers(purger, stores, documentId, held.branches);
|
|
5122
|
+
const blocking = members.required.filter((id) => !requested.has(id));
|
|
5123
|
+
if (blocking.length > 0) return new DocumentNotDeletedError(documentId, `Drive ${documentId} cannot be purged: ${blocking.join(", ")} neither purged nor in the request`);
|
|
5124
|
+
survivors = members.survivors;
|
|
5125
|
+
}
|
|
5126
|
+
const operationCount = await purger.operationCount(documentId);
|
|
5127
|
+
if (operationCount > this.config.maxPurgeOperations && !job.purge.allowLarge) return new PurgeTooLargeError(documentId, operationCount, this.config.maxPurgeOperations);
|
|
5128
|
+
return {
|
|
5129
|
+
purger,
|
|
5130
|
+
marker: await this.signMarker(documentId, documentType, job.purge.requestId, signal),
|
|
5131
|
+
documentType,
|
|
5132
|
+
held,
|
|
5133
|
+
sourceRemote: "",
|
|
5134
|
+
collectionIds: [],
|
|
5135
|
+
survivors
|
|
5136
|
+
};
|
|
5137
|
+
}
|
|
5138
|
+
/** A peer's marker, admitted alone; a receiver cannot refuse an erasure. */
|
|
5139
|
+
async preparePurgeLoad(params, purger) {
|
|
5140
|
+
const { job, stores, signal } = params;
|
|
5141
|
+
const documentId = job.documentId;
|
|
5142
|
+
const marker = job.operations.find((operation) => isPurgeMarker(operation));
|
|
5143
|
+
const refusal = await this.signatureAdmission.admitMarker(job, marker, signal);
|
|
5144
|
+
if (refusal) return refusal;
|
|
5145
|
+
const held = await this.heldStreams(purger, documentId);
|
|
5146
|
+
let documentType = marker.action.input.documentType;
|
|
5147
|
+
let appliedDeletion = held.branches.length > 0;
|
|
5148
|
+
if (held.branches.length > 0) try {
|
|
5149
|
+
const meta = await stores.documentMetaCache.getDocumentMeta(documentId, held.branches.includes("main") ? "main" : held.branches[0], signal);
|
|
5150
|
+
documentType = meta.documentType;
|
|
5151
|
+
appliedDeletion = !meta.state.isDeleted;
|
|
5152
|
+
} catch {}
|
|
5153
|
+
let survivors = [];
|
|
5154
|
+
if (this.driveContainerTypes.has(documentType)) {
|
|
5155
|
+
const members = await this.driveMembers(purger, stores, documentId, held.branches);
|
|
5156
|
+
survivors = [...members.required, ...members.survivors];
|
|
5157
|
+
}
|
|
5158
|
+
const sourceRemote = typeof job.meta.sourceRemote === "string" ? job.meta.sourceRemote : "";
|
|
5159
|
+
const collectionId = sourceRemote ? await purger.remoteCollection(sourceRemote) : void 0;
|
|
5160
|
+
return {
|
|
5161
|
+
purger,
|
|
5162
|
+
marker: purgeMarkerOperation(marker.action),
|
|
5163
|
+
documentType,
|
|
5164
|
+
held,
|
|
5165
|
+
sourceRemote,
|
|
5166
|
+
collectionIds: collectionId === void 0 ? [] : [collectionId],
|
|
5167
|
+
survivors,
|
|
5168
|
+
...appliedDeletion ? { appliedDeletion: true } : {}
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
/** Execution: delete, write the marker and its twin, tombstone, reopen. */
|
|
5172
|
+
async commitPurge(params, commit) {
|
|
5173
|
+
const { job, startTime, stores, signal, touchedStreams } = params;
|
|
5174
|
+
const { purger, marker, documentType, held, sourceRemote } = commit;
|
|
5175
|
+
const documentId = job.documentId;
|
|
5176
|
+
const streams = [...held.streams, {
|
|
5177
|
+
scope: "document",
|
|
5178
|
+
branch: "main"
|
|
5179
|
+
}];
|
|
5180
|
+
for (const stream of streams) {
|
|
5181
|
+
touchedStreams.add(documentId, stream.scope, stream.branch);
|
|
5182
|
+
params.postCommitInvalidations.push({
|
|
5183
|
+
documentId,
|
|
5184
|
+
...stream
|
|
5185
|
+
});
|
|
5186
|
+
}
|
|
5187
|
+
params.postCommitMetaInvalidations.push(documentId);
|
|
5188
|
+
const removedRows = await purger.deleteRows(documentId);
|
|
5189
|
+
const [stored] = await stores.operationStore.apply(documentId, documentType, "document", "main", 0, (txn) => {
|
|
5190
|
+
txn.addOperations(marker);
|
|
5191
|
+
}, signal);
|
|
5192
|
+
const indexTxn = stores.operationIndex.start();
|
|
5193
|
+
indexTxn.write([{
|
|
5194
|
+
...stored,
|
|
5195
|
+
documentId,
|
|
5196
|
+
documentType,
|
|
5197
|
+
scope: "document",
|
|
5198
|
+
branch: "main",
|
|
5199
|
+
sourceRemote
|
|
5200
|
+
}]);
|
|
5201
|
+
const [ordinal] = await stores.operationIndex.commit(indexTxn, signal);
|
|
5202
|
+
await purger.writeTombstone({
|
|
5203
|
+
documentId,
|
|
5204
|
+
ordinal,
|
|
5205
|
+
removedRows,
|
|
5206
|
+
purgedAtUtc: marker.action.input.purgedAtUtcIso,
|
|
5207
|
+
requestId: marker.action.input.requestId
|
|
5208
|
+
});
|
|
5209
|
+
const collections = await purger.reopenMemberships(documentId, ordinal, commit.collectionIds);
|
|
5210
|
+
params.postCommitMembershipInvalidations.push(documentId, ...commit.survivors, ...indexTxn.getMembershipInvalidations());
|
|
5211
|
+
const operationWithContext = {
|
|
5212
|
+
operation: stored,
|
|
5213
|
+
context: {
|
|
5214
|
+
documentId,
|
|
5215
|
+
scope: "document",
|
|
5216
|
+
branch: "main",
|
|
5217
|
+
documentType,
|
|
5218
|
+
ordinal,
|
|
5219
|
+
...commit.appliedDeletion ? { appliedDeletion: true } : {}
|
|
5220
|
+
}
|
|
5221
|
+
};
|
|
5222
|
+
return {
|
|
5223
|
+
result: {
|
|
5224
|
+
job,
|
|
5225
|
+
success: true,
|
|
5226
|
+
operations: [stored],
|
|
5227
|
+
operationsWithContext: [operationWithContext],
|
|
5228
|
+
duration: Date.now() - startTime
|
|
5229
|
+
},
|
|
5230
|
+
pendingEvent: {
|
|
5231
|
+
jobId: job.id,
|
|
5232
|
+
operations: [operationWithContext],
|
|
5233
|
+
jobMeta: job.meta,
|
|
5234
|
+
collectionMemberships: { [documentId]: collections }
|
|
5235
|
+
}
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
async heldStreams(purger, documentId) {
|
|
5239
|
+
const streams = await purger.streams(documentId);
|
|
5240
|
+
return {
|
|
5241
|
+
streams,
|
|
5242
|
+
branches: [...new Set(streams.filter((stream) => stream.scope === "document").map((stream) => stream.branch))]
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
/** Members a drive purge needs gone, and those living on elsewhere. */
|
|
5246
|
+
async driveMembers(purger, stores, driveId, branches) {
|
|
5247
|
+
const required = /* @__PURE__ */ new Set();
|
|
5248
|
+
const survivors = /* @__PURE__ */ new Set();
|
|
5249
|
+
for (const branch of branches.length > 0 ? branches : ["main"]) {
|
|
5250
|
+
const collectionId = DriveCollectionId.forDrive(driveId, branch).key;
|
|
5251
|
+
const members = await purger.collectionMembers(collectionId, driveId, branch);
|
|
5252
|
+
const purged = await stores.documentLocks.purged(members.map((member) => member.documentId));
|
|
5253
|
+
for (const member of members) {
|
|
5254
|
+
if (purged.has(member.documentId)) continue;
|
|
5255
|
+
if (member.required) required.add(member.documentId);
|
|
5256
|
+
else survivors.add(member.documentId);
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
return {
|
|
5260
|
+
required: [...required],
|
|
5261
|
+
survivors: [...survivors]
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
/** The marker, signed by this executor's signer as any peer admits it. */
|
|
5265
|
+
async signMarker(documentId, documentType, requestId, signal) {
|
|
5266
|
+
const action = purgeDocumentAction({
|
|
5267
|
+
documentId,
|
|
5268
|
+
documentType,
|
|
5269
|
+
requestId
|
|
5270
|
+
});
|
|
5271
|
+
const signature = await this.signer.signAction(action, {
|
|
5272
|
+
documentId,
|
|
5273
|
+
branch: "main"
|
|
5274
|
+
}, signal);
|
|
5275
|
+
return purgeMarkerOperation({
|
|
5276
|
+
...action,
|
|
5277
|
+
context: { signer: {
|
|
5278
|
+
...actionSignerIdentity(this.signer),
|
|
5279
|
+
signatures: [signature]
|
|
5280
|
+
} }
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
/** A stored document at a version this reactor does not run is read-only here. */
|
|
5284
|
+
async unsupportedStoredProtocol(job, stores, signal) {
|
|
5285
|
+
let versions;
|
|
5286
|
+
try {
|
|
5287
|
+
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;
|
|
5288
|
+
} catch {
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
5291
|
+
return this.documentActionHandler.unsupportedProtocol(job.documentId, versions);
|
|
5292
|
+
}
|
|
5293
|
+
/**
|
|
5294
|
+
* A successful mutation's result and the write-ready event it owes, carrying
|
|
5295
|
+
* what earlier attempts committed ahead of what this one wrote.
|
|
5296
|
+
*/
|
|
5297
|
+
async committedOutcome(job, reloaded, written, generated, stores, startTime) {
|
|
5298
|
+
const operationsWithContext = [...reloaded, ...written];
|
|
5299
|
+
let pendingEvent;
|
|
5300
|
+
if (operationsWithContext.length > 0) {
|
|
5301
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(operationsWithContext, stores);
|
|
3468
5302
|
pendingEvent = {
|
|
3469
5303
|
jobId: job.id,
|
|
3470
|
-
operations:
|
|
5304
|
+
operations: operationsWithContext,
|
|
3471
5305
|
jobMeta: job.meta,
|
|
5306
|
+
submittedActionIds: submittedActionIds(job),
|
|
3472
5307
|
collectionMemberships
|
|
3473
5308
|
};
|
|
3474
5309
|
}
|
|
@@ -3476,14 +5311,62 @@ var SimpleJobExecutor = class {
|
|
|
3476
5311
|
result: {
|
|
3477
5312
|
job,
|
|
3478
5313
|
success: true,
|
|
3479
|
-
operations:
|
|
3480
|
-
operationsWithContext
|
|
5314
|
+
operations: [...reloaded.map((entry) => entry.operation), ...generated],
|
|
5315
|
+
operationsWithContext,
|
|
3481
5316
|
duration: Date.now() - startTime
|
|
3482
5317
|
},
|
|
3483
5318
|
pendingEvent
|
|
3484
5319
|
};
|
|
3485
5320
|
}
|
|
3486
5321
|
/**
|
|
5322
|
+
* The stored operations of writes an earlier attempt committed, as a fresh
|
|
5323
|
+
* write would report them. State is rebuilt from the write cache, and left
|
|
5324
|
+
* out when it cannot be.
|
|
5325
|
+
*/
|
|
5326
|
+
async reloadCommitted(writes, stores, signal) {
|
|
5327
|
+
const streams = /* @__PURE__ */ new Map();
|
|
5328
|
+
for (const write of writes) {
|
|
5329
|
+
const key = `${write.documentId}\u0000${write.scope}\u0000${write.branch}`;
|
|
5330
|
+
streams.set(key, [...streams.get(key) ?? [], write]);
|
|
5331
|
+
}
|
|
5332
|
+
const reloaded = [];
|
|
5333
|
+
for (const group of streams.values()) {
|
|
5334
|
+
const { documentId, scope, branch } = group[0];
|
|
5335
|
+
const opIds = [...new Set(group.map((write) => write.opId))];
|
|
5336
|
+
const operations = await stores.operationStore.getOperationsByIds(documentId, scope, branch, opIds, signal);
|
|
5337
|
+
const ordinals = await stores.operationIndex.getOrdinalsByOpIds(documentId, scope, branch, opIds, signal);
|
|
5338
|
+
const { documentType } = await stores.documentMetaCache.getDocumentMeta(documentId, branch, signal);
|
|
5339
|
+
for (const operation of operations.sort((a, b) => a.index - b.index)) reloaded.push({
|
|
5340
|
+
operation,
|
|
5341
|
+
context: {
|
|
5342
|
+
documentId,
|
|
5343
|
+
scope,
|
|
5344
|
+
branch,
|
|
5345
|
+
documentType,
|
|
5346
|
+
resultingState: await this.rebuiltState(stores, {
|
|
5347
|
+
documentId,
|
|
5348
|
+
scope,
|
|
5349
|
+
branch
|
|
5350
|
+
}, operation, signal),
|
|
5351
|
+
ordinal: ordinals.get(operation.id) ?? 0
|
|
5352
|
+
}
|
|
5353
|
+
});
|
|
5354
|
+
}
|
|
5355
|
+
return reloaded;
|
|
5356
|
+
}
|
|
5357
|
+
async rebuiltState(stores, stream, operation, signal) {
|
|
5358
|
+
try {
|
|
5359
|
+
const document = await stores.writeCache.getState(stream.documentId, stream.scope, stream.branch, operation.index, signal);
|
|
5360
|
+
return JSON.stringify({
|
|
5361
|
+
...document.state,
|
|
5362
|
+
header: document.header
|
|
5363
|
+
});
|
|
5364
|
+
} catch (error) {
|
|
5365
|
+
this.logger.warn("No state for committed operation @OperationId in @Stream: @Error", operation.id, stream, error);
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
/**
|
|
3487
5370
|
* Drops the cached state of every stream a job wrote, after its transaction
|
|
3488
5371
|
* did not commit.
|
|
3489
5372
|
*
|
|
@@ -3512,16 +5395,6 @@ var SimpleJobExecutor = class {
|
|
|
3512
5395
|
const actions = writes.map((write) => write.action);
|
|
3513
5396
|
const generatedOperations = [];
|
|
3514
5397
|
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
5398
|
for (const action of actions) if (action.timestampUtcMs && !isValidISOTimestamp(action.timestampUtcMs)) return {
|
|
3526
5399
|
success: false,
|
|
3527
5400
|
generatedOperations,
|
|
@@ -3619,7 +5492,7 @@ var SimpleJobExecutor = class {
|
|
|
3619
5492
|
if (docMeta.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3620
5493
|
documentVersion = docMeta.state.version;
|
|
3621
5494
|
}
|
|
3622
|
-
if (isUndoRedo(action) ||
|
|
5495
|
+
if (isUndoRedo(action) || skip > 0) stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3623
5496
|
let document;
|
|
3624
5497
|
if (baseDocument !== void 0) document = baseDocument;
|
|
3625
5498
|
else try {
|
|
@@ -3679,18 +5552,27 @@ var SimpleJobExecutor = class {
|
|
|
3679
5552
|
branch: job.branch,
|
|
3680
5553
|
protocolVersion
|
|
3681
5554
|
};
|
|
3682
|
-
updatedDocument = module.reducer(document, action, void 0, reducerOptions);
|
|
5555
|
+
updatedDocument = withProtocolVersions(module.reducer(document, action, void 0, reducerOptions), document.header.protocolVersions);
|
|
3683
5556
|
} catch (error) {
|
|
3684
5557
|
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
5558
|
const enhancedError = new Error(contextMessage);
|
|
3686
5559
|
if (error instanceof Error && error.stack) enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
3687
5560
|
return buildErrorResult(job, enhancedError, startTime);
|
|
3688
5561
|
}
|
|
3689
|
-
const scope = job.scope;
|
|
3690
|
-
const operations = updatedDocument.operations[scope];
|
|
3691
|
-
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
3692
|
-
const newOperation = operations[operations.length - 1];
|
|
3693
|
-
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
5562
|
+
const scope = job.scope;
|
|
5563
|
+
const operations = updatedDocument.operations[scope];
|
|
5564
|
+
if (operations.length === 0) return buildErrorResult(job, /* @__PURE__ */ new Error("No operation generated from action"), startTime);
|
|
5565
|
+
const newOperation = operations[operations.length - 1];
|
|
5566
|
+
if (!isUndoRedo(action)) newOperation.skip = skip;
|
|
5567
|
+
if (deniedReason === void 0 && sourceOperation === void 0 && !executing.replayingAcceptedHistory && isSynthesized(action, newOperation, document.operations[scope] ?? [])) try {
|
|
5568
|
+
await signSynthesized(newOperation, action, {
|
|
5569
|
+
documentId: job.documentId,
|
|
5570
|
+
scope,
|
|
5571
|
+
branch: job.branch
|
|
5572
|
+
}, this.signer, signal);
|
|
5573
|
+
} catch (error) {
|
|
5574
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
5575
|
+
}
|
|
3694
5576
|
const resultingState = JSON.stringify({
|
|
3695
5577
|
...updatedDocument.state,
|
|
3696
5578
|
header: updatedDocument.header
|
|
@@ -3791,7 +5673,7 @@ var SimpleJobExecutor = class {
|
|
|
3791
5673
|
* apply and its own reasons for it.
|
|
3792
5674
|
* - A positional or replayed run carries skips and re-appended operations,
|
|
3793
5675
|
* whose indices are not a simple ascending run from the head.
|
|
3794
|
-
* - UNDO, REDO
|
|
5676
|
+
* - UNDO, REDO and NOOP-with-skip each invalidate the write cache to
|
|
3795
5677
|
* force a full-history rebuild, so they cannot be reduced against state
|
|
3796
5678
|
* threaded from the write before them.
|
|
3797
5679
|
* - The auth scope decides later writes against the policy earlier ones
|
|
@@ -3809,7 +5691,7 @@ var SimpleJobExecutor = class {
|
|
|
3809
5691
|
if (scope === "auth" || scope === "document") return false;
|
|
3810
5692
|
return writes.every((write) => {
|
|
3811
5693
|
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 !== "
|
|
5694
|
+
return write.skip === 0 && write.deniedReason === void 0 && write.sourceOperation === void 0 && !DOCUMENT_SCOPE_ACTIONS.has(type) && !isUndoRedo(write.action) && type !== "NOOP";
|
|
3813
5695
|
});
|
|
3814
5696
|
}
|
|
3815
5697
|
/**
|
|
@@ -3932,13 +5814,22 @@ var SimpleJobExecutor = class {
|
|
|
3932
5814
|
}
|
|
3933
5815
|
if (!backdated) return plain();
|
|
3934
5816
|
const conflicting = (await stores.operationStore.getConflicting(job.documentId, job.scope, job.branch, earliest, void 0, signal)).results.filter((operation) => !isGenesisOperation(operation));
|
|
3935
|
-
|
|
5817
|
+
const nothingToMove = async () => {
|
|
3936
5818
|
if (!this.featureFlags.authEnforcement) return plain();
|
|
3937
5819
|
return this.evaluatePositioned(job, stores, this.appendedOperations(job, revisions.revision[job.scope] ?? 0), signal);
|
|
3938
|
-
}
|
|
5820
|
+
};
|
|
5821
|
+
if (conflicting.length === 0) return nothingToMove();
|
|
3939
5822
|
const nextIndex = revisions.revision[job.scope] ?? 0;
|
|
3940
5823
|
let firstConflicting = conflicting[0].index;
|
|
3941
5824
|
for (const operation of conflicting) if (operation.index < firstConflicting) firstConflicting = operation.index;
|
|
5825
|
+
const stored = (await stores.operationStore.getSince(job.documentId, job.scope, job.branch, firstConflicting - 1, void 0, void 0, signal)).results;
|
|
5826
|
+
const conflictingIds = new Set(conflicting.map((operation) => operation.id));
|
|
5827
|
+
const effective = garbageCollect(sortOperations(stored)).filter((operation) => !isGenesisOperation(operation));
|
|
5828
|
+
const firstMoving = effective.findIndex((operation) => conflictingIds.has(operation.id));
|
|
5829
|
+
const moving = firstMoving === -1 ? [] : effective.slice(firstMoving);
|
|
5830
|
+
if (moving.length === 0) return nothingToMove();
|
|
5831
|
+
let firstRetracted = moving[0].index;
|
|
5832
|
+
for (const operation of moving) firstRetracted = Math.min(firstRetracted, operation.index - operation.skip);
|
|
3942
5833
|
const incoming = job.actions.map((action, i) => ({
|
|
3943
5834
|
id: action.id,
|
|
3944
5835
|
index: nextIndex + i,
|
|
@@ -3949,8 +5840,8 @@ var SimpleJobExecutor = class {
|
|
|
3949
5840
|
}));
|
|
3950
5841
|
const merged = reshuffleByTimestamp({
|
|
3951
5842
|
index: nextIndex,
|
|
3952
|
-
skip: retractionSkip(nextIndex,
|
|
3953
|
-
},
|
|
5843
|
+
skip: retractionSkip(nextIndex, firstRetracted)
|
|
5844
|
+
}, moving, incoming);
|
|
3954
5845
|
stores.writeCache.invalidate(job.documentId, job.scope, job.branch);
|
|
3955
5846
|
if (!this.featureFlags.authEnforcement) return {
|
|
3956
5847
|
writes: merged.map((operation) => ({
|
|
@@ -4098,7 +5989,7 @@ var SimpleJobExecutor = class {
|
|
|
4098
5989
|
stores.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
4099
5990
|
const result = await this.processActions(tail.map((operation, i) => ({
|
|
4100
5991
|
action: operation.action,
|
|
4101
|
-
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index) : 0,
|
|
5992
|
+
skip: i === 0 ? retractionSkip(nextIndex, tail[0].index - tail[0].skip) : 0,
|
|
4102
5993
|
sourceRemote: "",
|
|
4103
5994
|
deniedReason: reevaluated[firstChange + i]
|
|
4104
5995
|
})), {
|
|
@@ -4187,36 +6078,32 @@ var SimpleJobExecutor = class {
|
|
|
4187
6078
|
error: new InvalidOperationTimestampError(job.documentId, scope, operation.timestampUtcMs, `operation (index: ${operation.index})`),
|
|
4188
6079
|
duration: Date.now() - startTime
|
|
4189
6080
|
};
|
|
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));
|
|
6081
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
6082
|
+
const admitted = /* @__PURE__ */ new Set();
|
|
6083
|
+
let selection = await this.selectLoadWrites(job, job.operations, stores, signal);
|
|
6084
|
+
for (;;) {
|
|
6085
|
+
const unadmitted = selection.incomingOpsToApply.filter((operation) => !admitted.has(operation));
|
|
6086
|
+
if (unadmitted.length === 0) break;
|
|
6087
|
+
let refused;
|
|
4206
6088
|
try {
|
|
4207
|
-
|
|
4208
|
-
} catch {
|
|
4209
|
-
|
|
6089
|
+
refused = await this.signatureAdmission.admitLoad(job, unadmitted, stores, signal);
|
|
6090
|
+
} catch (error) {
|
|
6091
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
4210
6092
|
}
|
|
6093
|
+
for (const operation of unadmitted) if (refused.has(operation)) dropped.add(operation);
|
|
6094
|
+
else admitted.add(operation);
|
|
6095
|
+
if (refused.size === 0) break;
|
|
6096
|
+
const remaining = job.operations.filter((operation) => !dropped.has(operation));
|
|
6097
|
+
if (remaining.length === 0) return {
|
|
6098
|
+
job,
|
|
6099
|
+
success: true,
|
|
6100
|
+
operations: [],
|
|
6101
|
+
operationsWithContext: [],
|
|
6102
|
+
duration: Date.now() - startTime
|
|
6103
|
+
};
|
|
6104
|
+
selection = await this.selectLoadWrites(job, remaining, stores, signal);
|
|
4211
6105
|
}
|
|
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
|
-
});
|
|
6106
|
+
const { nonSupersededOps, allOpsFromMinConflictingIndex, incomingOpsToApply } = selection;
|
|
4220
6107
|
const existingOpsToReshuffle = monotonicAuthStream ? [] : nonSupersededOps.filter((operation) => !isGenesisOperation(operation));
|
|
4221
6108
|
const actionIdCounts = /* @__PURE__ */ new Map();
|
|
4222
6109
|
for (const operation of allOpsFromMinConflictingIndex) actionIdCounts.set(operation.action.id, (actionIdCounts.get(operation.action.id) ?? 0) + 1);
|
|
@@ -4237,14 +6124,6 @@ var SimpleJobExecutor = class {
|
|
|
4237
6124
|
const logicalSkip = latestRevision - minLogicalIndex;
|
|
4238
6125
|
if (logicalSkip > skipCount) skipCount = logicalSkip;
|
|
4239
6126
|
}
|
|
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
6127
|
if (incomingOpsToApply.length === 0) return {
|
|
4249
6128
|
job,
|
|
4250
6129
|
success: true,
|
|
@@ -4272,7 +6151,8 @@ var SimpleJobExecutor = class {
|
|
|
4272
6151
|
...operation,
|
|
4273
6152
|
id: operation.id
|
|
4274
6153
|
})));
|
|
4275
|
-
|
|
6154
|
+
const incomingIds = new Set(incomingOpsToApply.map((operation) => operation.action.id));
|
|
6155
|
+
for (const operation of reshuffledOperations) if (operation.action.type === "NOOP" && operation.skip === 0 && incomingIds.has(operation.action.id)) operation.skip = 1;
|
|
4276
6156
|
let deniedReasons;
|
|
4277
6157
|
if (this.featureFlags.documentDecisions) try {
|
|
4278
6158
|
deniedReasons = await evaluateByPosition(this.decisionModel, {
|
|
@@ -4324,6 +6204,71 @@ var SimpleJobExecutor = class {
|
|
|
4324
6204
|
duration: Date.now() - startTime
|
|
4325
6205
|
};
|
|
4326
6206
|
}
|
|
6207
|
+
/** The conflicting window a load opens, and which of its operations are new. */
|
|
6208
|
+
async selectLoadWrites(job, operations, stores, signal) {
|
|
6209
|
+
const scope = job.scope;
|
|
6210
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
6211
|
+
let minIncomingTimestamp = operations[0]?.timestampUtcMs || "";
|
|
6212
|
+
for (const operation of operations) {
|
|
6213
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
6214
|
+
const ts = operation.timestampUtcMs || "";
|
|
6215
|
+
if (Date.parse(ts) < Date.parse(minIncomingTimestamp)) minIncomingTimestamp = ts;
|
|
6216
|
+
}
|
|
6217
|
+
let conflictingOps;
|
|
6218
|
+
try {
|
|
6219
|
+
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
6220
|
+
} catch {
|
|
6221
|
+
conflictingOps = [];
|
|
6222
|
+
}
|
|
6223
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
6224
|
+
if (conflictingOps.length > 0) {
|
|
6225
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
6226
|
+
try {
|
|
6227
|
+
allOpsFromMinConflictingIndex = (await stores.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, void 0, void 0, signal)).results;
|
|
6228
|
+
} catch {
|
|
6229
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
const incomingActionIds = new Set(operations.map((op) => op.action.id));
|
|
6233
|
+
const supersededBy = (op) => {
|
|
6234
|
+
let nearest;
|
|
6235
|
+
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;
|
|
6236
|
+
return nearest;
|
|
6237
|
+
};
|
|
6238
|
+
const undoneActionIds = /* @__PURE__ */ new Set();
|
|
6239
|
+
let predecessorBound = minIncomingIndex;
|
|
6240
|
+
const rowsByActionId = /* @__PURE__ */ new Map();
|
|
6241
|
+
for (const op of conflictingOps) if (incomingActionIds.has(op.action.id)) rowsByActionId.set(op.action.id, [...rowsByActionId.get(op.action.id) ?? [], op]);
|
|
6242
|
+
const liveOps = /* @__PURE__ */ new Set();
|
|
6243
|
+
for (const op of conflictingOps) if (supersededBy(op) === void 0) liveOps.add(op);
|
|
6244
|
+
for (const [actionId, rows] of rowsByActionId) {
|
|
6245
|
+
if (rows.some((row) => liveOps.has(row))) continue;
|
|
6246
|
+
if (rows.some((row) => supersededBy(row)?.action.type === "NOOP")) undoneActionIds.add(actionId);
|
|
6247
|
+
else predecessorBound = Math.min(predecessorBound, Math.max(...rows.map((row) => row.index)));
|
|
6248
|
+
}
|
|
6249
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
6250
|
+
if (op.index < predecessorBound && !incomingActionIds.has(op.action.id)) return false;
|
|
6251
|
+
return liveOps.has(op);
|
|
6252
|
+
});
|
|
6253
|
+
let rewoundFrom = Number.POSITIVE_INFINITY;
|
|
6254
|
+
for (const op of nonSupersededOps) if (!isGenesisOperation(op)) rewoundFrom = Math.min(rewoundFrom, op.index - op.skip);
|
|
6255
|
+
const reappended = new Set(nonSupersededOps.map((op) => op.index));
|
|
6256
|
+
for (const op of allOpsFromMinConflictingIndex) if (op.index >= rewoundFrom && !reappended.has(op.index) && supersededBy(op) === void 0) nonSupersededOps.push(op);
|
|
6257
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
6258
|
+
const seenIncomingActionIds = /* @__PURE__ */ new Set();
|
|
6259
|
+
const incomingOpsToApply = operations.filter((op) => {
|
|
6260
|
+
if (existingActionIds.has(op.action.id)) return false;
|
|
6261
|
+
if (undoneActionIds.has(op.action.id)) return false;
|
|
6262
|
+
if (seenIncomingActionIds.has(op.action.id)) return false;
|
|
6263
|
+
seenIncomingActionIds.add(op.action.id);
|
|
6264
|
+
return true;
|
|
6265
|
+
});
|
|
6266
|
+
return {
|
|
6267
|
+
nonSupersededOps,
|
|
6268
|
+
allOpsFromMinConflictingIndex,
|
|
6269
|
+
incomingOpsToApply
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
4327
6272
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
4328
6273
|
if (!result.success) return result;
|
|
4329
6274
|
if (result.operations && result.operations.length > 0) generatedOperations.push(...result.operations);
|
|
@@ -4470,6 +6415,7 @@ var DocumentModelRegistry = class {
|
|
|
4470
6415
|
//#endregion
|
|
4471
6416
|
//#region src/storage/kysely/keyframe-store.ts
|
|
4472
6417
|
var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
6418
|
+
db;
|
|
4473
6419
|
trx;
|
|
4474
6420
|
constructor(db) {
|
|
4475
6421
|
this.db = db;
|
|
@@ -4484,19 +6430,28 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
|
4484
6430
|
}
|
|
4485
6431
|
async putKeyframe(documentId, scope, branch, revision, document, signal) {
|
|
4486
6432
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
4487
|
-
|
|
4488
|
-
documentId,
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
6433
|
+
const write = async (trx) => {
|
|
6434
|
+
await acquirePurgeLocks(trx, [documentId], "shared");
|
|
6435
|
+
if ((await findPurged(trx, [documentId])).has(documentId)) return;
|
|
6436
|
+
await trx.insertInto("Keyframe").values({
|
|
6437
|
+
documentId,
|
|
6438
|
+
documentType: document.header.documentType,
|
|
6439
|
+
scope,
|
|
6440
|
+
branch,
|
|
6441
|
+
revision,
|
|
6442
|
+
document
|
|
6443
|
+
}).onConflict((oc) => oc.columns([
|
|
6444
|
+
"documentId",
|
|
6445
|
+
"scope",
|
|
6446
|
+
"branch",
|
|
6447
|
+
"revision"
|
|
6448
|
+
]).doUpdateSet({ document })).execute();
|
|
6449
|
+
};
|
|
6450
|
+
if (this.trx) {
|
|
6451
|
+
await write(this.trx);
|
|
6452
|
+
return;
|
|
6453
|
+
}
|
|
6454
|
+
await this.db.transaction().execute(write);
|
|
4500
6455
|
}
|
|
4501
6456
|
async findNearestKeyframe(documentId, scope, branch, targetRevision, signal) {
|
|
4502
6457
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
@@ -4554,6 +6509,11 @@ function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
|
4554
6509
|
//#endregion
|
|
4555
6510
|
//#region src/storage/txn.ts
|
|
4556
6511
|
var AtomicTransaction = class {
|
|
6512
|
+
documentId;
|
|
6513
|
+
documentType;
|
|
6514
|
+
scope;
|
|
6515
|
+
branch;
|
|
6516
|
+
baseRevision;
|
|
4557
6517
|
operations = [];
|
|
4558
6518
|
constructor(documentId, documentType, scope, branch, baseRevision) {
|
|
4559
6519
|
this.documentId = documentId;
|
|
@@ -4587,6 +6547,11 @@ var AtomicTransaction = class {
|
|
|
4587
6547
|
//#endregion
|
|
4588
6548
|
//#region src/storage/kysely/store.ts
|
|
4589
6549
|
var _UniqueConstraintContext = class extends Error {
|
|
6550
|
+
documentId;
|
|
6551
|
+
scope;
|
|
6552
|
+
branch;
|
|
6553
|
+
revision;
|
|
6554
|
+
stagedOps;
|
|
4590
6555
|
constructor(documentId, scope, branch, revision, stagedOps) {
|
|
4591
6556
|
super("unique constraint");
|
|
4592
6557
|
this.documentId = documentId;
|
|
@@ -4598,16 +6563,20 @@ var _UniqueConstraintContext = class extends Error {
|
|
|
4598
6563
|
}
|
|
4599
6564
|
};
|
|
4600
6565
|
var KyselyOperationStore = class KyselyOperationStore {
|
|
6566
|
+
db;
|
|
4601
6567
|
trx;
|
|
6568
|
+
liveIds;
|
|
4602
6569
|
constructor(db) {
|
|
4603
6570
|
this.db = db;
|
|
4604
6571
|
}
|
|
4605
6572
|
get queryExecutor() {
|
|
4606
6573
|
return this.trx ?? this.db;
|
|
4607
6574
|
}
|
|
4608
|
-
|
|
6575
|
+
/** `liveIds`: ids the transaction read untombstoned under its shared lock. */
|
|
6576
|
+
withTransaction(trx, liveIds) {
|
|
4609
6577
|
const instance = new KyselyOperationStore(this.db);
|
|
4610
6578
|
instance.trx = trx;
|
|
6579
|
+
instance.liveIds = liveIds;
|
|
4611
6580
|
return instance;
|
|
4612
6581
|
}
|
|
4613
6582
|
async apply(documentId, documentType, scope, branch, revision, fn, signal, condition) {
|
|
@@ -4652,6 +6621,8 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4652
6621
|
await fn(atomicTxn);
|
|
4653
6622
|
const operations = atomicTxn.getOperations();
|
|
4654
6623
|
if (operations.length === 0) return [];
|
|
6624
|
+
const purgedHead = this.liveIds?.has(documentId) ? void 0 : await this.refusePurgedAppend(trx, documentId, operations);
|
|
6625
|
+
if (purgedHead !== void 0) return purgedHead;
|
|
4655
6626
|
if (condition) await this.acquireStreamLocks(trx, documentId, scope, branch, condition);
|
|
4656
6627
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
4657
6628
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
@@ -4697,12 +6668,13 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4697
6668
|
* otherwise read a snapshot taken before the locks were held.
|
|
4698
6669
|
*/
|
|
4699
6670
|
async acquireStreamLocks(trx, documentId, scope, branch, condition) {
|
|
4700
|
-
const keys = new Set([`${documentId}:${scope}:${branch}`]);
|
|
6671
|
+
const keys = /* @__PURE__ */ new Set([`${documentId}:${scope}:${branch}`]);
|
|
4701
6672
|
for (const stream of condition.streams) keys.add(`${stream.documentId}:${stream.scope}:${stream.branch}`);
|
|
6673
|
+
const sortedKeys = sql.join([...keys].sort());
|
|
4702
6674
|
await sql`
|
|
4703
6675
|
with ordered as materialized (
|
|
4704
6676
|
select key
|
|
4705
|
-
from unnest(array[${
|
|
6677
|
+
from unnest(array[${sortedKeys}]::text[]) with ordinality as t(key, ord)
|
|
4706
6678
|
order by ord
|
|
4707
6679
|
)
|
|
4708
6680
|
select pg_advisory_xact_lock(hashtext(key)) from ordered
|
|
@@ -4766,6 +6738,24 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4766
6738
|
}
|
|
4767
6739
|
return storedRows.map((row) => this.rowToOperation(row));
|
|
4768
6740
|
}
|
|
6741
|
+
/** A purged stream refuses appends; a marker gets the stored one back. */
|
|
6742
|
+
async refusePurgedAppend(trx, documentId, operations) {
|
|
6743
|
+
if (!(await findPurged(trx, [documentId])).has(documentId)) return;
|
|
6744
|
+
if (!operations.every((operation) => this.isMarker(operation))) throw new DocumentPurgedError(documentId);
|
|
6745
|
+
return (await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where(sql`action->>'type' = ${PURGE_DOCUMENT}`).orderBy("index", "asc").execute()).map((row) => this.rowToOperation(row));
|
|
6746
|
+
}
|
|
6747
|
+
isMarker(operation) {
|
|
6748
|
+
return this.actionType(operation) === PURGE_DOCUMENT;
|
|
6749
|
+
}
|
|
6750
|
+
actionType(operation) {
|
|
6751
|
+
let action = operation.action;
|
|
6752
|
+
if (typeof action === "string") try {
|
|
6753
|
+
action = JSON.parse(action);
|
|
6754
|
+
} catch {
|
|
6755
|
+
return;
|
|
6756
|
+
}
|
|
6757
|
+
if (typeof action === "object" && action !== null && "type" in action && typeof action.type === "string") return action.type;
|
|
6758
|
+
}
|
|
4769
6759
|
/** True when the staged write creates a document rather than appending to one. */
|
|
4770
6760
|
isCreate(operations) {
|
|
4771
6761
|
for (const operation of operations) {
|
|
@@ -4779,6 +6769,14 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4779
6769
|
}
|
|
4780
6770
|
return false;
|
|
4781
6771
|
}
|
|
6772
|
+
/**
|
|
6773
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
6774
|
+
* last row returned), not the last row's own index. This keeps "0" an
|
|
6775
|
+
* unambiguous start-of-stream sentinel even when a page ends at index 0
|
|
6776
|
+
* (e.g. `limit: 1` on a fresh stream), which would otherwise make
|
|
6777
|
+
* `nextCursor` equal the start cursor and loop a caller that walks pages
|
|
6778
|
+
* forever.
|
|
6779
|
+
*/
|
|
4782
6780
|
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
4783
6781
|
throwIfAborted(signal);
|
|
4784
6782
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
@@ -4793,10 +6791,10 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4793
6791
|
}
|
|
4794
6792
|
if (paging) {
|
|
4795
6793
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4796
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6794
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4797
6795
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4798
6796
|
}
|
|
4799
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
6797
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
4800
6798
|
cursor,
|
|
4801
6799
|
limit
|
|
4802
6800
|
}, signal));
|
|
@@ -4814,26 +6812,28 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4814
6812
|
limit
|
|
4815
6813
|
}, signal));
|
|
4816
6814
|
}
|
|
6815
|
+
/**
|
|
6816
|
+
* The paging cursor here encodes the index to resume from (one past the
|
|
6817
|
+
* last row returned), not the last row's own index, for the same reason as
|
|
6818
|
+
* `getSince`: a page ending at index 0 must not produce a cursor that is
|
|
6819
|
+
* indistinguishable from the start-of-stream sentinel.
|
|
6820
|
+
*/
|
|
4817
6821
|
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
4818
6822
|
throwIfAborted(signal);
|
|
4819
6823
|
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
6824
|
if (paging) {
|
|
4821
6825
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
4822
|
-
if (cursorValue > 0) query = query.where("index", "
|
|
6826
|
+
if (Number.isFinite(cursorValue) && cursorValue > 0) query = query.where("index", ">=", cursorValue);
|
|
4823
6827
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
4824
6828
|
}
|
|
4825
|
-
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
6829
|
+
return paginateRows(await query.execute(), paging, (row) => row.index + 1, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
4826
6830
|
cursor,
|
|
4827
6831
|
limit
|
|
4828
6832
|
}, signal));
|
|
4829
6833
|
}
|
|
4830
6834
|
async getRevisions(documentId, branch, signal) {
|
|
4831
6835
|
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();
|
|
6836
|
+
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
6837
|
const latest = await this.queryExecutor.selectFrom("Operation").select((eb) => eb.fn.max("timestampUtcMs").as("latestTimestamp")).where("documentId", "=", documentId).where("branch", "=", branch).executeTakeFirst();
|
|
4838
6838
|
const revision = {};
|
|
4839
6839
|
for (const row of scopeRevisions) revision[row.scope] = row.index + 1;
|
|
@@ -4846,6 +6846,20 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
4846
6846
|
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
6847
|
return latest?.latestTimestamp ? new Date(latest.latestTimestamp).toISOString() : void 0;
|
|
4848
6848
|
}
|
|
6849
|
+
async findOperationIds(documentId, scope, branch, opIds, signal) {
|
|
6850
|
+
throwIfAborted(signal);
|
|
6851
|
+
if (opIds.length === 0) return /* @__PURE__ */ new Set();
|
|
6852
|
+
const rows = await this.queryExecutor.selectFrom("Operation").select("opId").distinct().where("opId", "in", opIds).where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).execute();
|
|
6853
|
+
return new Set(rows.map((row) => row.opId));
|
|
6854
|
+
}
|
|
6855
|
+
async getOperationsByIds(documentId, scope, branch, opIds, signal) {
|
|
6856
|
+
throwIfAborted(signal);
|
|
6857
|
+
if (opIds.length === 0) return [];
|
|
6858
|
+
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();
|
|
6859
|
+
const latest = /* @__PURE__ */ new Map();
|
|
6860
|
+
for (const row of rows) latest.set(row.opId, this.rowToOperation(row));
|
|
6861
|
+
return [...latest.values()];
|
|
6862
|
+
}
|
|
4849
6863
|
rowToOperation(row) {
|
|
4850
6864
|
return {
|
|
4851
6865
|
index: row.index,
|
|
@@ -4939,8 +6953,8 @@ function createForwardingPoolInstrumentation(name) {
|
|
|
4939
6953
|
}
|
|
4940
6954
|
//#endregion
|
|
4941
6955
|
//#region src/storage/migrations/001_create_operation_table.ts
|
|
4942
|
-
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4943
|
-
async function up$
|
|
6956
|
+
var _001_create_operation_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$25 });
|
|
6957
|
+
async function up$25(db) {
|
|
4944
6958
|
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
6959
|
"documentId",
|
|
4946
6960
|
"scope",
|
|
@@ -4965,8 +6979,8 @@ async function up$18(db) {
|
|
|
4965
6979
|
}
|
|
4966
6980
|
//#endregion
|
|
4967
6981
|
//#region src/storage/migrations/002_create_keyframe_table.ts
|
|
4968
|
-
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4969
|
-
async function up$
|
|
6982
|
+
var _002_create_keyframe_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$24 });
|
|
6983
|
+
async function up$24(db) {
|
|
4970
6984
|
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
6985
|
"documentId",
|
|
4972
6986
|
"scope",
|
|
@@ -4982,14 +6996,14 @@ async function up$17(db) {
|
|
|
4982
6996
|
}
|
|
4983
6997
|
//#endregion
|
|
4984
6998
|
//#region src/storage/migrations/003_create_document_table.ts
|
|
4985
|
-
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
4986
|
-
async function up$
|
|
6999
|
+
var _003_create_document_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$23 });
|
|
7000
|
+
async function up$23(db) {
|
|
4987
7001
|
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
7002
|
}
|
|
4989
7003
|
//#endregion
|
|
4990
7004
|
//#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$
|
|
7005
|
+
var _004_create_document_relationship_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$22 });
|
|
7006
|
+
async function up$22(db) {
|
|
4993
7007
|
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
7008
|
"sourceId",
|
|
4995
7009
|
"targetId",
|
|
@@ -5001,14 +7015,14 @@ async function up$15(db) {
|
|
|
5001
7015
|
}
|
|
5002
7016
|
//#endregion
|
|
5003
7017
|
//#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$
|
|
7018
|
+
var _005_create_indexer_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$21 });
|
|
7019
|
+
async function up$21(db) {
|
|
5006
7020
|
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
7021
|
}
|
|
5008
7022
|
//#endregion
|
|
5009
7023
|
//#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$
|
|
7024
|
+
var _006_create_document_snapshot_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$20 });
|
|
7025
|
+
async function up$20(db) {
|
|
5012
7026
|
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
7027
|
"documentId",
|
|
5014
7028
|
"scope",
|
|
@@ -5029,8 +7043,8 @@ async function up$13(db) {
|
|
|
5029
7043
|
}
|
|
5030
7044
|
//#endregion
|
|
5031
7045
|
//#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$
|
|
7046
|
+
var _007_create_slug_mapping_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$19 });
|
|
7047
|
+
async function up$19(db) {
|
|
5034
7048
|
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
7049
|
"documentId",
|
|
5036
7050
|
"scope",
|
|
@@ -5040,14 +7054,14 @@ async function up$12(db) {
|
|
|
5040
7054
|
}
|
|
5041
7055
|
//#endregion
|
|
5042
7056
|
//#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$
|
|
7057
|
+
var _008_create_view_state_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$18 });
|
|
7058
|
+
async function up$18(db) {
|
|
5045
7059
|
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
7060
|
}
|
|
5047
7061
|
//#endregion
|
|
5048
7062
|
//#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$
|
|
7063
|
+
var _009_create_operation_index_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$17 });
|
|
7064
|
+
async function up$17(db) {
|
|
5051
7065
|
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
7066
|
await db.schema.createIndex("idx_document_collections_collectionId").on("document_collections").column("collectionId").execute();
|
|
5053
7067
|
await db.schema.createIndex("idx_doc_collections_collection_range").on("document_collections").columns(["collectionId", "joinedOrdinal"]).execute();
|
|
@@ -5061,8 +7075,8 @@ async function up$10(db) {
|
|
|
5061
7075
|
}
|
|
5062
7076
|
//#endregion
|
|
5063
7077
|
//#region src/storage/migrations/010_create_sync_tables.ts
|
|
5064
|
-
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$
|
|
5065
|
-
async function up$
|
|
7078
|
+
var _010_create_sync_tables_exports = /* @__PURE__ */ __exportAll({ up: () => up$16 });
|
|
7079
|
+
async function up$16(db) {
|
|
5066
7080
|
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
7081
|
await db.schema.createIndex("idx_sync_remotes_collection").on("sync_remotes").column("collection_id").execute();
|
|
5068
7082
|
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 +7084,8 @@ async function up$9(db) {
|
|
|
5070
7084
|
}
|
|
5071
7085
|
//#endregion
|
|
5072
7086
|
//#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$
|
|
7087
|
+
var _011_add_cursor_type_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$15 });
|
|
7088
|
+
async function up$15(db) {
|
|
5075
7089
|
await db.deleteFrom("sync_cursors").where("remote_name", "like", "outbox::%").execute();
|
|
5076
7090
|
await db.deleteFrom("sync_remotes").where("name", "like", "outbox::%").execute();
|
|
5077
7091
|
await db.schema.dropTable("sync_cursors").execute();
|
|
@@ -5080,64 +7094,64 @@ async function up$8(db) {
|
|
|
5080
7094
|
}
|
|
5081
7095
|
//#endregion
|
|
5082
7096
|
//#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$
|
|
7097
|
+
var _012_add_source_remote_column_exports = /* @__PURE__ */ __exportAll({ up: () => up$14 });
|
|
7098
|
+
async function up$14(db) {
|
|
5085
7099
|
await db.schema.alterTable("operation_index_operations").addColumn("sourceRemote", "text", (col) => col.notNull().defaultTo("")).execute();
|
|
5086
7100
|
}
|
|
5087
7101
|
//#endregion
|
|
5088
7102
|
//#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$
|
|
7103
|
+
var _013_create_sync_dead_letters_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$13 });
|
|
7104
|
+
async function up$13(db) {
|
|
5091
7105
|
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
7106
|
await db.schema.createIndex("idx_sync_dead_letters_remote").on("sync_dead_letters").column("remote_name").execute();
|
|
5093
7107
|
}
|
|
5094
7108
|
//#endregion
|
|
5095
7109
|
//#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$
|
|
7110
|
+
var _014_create_processor_cursor_table_exports = /* @__PURE__ */ __exportAll({ up: () => up$12 });
|
|
7111
|
+
async function up$12(db) {
|
|
5098
7112
|
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
7113
|
}
|
|
5100
7114
|
//#endregion
|
|
5101
7115
|
//#region src/storage/migrations/015_add_operation_denied_reason.ts
|
|
5102
7116
|
var _015_add_operation_denied_reason_exports = /* @__PURE__ */ __exportAll({
|
|
5103
|
-
down: () => down$
|
|
5104
|
-
up: () => up$
|
|
7117
|
+
down: () => down$11,
|
|
7118
|
+
up: () => up$11
|
|
5105
7119
|
});
|
|
5106
7120
|
/**
|
|
5107
7121
|
* Records why authorization refused an operation. Separate from `error` so a
|
|
5108
7122
|
* denial is distinguishable from a reducer failure without matching on a
|
|
5109
7123
|
* message. Null for every operation written before decisions were enforced.
|
|
5110
7124
|
*/
|
|
5111
|
-
async function up$
|
|
7125
|
+
async function up$11(db) {
|
|
5112
7126
|
await db.schema.alterTable("Operation").addColumn("deniedReason", "text").execute();
|
|
5113
7127
|
await db.schema.alterTable("operation_index_operations").addColumn("deniedReason", "text").execute();
|
|
5114
7128
|
}
|
|
5115
|
-
async function down$
|
|
7129
|
+
async function down$11(db) {
|
|
5116
7130
|
await db.schema.alterTable("operation_index_operations").dropColumn("deniedReason").execute();
|
|
5117
7131
|
await db.schema.alterTable("Operation").dropColumn("deniedReason").execute();
|
|
5118
7132
|
}
|
|
5119
7133
|
//#endregion
|
|
5120
7134
|
//#region src/storage/migrations/016_add_dead_letter_error_type.ts
|
|
5121
7135
|
var _016_add_dead_letter_error_type_exports = /* @__PURE__ */ __exportAll({
|
|
5122
|
-
down: () => down$
|
|
5123
|
-
up: () => up$
|
|
7136
|
+
down: () => down$10,
|
|
7137
|
+
up: () => up$10
|
|
5124
7138
|
});
|
|
5125
7139
|
/**
|
|
5126
7140
|
* The classification a dead letter falls into, stored because it decides whether
|
|
5127
7141
|
* the document stays quarantined and the in-memory error is gone after a restart.
|
|
5128
7142
|
* Defaulted rather than nullable, so a pre-existing row rehydrates.
|
|
5129
7143
|
*/
|
|
5130
|
-
async function up$
|
|
7144
|
+
async function up$10(db) {
|
|
5131
7145
|
await db.schema.alterTable("sync_dead_letters").addColumn("error_type", "text", (col) => col.notNull().defaultTo("UNCLASSIFIED")).execute();
|
|
5132
7146
|
}
|
|
5133
|
-
async function down$
|
|
7147
|
+
async function down$10(db) {
|
|
5134
7148
|
await db.schema.alterTable("sync_dead_letters").dropColumn("error_type").execute();
|
|
5135
7149
|
}
|
|
5136
7150
|
//#endregion
|
|
5137
7151
|
//#region src/storage/migrations/017_create_group_references.ts
|
|
5138
7152
|
var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
5139
|
-
down: () => down$
|
|
5140
|
-
up: () => up$
|
|
7153
|
+
down: () => down$9,
|
|
7154
|
+
up: () => up$9
|
|
5141
7155
|
});
|
|
5142
7156
|
/**
|
|
5143
7157
|
* One row per (document, group) reference ever discovered from an auth
|
|
@@ -5148,18 +7162,18 @@ var _017_create_group_references_exports = /* @__PURE__ */ __exportAll({
|
|
|
5148
7162
|
* requires (sync), and by groupId for the documents a group change affects
|
|
5149
7163
|
* (re-evaluation).
|
|
5150
7164
|
*/
|
|
5151
|
-
async function up$
|
|
7165
|
+
async function up$9(db) {
|
|
5152
7166
|
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
7167
|
await db.schema.createIndex("idx_group_references_groupId").on("group_references").column("groupId").execute();
|
|
5154
7168
|
}
|
|
5155
|
-
async function down$
|
|
7169
|
+
async function down$9(db) {
|
|
5156
7170
|
await db.schema.dropTable("group_references").execute();
|
|
5157
7171
|
}
|
|
5158
7172
|
//#endregion
|
|
5159
7173
|
//#region src/storage/migrations/018_add_sync_remote_bound_address.ts
|
|
5160
7174
|
var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
5161
|
-
down: () => down$
|
|
5162
|
-
up: () => up$
|
|
7175
|
+
down: () => down$8,
|
|
7176
|
+
up: () => up$8
|
|
5163
7177
|
});
|
|
5164
7178
|
/**
|
|
5165
7179
|
* The address a sync channel is bound to, so a channel created by one subject
|
|
@@ -5170,17 +7184,17 @@ var _018_add_sync_remote_bound_address_exports = /* @__PURE__ */ __exportAll({
|
|
|
5170
7184
|
* until its first authenticated poll adopts it. A default would claim them all
|
|
5171
7185
|
* for one address.
|
|
5172
7186
|
*/
|
|
5173
|
-
async function up$
|
|
7187
|
+
async function up$8(db) {
|
|
5174
7188
|
await db.schema.alterTable("sync_remotes").addColumn("bound_address", "text").execute();
|
|
5175
7189
|
}
|
|
5176
|
-
async function down$
|
|
7190
|
+
async function down$8(db) {
|
|
5177
7191
|
await db.schema.alterTable("sync_remotes").dropColumn("bound_address").execute();
|
|
5178
7192
|
}
|
|
5179
7193
|
//#endregion
|
|
5180
7194
|
//#region src/storage/migrations/019_require_action_id.ts
|
|
5181
7195
|
var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
5182
|
-
down: () => down,
|
|
5183
|
-
up: () => up
|
|
7196
|
+
down: () => down$7,
|
|
7197
|
+
up: () => up$7
|
|
5184
7198
|
});
|
|
5185
7199
|
/**
|
|
5186
7200
|
* Makes an operation whose action carries no id physically unstorable.
|
|
@@ -5207,7 +7221,7 @@ var _019_require_action_id_exports = /* @__PURE__ */ __exportAll({
|
|
|
5207
7221
|
* The empty string is rejected alongside null. It derives the same colliding
|
|
5208
7222
|
* operation id as an absent id, so admitting it would leave the hole open.
|
|
5209
7223
|
*/
|
|
5210
|
-
async function up(db) {
|
|
7224
|
+
async function up$7(db) {
|
|
5211
7225
|
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
7226
|
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
7227
|
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 +7233,118 @@ async function up(db) {
|
|
|
5219
7233
|
* their operations are now known by, and reverting them would reintroduce the
|
|
5220
7234
|
* collision the migration removed.
|
|
5221
7235
|
*/
|
|
5222
|
-
async function down(db) {
|
|
7236
|
+
async function down$7(db) {
|
|
5223
7237
|
await db.schema.alterTable("operation_index_operations").dropConstraint("action_must_have_id").execute();
|
|
5224
7238
|
await db.schema.alterTable("Operation").dropConstraint("action_must_have_id").execute();
|
|
5225
7239
|
}
|
|
5226
7240
|
//#endregion
|
|
7241
|
+
//#region src/storage/migrations/020_add_snapshot_operation_ordinal.ts
|
|
7242
|
+
var _020_add_snapshot_operation_ordinal_exports = /* @__PURE__ */ __exportAll({
|
|
7243
|
+
down: () => down$6,
|
|
7244
|
+
up: () => up$6
|
|
7245
|
+
});
|
|
7246
|
+
/** Orders header row writes across scopes; `lastOperationIndex` is per scope. */
|
|
7247
|
+
async function up$6(db) {
|
|
7248
|
+
await db.schema.alterTable("DocumentSnapshot").addColumn("lastOperationOrdinal", "integer", (col) => col.notNull().defaultTo(0)).execute();
|
|
7249
|
+
}
|
|
7250
|
+
async function down$6(db) {
|
|
7251
|
+
await db.schema.alterTable("DocumentSnapshot").dropColumn("lastOperationOrdinal").execute();
|
|
7252
|
+
}
|
|
7253
|
+
//#endregion
|
|
7254
|
+
//#region src/storage/migrations/021_add_sync_remote_peer.ts
|
|
7255
|
+
var _021_add_sync_remote_peer_exports = /* @__PURE__ */ __exportAll({
|
|
7256
|
+
down: () => down$5,
|
|
7257
|
+
up: () => up$5
|
|
7258
|
+
});
|
|
7259
|
+
/**
|
|
7260
|
+
* The peer's manifest as canonical JSON, and when it was heard. Both null: not
|
|
7261
|
+
* yet heard. A time with no manifest: a silent peer, which gets the baselines.
|
|
7262
|
+
*/
|
|
7263
|
+
async function up$5(db) {
|
|
7264
|
+
await db.schema.alterTable("sync_remotes").addColumn("peer_manifest", "text").addColumn("peer_manifest_at_utc_ms", "bigint").execute();
|
|
7265
|
+
}
|
|
7266
|
+
async function down$5(db) {
|
|
7267
|
+
await db.schema.alterTable("sync_remotes").dropColumn("peer_manifest").dropColumn("peer_manifest_at_utc_ms").execute();
|
|
7268
|
+
}
|
|
7269
|
+
//#endregion
|
|
7270
|
+
//#region src/storage/migrations/022_create_sync_holds.ts
|
|
7271
|
+
var _022_create_sync_holds_exports = /* @__PURE__ */ __exportAll({
|
|
7272
|
+
down: () => down$4,
|
|
7273
|
+
up: () => up$4
|
|
7274
|
+
});
|
|
7275
|
+
/** Documents held back from a remote whose peer cannot run their versions. */
|
|
7276
|
+
async function up$4(db) {
|
|
7277
|
+
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", [
|
|
7278
|
+
"remote_name",
|
|
7279
|
+
"document_id",
|
|
7280
|
+
"branch"
|
|
7281
|
+
]).execute();
|
|
7282
|
+
}
|
|
7283
|
+
async function down$4(db) {
|
|
7284
|
+
await db.schema.dropTable("sync_holds").execute();
|
|
7285
|
+
}
|
|
7286
|
+
//#endregion
|
|
7287
|
+
//#region src/storage/migrations/023_index_created_protocol_versions.ts
|
|
7288
|
+
var _023_index_created_protocol_versions_exports = /* @__PURE__ */ __exportAll({
|
|
7289
|
+
down: () => down$3,
|
|
7290
|
+
up: () => up$3
|
|
7291
|
+
});
|
|
7292
|
+
/**
|
|
7293
|
+
* One entry per document created with protocolVersions, keyed by a hash so an
|
|
7294
|
+
* oversized header cannot make its creation unstorable. Startup reads the
|
|
7295
|
+
* distinct values without visiting every document.
|
|
7296
|
+
*/
|
|
7297
|
+
async function up$3(db) {
|
|
7298
|
+
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();
|
|
7299
|
+
}
|
|
7300
|
+
async function down$3(db) {
|
|
7301
|
+
await db.schema.dropIndex("idx_operation_created_protocol_versions").execute();
|
|
7302
|
+
}
|
|
7303
|
+
//#endregion
|
|
7304
|
+
//#region src/storage/migrations/024_create_document_purges.ts
|
|
7305
|
+
var _024_create_document_purges_exports = /* @__PURE__ */ __exportAll({
|
|
7306
|
+
down: () => down$2,
|
|
7307
|
+
up: () => up$2
|
|
7308
|
+
});
|
|
7309
|
+
/** One row per purged document: the tombstone. Written once, never deleted. */
|
|
7310
|
+
async function up$2(db) {
|
|
7311
|
+
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();
|
|
7312
|
+
}
|
|
7313
|
+
async function down$2(db) {
|
|
7314
|
+
await db.schema.dropTable("document_purges").execute();
|
|
7315
|
+
}
|
|
7316
|
+
//#endregion
|
|
7317
|
+
//#region src/storage/migrations/025_create_sync_received_markers.ts
|
|
7318
|
+
var _025_create_sync_received_markers_exports = /* @__PURE__ */ __exportAll({
|
|
7319
|
+
down: () => down$1,
|
|
7320
|
+
up: () => up$1
|
|
7321
|
+
});
|
|
7322
|
+
/** Received purge markers awaiting their outcome; removed with their remote. */
|
|
7323
|
+
async function up$1(db) {
|
|
7324
|
+
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();
|
|
7325
|
+
}
|
|
7326
|
+
async function down$1(db) {
|
|
7327
|
+
await db.schema.dropTable("sync_received_markers").execute();
|
|
7328
|
+
}
|
|
7329
|
+
//#endregion
|
|
7330
|
+
//#region src/storage/migrations/026_create_sync_purge_refusals.ts
|
|
7331
|
+
var _026_create_sync_purge_refusals_exports = /* @__PURE__ */ __exportAll({
|
|
7332
|
+
down: () => down,
|
|
7333
|
+
up: () => up
|
|
7334
|
+
});
|
|
7335
|
+
/** A remote's refusal of a purge marker; payload-free, kept like the tombstone. */
|
|
7336
|
+
async function up(db) {
|
|
7337
|
+
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", [
|
|
7338
|
+
"remote_name",
|
|
7339
|
+
"document_id",
|
|
7340
|
+
"branch"
|
|
7341
|
+
]).execute();
|
|
7342
|
+
await db.schema.createIndex("idx_sync_purge_refusals_document").on("sync_purge_refusals").column("document_id").execute();
|
|
7343
|
+
}
|
|
7344
|
+
async function down(db) {
|
|
7345
|
+
await db.schema.dropTable("sync_purge_refusals").execute();
|
|
7346
|
+
}
|
|
7347
|
+
//#endregion
|
|
5227
7348
|
//#region src/storage/migrations/migrator.ts
|
|
5228
7349
|
const REACTOR_SCHEMA = "reactor";
|
|
5229
7350
|
const migrations = {
|
|
@@ -5245,7 +7366,14 @@ const migrations = {
|
|
|
5245
7366
|
"016_add_dead_letter_error_type": _016_add_dead_letter_error_type_exports,
|
|
5246
7367
|
"017_create_group_references": _017_create_group_references_exports,
|
|
5247
7368
|
"018_add_sync_remote_bound_address": _018_add_sync_remote_bound_address_exports,
|
|
5248
|
-
"019_require_action_id": _019_require_action_id_exports
|
|
7369
|
+
"019_require_action_id": _019_require_action_id_exports,
|
|
7370
|
+
"020_add_snapshot_operation_ordinal": _020_add_snapshot_operation_ordinal_exports,
|
|
7371
|
+
"021_add_sync_remote_peer": _021_add_sync_remote_peer_exports,
|
|
7372
|
+
"022_create_sync_holds": _022_create_sync_holds_exports,
|
|
7373
|
+
"023_index_created_protocol_versions": _023_index_created_protocol_versions_exports,
|
|
7374
|
+
"024_create_document_purges": _024_create_document_purges_exports,
|
|
7375
|
+
"025_create_sync_received_markers": _025_create_sync_received_markers_exports,
|
|
7376
|
+
"026_create_sync_purge_refusals": _026_create_sync_purge_refusals_exports
|
|
5249
7377
|
};
|
|
5250
7378
|
var ProgrammaticMigrationProvider = class {
|
|
5251
7379
|
getMigrations() {
|
|
@@ -5303,8 +7431,163 @@ async function getMigrationStatus(db, schema = REACTOR_SCHEMA) {
|
|
|
5303
7431
|
}
|
|
5304
7432
|
//#endregion
|
|
5305
7433
|
//#region src/core/drive-container-types.ts
|
|
5306
|
-
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
7434
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = /* @__PURE__ */ new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
7435
|
+
//#endregion
|
|
7436
|
+
//#region src/executor/worker/error-info.ts
|
|
7437
|
+
const MAX_STACK_LENGTH = 8192;
|
|
7438
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
7439
|
+
function toErrorInfo(err, depth = 0) {
|
|
7440
|
+
if (depth > MAX_CAUSE_DEPTH) return {
|
|
7441
|
+
name: "Error",
|
|
7442
|
+
message: "cause chain truncated"
|
|
7443
|
+
};
|
|
7444
|
+
if (err instanceof Error) {
|
|
7445
|
+
const info = {
|
|
7446
|
+
name: err.name || "Error",
|
|
7447
|
+
message: err.message
|
|
7448
|
+
};
|
|
7449
|
+
if (typeof err.stack === "string") info.stack = err.stack.length > MAX_STACK_LENGTH ? err.stack.slice(0, MAX_STACK_LENGTH) : err.stack;
|
|
7450
|
+
if (err.cause !== void 0) info.cause = toErrorInfo(err.cause, depth + 1);
|
|
7451
|
+
const documentId = err.documentId;
|
|
7452
|
+
if (typeof documentId === "string") info.documentId = documentId;
|
|
7453
|
+
const { documentType, requestedVersion } = err;
|
|
7454
|
+
if (typeof documentType === "string") info.documentType = documentType;
|
|
7455
|
+
if (typeof requestedVersion === "number") info.requestedVersion = requestedVersion;
|
|
7456
|
+
return info;
|
|
7457
|
+
}
|
|
7458
|
+
if (typeof err === "string") return {
|
|
7459
|
+
name: "Error",
|
|
7460
|
+
message: err
|
|
7461
|
+
};
|
|
7462
|
+
if (err && typeof err === "object") {
|
|
7463
|
+
const record = err;
|
|
7464
|
+
return {
|
|
7465
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
7466
|
+
message: typeof record.message === "string" ? record.message : safeStringify(err)
|
|
7467
|
+
};
|
|
7468
|
+
}
|
|
7469
|
+
return {
|
|
7470
|
+
name: "Error",
|
|
7471
|
+
message: safeStringify(err)
|
|
7472
|
+
};
|
|
7473
|
+
}
|
|
7474
|
+
function safeStringify(value) {
|
|
7475
|
+
if (value === null || value === void 0) return String(value);
|
|
7476
|
+
if (typeof value === "string") return value;
|
|
7477
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
7478
|
+
try {
|
|
7479
|
+
return JSON.stringify(value);
|
|
7480
|
+
} catch {
|
|
7481
|
+
return "[unserializable]";
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
function fromErrorInfo(info) {
|
|
7485
|
+
const err = new Error(info.message);
|
|
7486
|
+
Object.defineProperty(err, "name", {
|
|
7487
|
+
value: info.name,
|
|
7488
|
+
configurable: true,
|
|
7489
|
+
writable: true
|
|
7490
|
+
});
|
|
7491
|
+
if (info.stack !== void 0) err.stack = info.stack;
|
|
7492
|
+
if (info.documentId !== void 0) Object.defineProperty(err, "documentId", {
|
|
7493
|
+
value: info.documentId,
|
|
7494
|
+
configurable: true,
|
|
7495
|
+
writable: true
|
|
7496
|
+
});
|
|
7497
|
+
for (const key of ["documentType", "requestedVersion"]) if (info[key] !== void 0) Object.defineProperty(err, key, {
|
|
7498
|
+
value: info[key],
|
|
7499
|
+
configurable: true,
|
|
7500
|
+
writable: true
|
|
7501
|
+
});
|
|
7502
|
+
if (info.cause !== void 0) Object.defineProperty(err, "cause", {
|
|
7503
|
+
value: fromErrorInfo(info.cause),
|
|
7504
|
+
configurable: true,
|
|
7505
|
+
writable: true
|
|
7506
|
+
});
|
|
7507
|
+
return err;
|
|
7508
|
+
}
|
|
7509
|
+
//#endregion
|
|
7510
|
+
//#region src/executor/worker/sanitize.ts
|
|
7511
|
+
const MAX_DEPTH = 8;
|
|
7512
|
+
function errorToInfo(err) {
|
|
7513
|
+
return toErrorInfo(err);
|
|
7514
|
+
}
|
|
7515
|
+
function sanitizeArg(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
7516
|
+
if (depth > MAX_DEPTH) return "[Truncated]";
|
|
7517
|
+
if (value === null || value === void 0) return null;
|
|
7518
|
+
if (typeof value === "boolean") return value;
|
|
7519
|
+
if (typeof value === "number") return value;
|
|
7520
|
+
if (typeof value === "string") return value;
|
|
7521
|
+
if (typeof value === "bigint") return value.toString();
|
|
7522
|
+
if (typeof value === "function" || typeof value === "symbol") return null;
|
|
7523
|
+
if (value instanceof Error) return toErrorInfo(value);
|
|
7524
|
+
if (value instanceof Date) return value.toISOString();
|
|
7525
|
+
if (value instanceof Map) {
|
|
7526
|
+
const obj = {};
|
|
7527
|
+
for (const [k, v] of value.entries()) obj[String(k)] = sanitizeArg(v, depth + 1, seen);
|
|
7528
|
+
return obj;
|
|
7529
|
+
}
|
|
7530
|
+
if (value instanceof Set) {
|
|
7531
|
+
const arr = [];
|
|
7532
|
+
for (const v of value.values()) arr.push(sanitizeArg(v, depth + 1, seen));
|
|
7533
|
+
return arr;
|
|
7534
|
+
}
|
|
7535
|
+
if (Array.isArray(value)) {
|
|
7536
|
+
if (seen.has(value)) return "[Circular]";
|
|
7537
|
+
seen.add(value);
|
|
7538
|
+
const result = value.map((v) => sanitizeArg(v, depth + 1, seen));
|
|
7539
|
+
seen.delete(value);
|
|
7540
|
+
return result;
|
|
7541
|
+
}
|
|
7542
|
+
if (typeof value === "object") {
|
|
7543
|
+
const proto = Object.getPrototypeOf(value);
|
|
7544
|
+
if (proto !== Object.prototype && proto !== null) return { __nonClonable: value.constructor?.name || "Unknown" };
|
|
7545
|
+
if (seen.has(value)) return "[Circular]";
|
|
7546
|
+
seen.add(value);
|
|
7547
|
+
const result = {};
|
|
7548
|
+
for (const [k, v] of Object.entries(value)) result[k] = sanitizeArg(v, depth + 1, seen);
|
|
7549
|
+
seen.delete(value);
|
|
7550
|
+
return result;
|
|
7551
|
+
}
|
|
7552
|
+
return null;
|
|
7553
|
+
}
|
|
7554
|
+
//#endregion
|
|
7555
|
+
//#region src/executor/worker/forwarding-logger.ts
|
|
7556
|
+
function createForwardingLogger(post, level = "info", tags = []) {
|
|
7557
|
+
function safePost(msg) {
|
|
7558
|
+
try {
|
|
7559
|
+
post(msg);
|
|
7560
|
+
} catch {
|
|
7561
|
+
try {
|
|
7562
|
+
process.stderr.write(`[forwarding-logger] postMessage failed: ${msg.level} ${msg.message}\n`);
|
|
7563
|
+
} catch {}
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7566
|
+
function emit(wireLevel, message, args) {
|
|
7567
|
+
const sanitizedArgs = args.map((a) => sanitizeArg(a));
|
|
7568
|
+
safePost({
|
|
7569
|
+
type: "log",
|
|
7570
|
+
level: wireLevel,
|
|
7571
|
+
message,
|
|
7572
|
+
args: tags.length > 0 ? [sanitizeArg({ tags }), ...sanitizedArgs] : sanitizedArgs,
|
|
7573
|
+
timestamp: Date.now()
|
|
7574
|
+
});
|
|
7575
|
+
}
|
|
7576
|
+
return {
|
|
7577
|
+
level,
|
|
7578
|
+
verbose: (message, ...args) => emit("debug", message, args),
|
|
7579
|
+
debug: (message, ...args) => emit("debug", message, args),
|
|
7580
|
+
info: (message, ...args) => emit("info", message, args),
|
|
7581
|
+
warn: (message, ...args) => emit("warn", message, args),
|
|
7582
|
+
error: (message, ...args) => emit("error", message, args),
|
|
7583
|
+
errorHandler: ((...data) => {
|
|
7584
|
+
const first = data[0];
|
|
7585
|
+
emit("error", typeof first === "string" ? first : "", data.slice(1));
|
|
7586
|
+
}),
|
|
7587
|
+
child: (childTags) => createForwardingLogger(post, level, [...tags, ...childTags])
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
5307
7590
|
//#endregion
|
|
5308
|
-
export {
|
|
7591
|
+
export { decideAtHead as $, ProbeSettler as A, ReactorEventTypes as At, JobExecutorEventTypes as B, KyselyWriteCache as C, PurgeTooLargeError as Ct, acquirePurgeLocks as D, UnsupportedStoredProtocolError as Dt, PURGE_NS as E, UnsupportedProtocolVersionError as Et, snapshotFunctionsFor as F, __exportAll as Ft, ModuleNotFoundError as G, DuplicateModuleError as H, DocumentMetaCache as I, createDocumentFromAction as J, GATED_DOCUMENT_ACTIONS as K, CollectionMembershipCache as L, createKyselyWatermarkProbe as M, parsePagingOptions as Mt, describeWaitingSessions as N, throwIfAborted as Nt, findPurged as O, UpgradePreconditionFailedError as Ot, parseSnapshot as P, yieldToMain as Pt, targetDocumentId as Q, DEFAULT_DEFERRED_JOB_TTL_MS as R, EventBus as S, InvalidSignatureError as St, PURGE_LOCK_BUCKETS as T, ReservedActionError as Tt, InvalidModuleError as U, DuplicateManifestError as V, ModelNotWorkerImportableError as W, submittedActionIds as X, createEmptyConsistencyToken as Y, summarizeSubmittedActions as Z, KyselyExecutionScope as _, DocumentNotFoundError as _t, DEFAULT_DRIVE_CONTAINER_TYPES as a, APPEND_CONDITION_FAILED_PREFIX as at, appliedDelete as b, GroupInUseError as bt, runMigrations as c, DocumentExistence as ct, KyselyOperationStore as d, RevisionMismatchError as dt, selectDecisionModel as et, KyselyKeyframeStore as f, AuthEnforcementDisabledError as ft, PassthroughSigner as g, DocumentNotDeletedError as gt, verifyActionSignature as h, DocumentDeletedError as ht, fromErrorInfo as i, DriveCollectionId as it, SettledWatermark as j, matchesScope as jt, listPurged as k, EventBusAggregateError as kt, createForwardingPoolInstrumentation as l, DuplicateOperationError as lt, SimpleJobExecutor as m, AuthorizationDeniedError as mt, errorToInfo as n, authDecisionModel as nt, REACTOR_SCHEMA as o, AppendConditionFailedError as ot, DocumentModelRegistry as p, AuthTimestampNotMonotonicError as pt, createConsistencyToken as q, sanitizeArg as r, buildDecisionModel as rt, getMigrationStatus as s, DocumentAlreadyExistsError as st, createForwardingLogger as t, documentDecisionModel as tt, instrumentPgPool as u, OptimisticLockError as ut, DEFAULT_PURGE_DELETE_BATCH as v, DocumentPurgedError as vt, KyselyOperationIndex as w, RelationshipNotFoundError as wt, resolveFeatureFlags as x, InvalidOperationTimestampError as xt, KyselyDocumentPurger as y, ExcessiveReshuffleError as yt, DEFAULT_MAX_PURGE_OPERATIONS as z };
|
|
5309
7592
|
|
|
5310
|
-
//# sourceMappingURL=
|
|
7593
|
+
//# sourceMappingURL=forwarding-logger-BKDP4JnD.js.map
|