@powerhousedao/reactor 6.0.2-staging.8 → 6.1.0-dev.0
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/index.d.ts +109 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +536 -392
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -55,13 +55,18 @@ function deleteDocumentAction(documentId) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Creates an ADD_RELATIONSHIP action
|
|
58
|
+
* Creates an ADD_RELATIONSHIP action that records a directed edge from
|
|
59
|
+
* `sourceId` to `targetId` with an arbitrary `relationshipType` and optional
|
|
60
|
+
* `metadata`. The edge is opaque to the reactor — consumers (e.g. reactor-drive)
|
|
61
|
+
* define their own type strings such as `"drive/child"` and attach
|
|
62
|
+
* domain-specific metadata.
|
|
59
63
|
*/
|
|
60
|
-
function addRelationshipAction(sourceId, targetId, relationshipType
|
|
64
|
+
function addRelationshipAction(sourceId, targetId, relationshipType, metadata) {
|
|
61
65
|
const input = {
|
|
62
66
|
sourceId,
|
|
63
67
|
targetId,
|
|
64
|
-
relationshipType
|
|
68
|
+
relationshipType,
|
|
69
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
65
70
|
};
|
|
66
71
|
return {
|
|
67
72
|
id: generateId(),
|
|
@@ -72,6 +77,25 @@ function addRelationshipAction(sourceId, targetId, relationshipType = "child") {
|
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
80
|
+
* Creates an UPDATE_RELATIONSHIP action to replace a relationship's metadata
|
|
81
|
+
* without losing its createdAt ordering.
|
|
82
|
+
*/
|
|
83
|
+
function updateRelationshipAction(sourceId, targetId, relationshipType, metadata) {
|
|
84
|
+
const input = {
|
|
85
|
+
sourceId,
|
|
86
|
+
targetId,
|
|
87
|
+
relationshipType,
|
|
88
|
+
metadata
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
id: generateId(),
|
|
92
|
+
type: "UPDATE_RELATIONSHIP",
|
|
93
|
+
scope: "document",
|
|
94
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
95
|
+
input
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
75
99
|
* Creates a REMOVE_RELATIONSHIP action to remove a parent-child relationship.
|
|
76
100
|
*/
|
|
77
101
|
function removeRelationshipAction(sourceId, targetId, relationshipType = "child") {
|
|
@@ -291,6 +315,44 @@ let JobStatus = /* @__PURE__ */ function(JobStatus) {
|
|
|
291
315
|
return JobStatus;
|
|
292
316
|
}({});
|
|
293
317
|
//#endregion
|
|
318
|
+
//#region src/shared/utils.ts
|
|
319
|
+
function matchesScope(view = {}, scope) {
|
|
320
|
+
if (view.scopes) return view.scopes.includes(scope);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
function yieldToMain() {
|
|
324
|
+
const s = globalThis.scheduler;
|
|
325
|
+
if (s?.yield) return s.yield();
|
|
326
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
327
|
+
}
|
|
328
|
+
const defaultAbortError = () => /* @__PURE__ */ new Error("Operation aborted");
|
|
329
|
+
function throwIfAborted(signal, makeError = defaultAbortError) {
|
|
330
|
+
if (signal?.aborted) throw makeError();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Validates PagingOptions and returns a normalized offset and limit.
|
|
334
|
+
* Throws if the cursor is not empty and not a non-negative integer, or if
|
|
335
|
+
* limit is less than 1. When `paging` is undefined, returns offset 0 and
|
|
336
|
+
* the caller-supplied `defaultLimit`.
|
|
337
|
+
*/
|
|
338
|
+
function parsePagingOptions(paging, defaultLimit) {
|
|
339
|
+
if (paging === void 0) return {
|
|
340
|
+
offset: 0,
|
|
341
|
+
limit: defaultLimit
|
|
342
|
+
};
|
|
343
|
+
if (!Number.isInteger(paging.limit) || paging.limit < 1) throw new Error(`Invalid paging limit: ${String(paging.limit)} (must be an integer >= 1)`);
|
|
344
|
+
if (paging.cursor === "") return {
|
|
345
|
+
offset: 0,
|
|
346
|
+
limit: paging.limit
|
|
347
|
+
};
|
|
348
|
+
const parsed = Number(paging.cursor);
|
|
349
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`Invalid paging cursor: ${JSON.stringify(paging.cursor)} (must be empty or a non-negative integer)`);
|
|
350
|
+
return {
|
|
351
|
+
offset: parsed,
|
|
352
|
+
limit: paging.limit
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
294
356
|
//#region src/client/drive-client.ts
|
|
295
357
|
/**
|
|
296
358
|
* Implementation of {@link IDriveClient}.
|
|
@@ -409,6 +471,10 @@ var DriveClient = class {
|
|
|
409
471
|
if (!node) throw new Error("Node missing from drive after rename");
|
|
410
472
|
return node;
|
|
411
473
|
}
|
|
474
|
+
async setPreferredEditorOnNode(nodeId, preferredEditor, signal) {
|
|
475
|
+
this.logger.verbose("drives.setPreferredEditorOnNode(@nodeId, @preferredEditor)", nodeId, preferredEditor);
|
|
476
|
+
return this.client.setPreferredEditor(nodeId, preferredEditor, "main", signal);
|
|
477
|
+
}
|
|
412
478
|
async moveNode(driveIdentifier, srcNodeId, targetParentFolderId, signal) {
|
|
413
479
|
this.logger.verbose("drives.moveNode(@driveIdentifier, @srcNodeId, @targetParentFolderId)", driveIdentifier, srcNodeId, targetParentFolderId);
|
|
414
480
|
return this.client.execute(driveIdentifier, "main", [moveNode({
|
|
@@ -456,11 +522,22 @@ var DriveClient = class {
|
|
|
456
522
|
if (!node) throw new Error(`Node ${nodeId} not found in drive ${driveIdentifier}`);
|
|
457
523
|
return node;
|
|
458
524
|
}
|
|
459
|
-
async listNodes(driveIdentifier, parentFolder, signal) {
|
|
460
|
-
this.logger.verbose("drives.listNodes(@driveIdentifier, @parentFolder)", driveIdentifier, parentFolder);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
525
|
+
async listNodes(driveIdentifier, parentFolder, paging, signal) {
|
|
526
|
+
this.logger.verbose("drives.listNodes(@driveIdentifier, @parentFolder, @paging)", driveIdentifier, parentFolder, paging);
|
|
527
|
+
const allNodes = (await this.client.get(driveIdentifier, void 0, signal)).state.global.nodes;
|
|
528
|
+
const filtered = parentFolder === void 0 ? [...allNodes] : allNodes.filter((n) => (n.parentFolder ?? null) === parentFolder);
|
|
529
|
+
const { offset: startIndex, limit } = parsePagingOptions(paging, filtered.length);
|
|
530
|
+
const effective = paging ?? {
|
|
531
|
+
cursor: "",
|
|
532
|
+
limit
|
|
533
|
+
};
|
|
534
|
+
const endIndex = startIndex + limit;
|
|
535
|
+
return {
|
|
536
|
+
results: filtered.slice(startIndex, endIndex),
|
|
537
|
+
options: effective,
|
|
538
|
+
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {},
|
|
539
|
+
totalCount: filtered.length
|
|
540
|
+
};
|
|
464
541
|
}
|
|
465
542
|
async removeFileNode(driveId, fileId, signal) {
|
|
466
543
|
const relationshipActions = await signActions([removeRelationshipAction(driveId, fileId, "child")], this.signer, signal);
|
|
@@ -642,7 +719,7 @@ function decodeCompositeCursor(cursor) {
|
|
|
642
719
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Invalid composite cursor format");
|
|
643
720
|
return parsed;
|
|
644
721
|
} catch (error) {
|
|
645
|
-
if (error instanceof SyntaxError) throw new Error("Invalid composite cursor format");
|
|
722
|
+
if (error instanceof SyntaxError) throw new Error("Invalid composite cursor format", { cause: error });
|
|
646
723
|
throw error;
|
|
647
724
|
}
|
|
648
725
|
}
|
|
@@ -920,6 +997,17 @@ var ReactorClient = class {
|
|
|
920
997
|
const signedActions = await signActions(actions, this.signer, signal);
|
|
921
998
|
return this.reactor.execute(documentIdentifier, branch, signedActions, signal);
|
|
922
999
|
}
|
|
1000
|
+
async executeBatch(request, signal) {
|
|
1001
|
+
this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
|
|
1002
|
+
const signedJobs = await Promise.all(request.jobs.map(async (job) => ({
|
|
1003
|
+
...job,
|
|
1004
|
+
actions: await signActions(job.actions, this.signer, signal)
|
|
1005
|
+
})));
|
|
1006
|
+
const batchResult = await this.reactor.executeBatch({ jobs: signedJobs }, signal);
|
|
1007
|
+
const completedJobs = await Promise.all(Object.values(batchResult.jobs).map((job) => this.waitForJob(job, signal)));
|
|
1008
|
+
for (const job of completedJobs) if (job.status === JobStatus.FAILED) throw new Error(job.error?.message);
|
|
1009
|
+
return batchResult;
|
|
1010
|
+
}
|
|
923
1011
|
/**
|
|
924
1012
|
* Renames a document and waits for completion
|
|
925
1013
|
*/
|
|
@@ -928,6 +1016,14 @@ var ReactorClient = class {
|
|
|
928
1016
|
return this.execute(documentIdentifier, branch, [actions.setName(name)], signal);
|
|
929
1017
|
}
|
|
930
1018
|
/**
|
|
1019
|
+
* Updates the preferred editor recorded in the document header meta.
|
|
1020
|
+
* Pass `null` to clear it.
|
|
1021
|
+
*/
|
|
1022
|
+
async setPreferredEditor(documentIdentifier, preferredEditor, branch = "main", signal) {
|
|
1023
|
+
this.logger.verbose("setPreferredEditor(@documentIdentifier, @preferredEditor, @branch)", documentIdentifier, preferredEditor, branch);
|
|
1024
|
+
return this.execute(documentIdentifier, branch, [actions.setPreferredEditor(preferredEditor)], signal);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
931
1027
|
* Adds multiple documents as children to another and waits for completion
|
|
932
1028
|
*/
|
|
933
1029
|
async addRelationship(sourceIdentifier, targetIdentifier, relationshipType, branch = "main", signal) {
|
|
@@ -2150,7 +2246,6 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2150
2246
|
startRevision = keyframe.revision;
|
|
2151
2247
|
documentType = keyframe.document.header.documentType;
|
|
2152
2248
|
} else {
|
|
2153
|
-
document = void 0;
|
|
2154
2249
|
startRevision = -1;
|
|
2155
2250
|
const createOpResult = await this.operationStore.getSince(documentId, "document", branch, -1, void 0, {
|
|
2156
2251
|
cursor: "0",
|
|
@@ -2205,7 +2300,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2205
2300
|
hasMorePages = Boolean(result.nextCursor) && !reachedTarget;
|
|
2206
2301
|
if (hasMorePages) cursor = result.nextCursor;
|
|
2207
2302
|
} catch (err) {
|
|
2208
|
-
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}
|
|
2303
|
+
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2209
2304
|
}
|
|
2210
2305
|
} while (hasMorePages);
|
|
2211
2306
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
@@ -2230,7 +2325,7 @@ var KyselyWriteCache = class KyselyWriteCache {
|
|
|
2230
2325
|
if (targetRevision !== void 0 && operation.index === targetRevision) break;
|
|
2231
2326
|
}
|
|
2232
2327
|
} catch (err) {
|
|
2233
|
-
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}
|
|
2328
|
+
throw new Error(`Failed to rebuild document ${documentId}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
2234
2329
|
}
|
|
2235
2330
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
2236
2331
|
document.header.revision = revisions.revision;
|
|
@@ -2368,7 +2463,7 @@ let JobQueueState = /* @__PURE__ */ function(JobQueueState) {
|
|
|
2368
2463
|
*/
|
|
2369
2464
|
const QueueEventTypes = { JOB_AVAILABLE: 1e4 };
|
|
2370
2465
|
//#endregion
|
|
2371
|
-
//#region src/registry/
|
|
2466
|
+
//#region src/registry/errors.ts
|
|
2372
2467
|
/**
|
|
2373
2468
|
* Error thrown when a document model module is not found in the registry.
|
|
2374
2469
|
*/
|
|
@@ -2456,140 +2551,6 @@ var InvalidUpgradeStepError = class extends Error {
|
|
|
2456
2551
|
this.name = "InvalidUpgradeStepError";
|
|
2457
2552
|
}
|
|
2458
2553
|
};
|
|
2459
|
-
/**
|
|
2460
|
-
* In-memory implementation of the IDocumentModelRegistry interface.
|
|
2461
|
-
* Manages document model modules with version-aware storage and upgrade manifest support.
|
|
2462
|
-
*/
|
|
2463
|
-
var DocumentModelRegistry = class {
|
|
2464
|
-
modules = [];
|
|
2465
|
-
manifests = [];
|
|
2466
|
-
registerModules(...modules) {
|
|
2467
|
-
return modules.map((module) => {
|
|
2468
|
-
try {
|
|
2469
|
-
const documentType = module.documentModel.global.id;
|
|
2470
|
-
const version = module.version ?? 1;
|
|
2471
|
-
for (let i = 0; i < this.modules.length; i++) {
|
|
2472
|
-
const existing = this.modules[i];
|
|
2473
|
-
const existingType = existing.documentModel.global.id;
|
|
2474
|
-
const existingVersion = existing.version ?? 1;
|
|
2475
|
-
if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
|
|
2476
|
-
}
|
|
2477
|
-
this.modules.push(module);
|
|
2478
|
-
return {
|
|
2479
|
-
status: "success",
|
|
2480
|
-
item: module
|
|
2481
|
-
};
|
|
2482
|
-
} catch (error) {
|
|
2483
|
-
return {
|
|
2484
|
-
status: "error",
|
|
2485
|
-
item: module,
|
|
2486
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
2487
|
-
};
|
|
2488
|
-
}
|
|
2489
|
-
});
|
|
2490
|
-
}
|
|
2491
|
-
unregisterModules(...documentTypes) {
|
|
2492
|
-
let allFound = true;
|
|
2493
|
-
for (const documentType of documentTypes) {
|
|
2494
|
-
if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
|
|
2495
|
-
this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
|
|
2496
|
-
}
|
|
2497
|
-
return allFound;
|
|
2498
|
-
}
|
|
2499
|
-
getModule(documentType, version) {
|
|
2500
|
-
let latestModule;
|
|
2501
|
-
let latestVersion = -1;
|
|
2502
|
-
for (let i = 0; i < this.modules.length; i++) {
|
|
2503
|
-
const module = this.modules[i];
|
|
2504
|
-
const moduleType = module.documentModel.global.id;
|
|
2505
|
-
const moduleVersion = module.version ?? 1;
|
|
2506
|
-
if (moduleType === documentType) {
|
|
2507
|
-
if (version !== void 0 && moduleVersion === version) return module;
|
|
2508
|
-
if (moduleVersion > latestVersion) {
|
|
2509
|
-
latestModule = module;
|
|
2510
|
-
latestVersion = moduleVersion;
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
if (version === void 0 && latestModule !== void 0) return latestModule;
|
|
2515
|
-
throw new ModuleNotFoundError(documentType, version);
|
|
2516
|
-
}
|
|
2517
|
-
getAllModules() {
|
|
2518
|
-
return [...this.modules];
|
|
2519
|
-
}
|
|
2520
|
-
clear() {
|
|
2521
|
-
this.modules = [];
|
|
2522
|
-
this.manifests = [];
|
|
2523
|
-
}
|
|
2524
|
-
getSupportedVersions(documentType) {
|
|
2525
|
-
const versions = [];
|
|
2526
|
-
for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
|
|
2527
|
-
if (versions.length === 0) throw new ModuleNotFoundError(documentType);
|
|
2528
|
-
return versions.sort((a, b) => a - b);
|
|
2529
|
-
}
|
|
2530
|
-
getLatestVersion(documentType) {
|
|
2531
|
-
let latest = -1;
|
|
2532
|
-
let found = false;
|
|
2533
|
-
for (const module of this.modules) if (module.documentModel.global.id === documentType) {
|
|
2534
|
-
found = true;
|
|
2535
|
-
const version = module.version ?? 1;
|
|
2536
|
-
if (version > latest) latest = version;
|
|
2537
|
-
}
|
|
2538
|
-
if (!found) throw new ModuleNotFoundError(documentType);
|
|
2539
|
-
return latest;
|
|
2540
|
-
}
|
|
2541
|
-
registerUpgradeManifests(...manifestsToRegister) {
|
|
2542
|
-
return manifestsToRegister.map((manifestToRegister) => {
|
|
2543
|
-
try {
|
|
2544
|
-
if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
|
|
2545
|
-
for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
|
|
2546
|
-
this.manifests.push(manifestToRegister);
|
|
2547
|
-
return {
|
|
2548
|
-
status: "success",
|
|
2549
|
-
item: manifestToRegister
|
|
2550
|
-
};
|
|
2551
|
-
} catch (error) {
|
|
2552
|
-
return {
|
|
2553
|
-
status: "error",
|
|
2554
|
-
item: manifestToRegister,
|
|
2555
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
});
|
|
2559
|
-
}
|
|
2560
|
-
unregisterUpgradeManifests(...documentTypes) {
|
|
2561
|
-
let allFound = true;
|
|
2562
|
-
for (const documentType of documentTypes) {
|
|
2563
|
-
if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
|
|
2564
|
-
this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
|
|
2565
|
-
}
|
|
2566
|
-
return allFound;
|
|
2567
|
-
}
|
|
2568
|
-
getUpgradeManifest(documentType) {
|
|
2569
|
-
for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
|
|
2570
|
-
throw new ManifestNotFoundError(documentType);
|
|
2571
|
-
}
|
|
2572
|
-
computeUpgradePath(documentType, fromVersion, toVersion) {
|
|
2573
|
-
if (fromVersion === toVersion) return [];
|
|
2574
|
-
if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
|
|
2575
|
-
const manifest = this.getUpgradeManifest(documentType);
|
|
2576
|
-
const path = [];
|
|
2577
|
-
for (let v = fromVersion + 1; v <= toVersion; v++) {
|
|
2578
|
-
const key = `v${v}`;
|
|
2579
|
-
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
|
|
2580
|
-
const transition = manifest.upgrades[key];
|
|
2581
|
-
path.push(transition);
|
|
2582
|
-
}
|
|
2583
|
-
return path;
|
|
2584
|
-
}
|
|
2585
|
-
getUpgradeReducer(documentType, fromVersion, toVersion) {
|
|
2586
|
-
if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
|
|
2587
|
-
const manifest = this.getUpgradeManifest(documentType);
|
|
2588
|
-
const key = `v${toVersion}`;
|
|
2589
|
-
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
|
|
2590
|
-
return manifest.upgrades[key].upgradeReducer;
|
|
2591
|
-
}
|
|
2592
|
-
};
|
|
2593
2554
|
//#endregion
|
|
2594
2555
|
//#region src/executor/simple-job-executor-manager.ts
|
|
2595
2556
|
/**
|
|
@@ -2844,24 +2805,17 @@ var SimpleJobExecutorManager = class {
|
|
|
2844
2805
|
}
|
|
2845
2806
|
};
|
|
2846
2807
|
//#endregion
|
|
2847
|
-
//#region src/shared/utils.ts
|
|
2848
|
-
function matchesScope(view = {}, scope) {
|
|
2849
|
-
if (view.scopes) return view.scopes.includes(scope);
|
|
2850
|
-
return true;
|
|
2851
|
-
}
|
|
2852
|
-
function yieldToMain() {
|
|
2853
|
-
const s = globalThis.scheduler;
|
|
2854
|
-
if (s?.yield) return s.yield();
|
|
2855
|
-
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
2856
|
-
}
|
|
2857
|
-
//#endregion
|
|
2858
2808
|
//#region src/utils/reshuffle.ts
|
|
2859
2809
|
const STRICT_ORDER_ACTION_TYPES = new Set([
|
|
2860
2810
|
"CREATE_DOCUMENT",
|
|
2861
2811
|
"DELETE_DOCUMENT",
|
|
2862
2812
|
"UPGRADE_DOCUMENT",
|
|
2863
2813
|
"ADD_RELATIONSHIP",
|
|
2864
|
-
"REMOVE_RELATIONSHIP"
|
|
2814
|
+
"REMOVE_RELATIONSHIP",
|
|
2815
|
+
"UPDATE_RELATIONSHIP",
|
|
2816
|
+
"ADD_FOLDER",
|
|
2817
|
+
"UPDATE_FOLDER",
|
|
2818
|
+
"REMOVE_FOLDER"
|
|
2865
2819
|
]);
|
|
2866
2820
|
/**
|
|
2867
2821
|
* Reshuffles operations by timestamp, then applies deterministic tie-breaking.
|
|
@@ -2908,9 +2862,10 @@ function driveCollectionId(branch, driveId) {
|
|
|
2908
2862
|
//#endregion
|
|
2909
2863
|
//#region src/executor/document-action-handler.ts
|
|
2910
2864
|
var DocumentActionHandler = class {
|
|
2911
|
-
constructor(registry, logger) {
|
|
2865
|
+
constructor(registry, logger, driveContainerTypes) {
|
|
2912
2866
|
this.registry = registry;
|
|
2913
2867
|
this.logger = logger;
|
|
2868
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
2914
2869
|
}
|
|
2915
2870
|
async execute(job, action, startTime, indexTxn, stores, skip = 0, sourceRemote = "", signal) {
|
|
2916
2871
|
switch (action.type) {
|
|
@@ -2919,6 +2874,7 @@ var DocumentActionHandler = class {
|
|
|
2919
2874
|
case "UPGRADE_DOCUMENT": return this.executeUpgrade(job, action, startTime, indexTxn, stores, skip, sourceRemote, signal);
|
|
2920
2875
|
case "ADD_RELATIONSHIP": return this.executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
2921
2876
|
case "REMOVE_RELATIONSHIP": return this.executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
2877
|
+
case "UPDATE_RELATIONSHIP": return this.executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote, signal);
|
|
2922
2878
|
default: return buildErrorResult(job, /* @__PURE__ */ new Error(`Unknown document action type: ${action.type}`), startTime);
|
|
2923
2879
|
}
|
|
2924
2880
|
}
|
|
@@ -2943,6 +2899,10 @@ var DocumentActionHandler = class {
|
|
|
2943
2899
|
const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
2944
2900
|
if (writeError !== null) return writeError;
|
|
2945
2901
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
2902
|
+
document.operations = {
|
|
2903
|
+
...document.operations,
|
|
2904
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2905
|
+
};
|
|
2946
2906
|
stores.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
2947
2907
|
indexTxn.write([{
|
|
2948
2908
|
...operation,
|
|
@@ -2952,7 +2912,7 @@ var DocumentActionHandler = class {
|
|
|
2952
2912
|
scope: job.scope,
|
|
2953
2913
|
sourceRemote
|
|
2954
2914
|
}]);
|
|
2955
|
-
if (document.header.documentType
|
|
2915
|
+
if (this.driveContainerTypes.has(document.header.documentType)) {
|
|
2956
2916
|
const collectionId = driveCollectionId(job.branch, document.header.id);
|
|
2957
2917
|
indexTxn.createCollection(collectionId);
|
|
2958
2918
|
indexTxn.addToCollection(collectionId, document.header.id);
|
|
@@ -2990,6 +2950,10 @@ var DocumentActionHandler = class {
|
|
|
2990
2950
|
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
2991
2951
|
if (writeError !== null) return writeError;
|
|
2992
2952
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
2953
|
+
document.operations = {
|
|
2954
|
+
...document.operations,
|
|
2955
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
2956
|
+
};
|
|
2993
2957
|
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
2994
2958
|
indexTxn.write([{
|
|
2995
2959
|
...operation,
|
|
@@ -3052,6 +3016,10 @@ var DocumentActionHandler = class {
|
|
|
3052
3016
|
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime, stores, signal);
|
|
3053
3017
|
if (writeError !== null) return writeError;
|
|
3054
3018
|
updateDocumentRevision(document, job.scope, operation.index);
|
|
3019
|
+
document.operations = {
|
|
3020
|
+
...document.operations,
|
|
3021
|
+
[job.scope]: [...document.operations[job.scope] ?? [], operation]
|
|
3022
|
+
};
|
|
3055
3023
|
stores.writeCache.putState(documentId, job.scope, job.branch, operation.index, document);
|
|
3056
3024
|
indexTxn.write([{
|
|
3057
3025
|
...operation,
|
|
@@ -3068,66 +3036,40 @@ var DocumentActionHandler = class {
|
|
|
3068
3036
|
});
|
|
3069
3037
|
return buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
3070
3038
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
3079
|
-
} catch (error) {
|
|
3080
|
-
return buildErrorResult(job, /* @__PURE__ */ new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3081
|
-
}
|
|
3082
|
-
const operation = createOperation(action, getNextIndexForScope(sourceDoc, job.scope), 0, {
|
|
3083
|
-
documentId: input.sourceId,
|
|
3084
|
-
scope: job.scope,
|
|
3085
|
-
branch: job.branch
|
|
3039
|
+
executeAddRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
3040
|
+
return this.withRelationshipAction("ADD_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, (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 }) => {
|
|
3041
|
+
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3042
|
+
const collectionId = driveCollectionId(j.branch, input.sourceId);
|
|
3043
|
+
txn.addToCollection(collectionId, input.targetId);
|
|
3044
|
+
s.collectionMembershipCache.invalidate(input.targetId);
|
|
3045
|
+
}
|
|
3086
3046
|
});
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
const scopeState = sourceDoc.state[job.scope];
|
|
3096
|
-
const resultingStateObj = {
|
|
3097
|
-
header: structuredClone(sourceDoc.header),
|
|
3098
|
-
[job.scope]: scopeState === void 0 ? {} : structuredClone(scopeState)
|
|
3099
|
-
};
|
|
3100
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
3101
|
-
stores.writeCache.putState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
3102
|
-
indexTxn.write([{
|
|
3103
|
-
...operation,
|
|
3104
|
-
documentId: input.sourceId,
|
|
3105
|
-
documentType: sourceDoc.header.documentType,
|
|
3106
|
-
branch: job.branch,
|
|
3107
|
-
scope: job.scope,
|
|
3108
|
-
sourceRemote
|
|
3109
|
-
}]);
|
|
3110
|
-
if (sourceDoc.header.documentType === "powerhouse/document-drive") {
|
|
3111
|
-
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
3112
|
-
indexTxn.addToCollection(collectionId, input.targetId);
|
|
3113
|
-
stores.collectionMembershipCache.invalidate(input.targetId);
|
|
3114
|
-
}
|
|
3115
|
-
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3116
|
-
state: sourceDoc.state.document,
|
|
3117
|
-
documentType: sourceDoc.header.documentType,
|
|
3118
|
-
documentScopeRevision: operation.index + 1
|
|
3047
|
+
}
|
|
3048
|
+
executeRemoveRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
3049
|
+
return this.withRelationshipAction("REMOVE_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, null, ({ indexTxn: txn, stores: s, sourceDoc, input, job: j }) => {
|
|
3050
|
+
if (this.driveContainerTypes.has(sourceDoc.header.documentType)) {
|
|
3051
|
+
const collectionId = driveCollectionId(j.branch, input.sourceId);
|
|
3052
|
+
txn.removeFromCollection(collectionId, input.targetId);
|
|
3053
|
+
s.collectionMembershipCache.invalidate(input.targetId);
|
|
3054
|
+
}
|
|
3119
3055
|
});
|
|
3120
|
-
return buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
3121
3056
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3057
|
+
executeUpdateRelationship(job, action, startTime, indexTxn, stores, sourceRemote = "", signal) {
|
|
3058
|
+
return this.withRelationshipAction("UPDATE_RELATIONSHIP", job, action, startTime, indexTxn, stores, sourceRemote, signal, null, null);
|
|
3059
|
+
}
|
|
3060
|
+
async withRelationshipAction(actionTypeName, job, action, startTime, indexTxn, stores, sourceRemote, signal, preValidate, postWrite) {
|
|
3061
|
+
if (job.scope !== "document") return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} must be in "document" scope, got "${job.scope}"`), startTime);
|
|
3124
3062
|
const input = action.input;
|
|
3125
|
-
if (!input.sourceId || !input.targetId || !input.relationshipType) return buildErrorResult(job, /* @__PURE__ */ new Error(
|
|
3063
|
+
if (!input.sourceId || !input.targetId || !input.relationshipType) return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName} action requires sourceId, targetId, and relationshipType in input`), startTime);
|
|
3064
|
+
if (preValidate !== null) {
|
|
3065
|
+
const validationError = preValidate(input);
|
|
3066
|
+
if (validationError !== null) return buildErrorResult(job, validationError, startTime);
|
|
3067
|
+
}
|
|
3126
3068
|
let sourceDoc;
|
|
3127
3069
|
try {
|
|
3128
3070
|
sourceDoc = await stores.writeCache.getState(input.sourceId, "document", job.branch, void 0, signal);
|
|
3129
3071
|
} catch (error) {
|
|
3130
|
-
return buildErrorResult(job, /* @__PURE__ */ new Error(
|
|
3072
|
+
return buildErrorResult(job, /* @__PURE__ */ new Error(`${actionTypeName}: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
3131
3073
|
}
|
|
3132
3074
|
const operation = createOperation(action, getNextIndexForScope(sourceDoc, job.scope), 0, {
|
|
3133
3075
|
documentId: input.sourceId,
|
|
@@ -3157,11 +3099,13 @@ var DocumentActionHandler = class {
|
|
|
3157
3099
|
scope: job.scope,
|
|
3158
3100
|
sourceRemote
|
|
3159
3101
|
}]);
|
|
3160
|
-
if (
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3102
|
+
if (postWrite !== null) postWrite({
|
|
3103
|
+
indexTxn,
|
|
3104
|
+
stores,
|
|
3105
|
+
sourceDoc,
|
|
3106
|
+
input,
|
|
3107
|
+
job
|
|
3108
|
+
});
|
|
3165
3109
|
stores.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
3166
3110
|
state: sourceDoc.state.document,
|
|
3167
3111
|
documentType: sourceDoc.header.documentType,
|
|
@@ -3200,7 +3144,7 @@ var SignatureVerifier = class {
|
|
|
3200
3144
|
if (!signer) continue;
|
|
3201
3145
|
if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Action ${action.id} has signer but no signatures`);
|
|
3202
3146
|
const publicKey = signer.app.key;
|
|
3203
|
-
let isValid
|
|
3147
|
+
let isValid;
|
|
3204
3148
|
try {
|
|
3205
3149
|
const tempOperation = {
|
|
3206
3150
|
id: deriveOperationId(documentId, action.scope, branch, action.id),
|
|
@@ -3226,7 +3170,7 @@ var SignatureVerifier = class {
|
|
|
3226
3170
|
if (!signer) continue;
|
|
3227
3171
|
if (signer.signatures.length === 0) throw new InvalidSignatureError(documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
3228
3172
|
const publicKey = signer.app.key;
|
|
3229
|
-
let isValid
|
|
3173
|
+
let isValid;
|
|
3230
3174
|
try {
|
|
3231
3175
|
isValid = await this.verifier(operation, publicKey);
|
|
3232
3176
|
} catch (error) {
|
|
@@ -3250,7 +3194,8 @@ const documentScopeActions = [
|
|
|
3250
3194
|
"DELETE_DOCUMENT",
|
|
3251
3195
|
"UPGRADE_DOCUMENT",
|
|
3252
3196
|
"ADD_RELATIONSHIP",
|
|
3253
|
-
"REMOVE_RELATIONSHIP"
|
|
3197
|
+
"REMOVE_RELATIONSHIP",
|
|
3198
|
+
"UPDATE_RELATIONSHIP"
|
|
3254
3199
|
];
|
|
3255
3200
|
/**
|
|
3256
3201
|
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
@@ -3260,7 +3205,7 @@ var SimpleJobExecutor = class {
|
|
|
3260
3205
|
signatureVerifierModule;
|
|
3261
3206
|
documentActionHandler;
|
|
3262
3207
|
executionScope;
|
|
3263
|
-
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier, executionScope) {
|
|
3208
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, driveContainerTypes, config, signatureVerifier, executionScope) {
|
|
3264
3209
|
this.logger = logger;
|
|
3265
3210
|
this.registry = registry;
|
|
3266
3211
|
this.operationStore = operationStore;
|
|
@@ -3269,6 +3214,7 @@ var SimpleJobExecutor = class {
|
|
|
3269
3214
|
this.operationIndex = operationIndex;
|
|
3270
3215
|
this.documentMetaCache = documentMetaCache;
|
|
3271
3216
|
this.collectionMembershipCache = collectionMembershipCache;
|
|
3217
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
3272
3218
|
this.config = {
|
|
3273
3219
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
3274
3220
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
@@ -3278,7 +3224,7 @@ var SimpleJobExecutor = class {
|
|
|
3278
3224
|
yieldDeadlineMs: config.yieldDeadlineMs ?? 50
|
|
3279
3225
|
};
|
|
3280
3226
|
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
3281
|
-
this.documentActionHandler = new DocumentActionHandler(registry, logger);
|
|
3227
|
+
this.documentActionHandler = new DocumentActionHandler(registry, logger, driveContainerTypes);
|
|
3282
3228
|
this.executionScope = executionScope ?? new DefaultExecutionScope(operationStore, operationIndex, writeCache, documentMetaCache, collectionMembershipCache);
|
|
3283
3229
|
}
|
|
3284
3230
|
/**
|
|
@@ -3513,7 +3459,7 @@ var SimpleJobExecutor = class {
|
|
|
3513
3459
|
} catch {}
|
|
3514
3460
|
if (docMeta?.state.isDeleted) return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
3515
3461
|
const scope = job.scope;
|
|
3516
|
-
let latestRevision
|
|
3462
|
+
let latestRevision;
|
|
3517
3463
|
try {
|
|
3518
3464
|
latestRevision = (await stores.operationStore.getRevisions(job.documentId, job.branch, signal)).revision[scope] ?? 0;
|
|
3519
3465
|
} catch {
|
|
@@ -3532,7 +3478,7 @@ var SimpleJobExecutor = class {
|
|
|
3532
3478
|
const ts = operation.timestampUtcMs || "";
|
|
3533
3479
|
if (ts < minIncomingTimestamp) minIncomingTimestamp = ts;
|
|
3534
3480
|
}
|
|
3535
|
-
let conflictingOps
|
|
3481
|
+
let conflictingOps;
|
|
3536
3482
|
try {
|
|
3537
3483
|
conflictingOps = (await stores.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, void 0, signal)).results;
|
|
3538
3484
|
} catch {
|
|
@@ -3547,7 +3493,9 @@ var SimpleJobExecutor = class {
|
|
|
3547
3493
|
allOpsFromMinConflictingIndex = conflictingOps;
|
|
3548
3494
|
}
|
|
3549
3495
|
}
|
|
3496
|
+
const incomingActionIds = new Set(job.operations.map((op) => op.action.id));
|
|
3550
3497
|
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
3498
|
+
if (op.index < minIncomingIndex && !incomingActionIds.has(op.action.id)) return false;
|
|
3551
3499
|
for (const laterOp of allOpsFromMinConflictingIndex) if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
3552
3500
|
if (laterOp.index - laterOp.skip <= op.index) return false;
|
|
3553
3501
|
}
|
|
@@ -3585,7 +3533,10 @@ var SimpleJobExecutor = class {
|
|
|
3585
3533
|
operationsWithContext: [],
|
|
3586
3534
|
duration: Date.now() - startTime
|
|
3587
3535
|
};
|
|
3588
|
-
const reshuffledOperations =
|
|
3536
|
+
const reshuffledOperations = existingOpsToReshuffle.length === 0 && skipCount === 0 ? incomingOpsToApply.slice().sort((a, b) => a.index - b.index).map((operation, i) => ({
|
|
3537
|
+
...operation,
|
|
3538
|
+
index: latestRevision + i
|
|
3539
|
+
})) : reshuffleByTimestamp({
|
|
3589
3540
|
index: latestRevision,
|
|
3590
3541
|
skip: skipCount
|
|
3591
3542
|
}, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
|
|
@@ -3854,10 +3805,6 @@ var BaseReadModel = class {
|
|
|
3854
3805
|
};
|
|
3855
3806
|
//#endregion
|
|
3856
3807
|
//#region src/processors/utils.ts
|
|
3857
|
-
const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
|
|
3858
|
-
function isDriveCreation(op) {
|
|
3859
|
-
return op.operation.action.type === "CREATE_DOCUMENT" && op.context.documentType === "powerhouse/document-drive";
|
|
3860
|
-
}
|
|
3861
3808
|
function isDriveDeletion(op) {
|
|
3862
3809
|
return op.operation.action.type === "DELETE_DOCUMENT";
|
|
3863
3810
|
}
|
|
@@ -3868,10 +3815,10 @@ function extractDriveHeader(op) {
|
|
|
3868
3815
|
function extractDeletedDocumentId(op) {
|
|
3869
3816
|
return op.operation.action.input.documentId ?? op.context.documentId;
|
|
3870
3817
|
}
|
|
3871
|
-
function createMinimalDriveHeader(driveId) {
|
|
3818
|
+
function createMinimalDriveHeader(driveId, documentType) {
|
|
3872
3819
|
return {
|
|
3873
3820
|
id: driveId,
|
|
3874
|
-
documentType
|
|
3821
|
+
documentType,
|
|
3875
3822
|
sig: {
|
|
3876
3823
|
publicKey: {},
|
|
3877
3824
|
nonce: ""
|
|
@@ -3916,15 +3863,17 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3916
3863
|
factoryRegistry = /* @__PURE__ */ new Map();
|
|
3917
3864
|
processorsByDrive = /* @__PURE__ */ new Map();
|
|
3918
3865
|
factoryToProcessors = /* @__PURE__ */ new Map();
|
|
3919
|
-
|
|
3866
|
+
knownDrives = /* @__PURE__ */ new Map();
|
|
3920
3867
|
cursorCache = /* @__PURE__ */ new Map();
|
|
3921
3868
|
logger;
|
|
3922
|
-
|
|
3869
|
+
driveContainerTypes;
|
|
3870
|
+
constructor(db, operationIndex, writeCache, consistencyTracker, logger, driveContainerTypes) {
|
|
3923
3871
|
super(db, operationIndex, writeCache, consistencyTracker, {
|
|
3924
3872
|
readModelId: "processor-manager",
|
|
3925
3873
|
rebuildStateOnInit: true
|
|
3926
3874
|
});
|
|
3927
3875
|
this.logger = logger;
|
|
3876
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
3928
3877
|
}
|
|
3929
3878
|
async init() {
|
|
3930
3879
|
await super.init();
|
|
@@ -3940,8 +3889,8 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3940
3889
|
if (this.factoryRegistry.has(identifier)) await this.unregisterFactory(identifier);
|
|
3941
3890
|
this.factoryRegistry.set(identifier, factory);
|
|
3942
3891
|
this.factoryToProcessors.set(identifier, /* @__PURE__ */ new Map());
|
|
3943
|
-
for (const driveId of this.
|
|
3944
|
-
const driveHeader = createMinimalDriveHeader(driveId);
|
|
3892
|
+
for (const [driveId, documentType] of this.knownDrives) {
|
|
3893
|
+
const driveHeader = createMinimalDriveHeader(driveId, documentType);
|
|
3945
3894
|
await this.createProcessorsForDrive(driveId, identifier, factory, driveHeader);
|
|
3946
3895
|
}
|
|
3947
3896
|
}
|
|
@@ -3972,34 +3921,37 @@ var ProcessorManager = class extends BaseReadModel {
|
|
|
3972
3921
|
}
|
|
3973
3922
|
async detectAndRegisterNewDrives(operations) {
|
|
3974
3923
|
for (const op of operations) {
|
|
3975
|
-
if (!isDriveCreation(op)) continue;
|
|
3924
|
+
if (!this.isDriveCreation(op)) continue;
|
|
3976
3925
|
const driveId = op.context.documentId;
|
|
3977
|
-
if (this.
|
|
3978
|
-
this.
|
|
3926
|
+
if (this.knownDrives.has(driveId)) continue;
|
|
3927
|
+
this.knownDrives.set(driveId, op.context.documentType);
|
|
3979
3928
|
const driveHeader = extractDriveHeader(op);
|
|
3980
3929
|
if (!driveHeader) continue;
|
|
3981
3930
|
for (const [identifier, factory] of this.factoryRegistry) await this.createProcessorsForDrive(driveId, identifier, factory, driveHeader);
|
|
3982
3931
|
}
|
|
3983
3932
|
}
|
|
3933
|
+
isDriveCreation(op) {
|
|
3934
|
+
return op.operation.action.type === "CREATE_DOCUMENT" && this.driveContainerTypes.has(op.context.documentType);
|
|
3935
|
+
}
|
|
3984
3936
|
async detectAndCleanupDeletedDrives(operations) {
|
|
3985
3937
|
for (const op of operations) {
|
|
3986
3938
|
if (!isDriveDeletion(op)) continue;
|
|
3987
3939
|
const driveId = extractDeletedDocumentId(op);
|
|
3988
|
-
if (!driveId || !this.
|
|
3940
|
+
if (!driveId || !this.knownDrives.has(driveId)) continue;
|
|
3989
3941
|
if (!this.isDeletedDocumentADrive(driveId)) continue;
|
|
3990
3942
|
await this.cleanupDriveProcessors(driveId);
|
|
3991
|
-
this.
|
|
3943
|
+
this.knownDrives.delete(driveId);
|
|
3992
3944
|
}
|
|
3993
3945
|
}
|
|
3994
3946
|
async discoverExistingDrives() {
|
|
3995
|
-
const drives = await this.db.selectFrom("DocumentSnapshot").select("documentId").where("documentType", "
|
|
3996
|
-
for (const drive of drives) this.
|
|
3947
|
+
const drives = await this.db.selectFrom("DocumentSnapshot").select(["documentId", "documentType"]).where("documentType", "in", [...this.driveContainerTypes]).where("isDeleted", "=", false).execute();
|
|
3948
|
+
for (const drive of drives) this.knownDrives.set(drive.documentId, drive.documentType);
|
|
3997
3949
|
}
|
|
3998
3950
|
isDeletedDocumentADrive(documentId) {
|
|
3999
|
-
return this.
|
|
3951
|
+
return this.knownDrives.has(documentId);
|
|
4000
3952
|
}
|
|
4001
3953
|
async createProcessorsForDrive(driveId, identifier, factory, driveHeader) {
|
|
4002
|
-
let records
|
|
3954
|
+
let records;
|
|
4003
3955
|
try {
|
|
4004
3956
|
records = await factory(driveHeader);
|
|
4005
3957
|
} catch (error) {
|
|
@@ -4668,11 +4620,11 @@ var KyselyDocumentView = class extends BaseReadModel {
|
|
|
4668
4620
|
const { documentId, scope, branch, documentType, resultingState } = context;
|
|
4669
4621
|
const { index, hash } = operation;
|
|
4670
4622
|
if (!resultingState) throw new Error(`Missing resultingState in context for operation ${operation.id || "unknown"}. IDocumentView requires resultingState from upstream - it does not rebuild documents.`);
|
|
4671
|
-
let fullState
|
|
4623
|
+
let fullState;
|
|
4672
4624
|
try {
|
|
4673
4625
|
fullState = JSON.parse(resultingState);
|
|
4674
4626
|
} catch (error) {
|
|
4675
|
-
throw new Error(`Failed to parse resultingState for operation ${operation.id || "unknown"}: ${error instanceof Error ? error.message : String(error)}
|
|
4627
|
+
throw new Error(`Failed to parse resultingState for operation ${operation.id || "unknown"}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4676
4628
|
}
|
|
4677
4629
|
const operationType = operation.action.type;
|
|
4678
4630
|
if (operationType === "DELETE_DOCUMENT") {
|
|
@@ -4995,6 +4947,142 @@ var NullDocumentModelResolver = class {
|
|
|
4995
4947
|
}
|
|
4996
4948
|
};
|
|
4997
4949
|
//#endregion
|
|
4950
|
+
//#region src/registry/implementation.ts
|
|
4951
|
+
/**
|
|
4952
|
+
* In-memory implementation of the IDocumentModelRegistry interface.
|
|
4953
|
+
* Manages document model modules with version-aware storage and upgrade manifest support.
|
|
4954
|
+
*/
|
|
4955
|
+
var DocumentModelRegistry = class {
|
|
4956
|
+
modules = [];
|
|
4957
|
+
manifests = [];
|
|
4958
|
+
registerModules(...modules) {
|
|
4959
|
+
return modules.map((module) => {
|
|
4960
|
+
try {
|
|
4961
|
+
const documentType = module.documentModel.global.id;
|
|
4962
|
+
const version = module.version ?? 1;
|
|
4963
|
+
for (let i = 0; i < this.modules.length; i++) {
|
|
4964
|
+
const existing = this.modules[i];
|
|
4965
|
+
const existingType = existing.documentModel.global.id;
|
|
4966
|
+
const existingVersion = existing.version ?? 1;
|
|
4967
|
+
if (existingType === documentType && existingVersion === version) throw new DuplicateModuleError(documentType, version);
|
|
4968
|
+
}
|
|
4969
|
+
this.modules.push(module);
|
|
4970
|
+
return {
|
|
4971
|
+
status: "success",
|
|
4972
|
+
item: module
|
|
4973
|
+
};
|
|
4974
|
+
} catch (error) {
|
|
4975
|
+
return {
|
|
4976
|
+
status: "error",
|
|
4977
|
+
item: module,
|
|
4978
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
unregisterModules(...documentTypes) {
|
|
4984
|
+
let allFound = true;
|
|
4985
|
+
for (const documentType of documentTypes) {
|
|
4986
|
+
if (!this.modules.some((m) => m.documentModel.global.id === documentType)) allFound = false;
|
|
4987
|
+
this.modules = this.modules.filter((m) => m.documentModel.global.id !== documentType);
|
|
4988
|
+
}
|
|
4989
|
+
return allFound;
|
|
4990
|
+
}
|
|
4991
|
+
getModule(documentType, version) {
|
|
4992
|
+
let latestModule;
|
|
4993
|
+
let latestVersion = -1;
|
|
4994
|
+
for (let i = 0; i < this.modules.length; i++) {
|
|
4995
|
+
const module = this.modules[i];
|
|
4996
|
+
const moduleType = module.documentModel.global.id;
|
|
4997
|
+
const moduleVersion = module.version ?? 1;
|
|
4998
|
+
if (moduleType === documentType) {
|
|
4999
|
+
if (version !== void 0 && moduleVersion === version) return module;
|
|
5000
|
+
if (moduleVersion > latestVersion) {
|
|
5001
|
+
latestModule = module;
|
|
5002
|
+
latestVersion = moduleVersion;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
if (version === void 0 && latestModule !== void 0) return latestModule;
|
|
5007
|
+
throw new ModuleNotFoundError(documentType, version);
|
|
5008
|
+
}
|
|
5009
|
+
getAllModules() {
|
|
5010
|
+
return [...this.modules];
|
|
5011
|
+
}
|
|
5012
|
+
clear() {
|
|
5013
|
+
this.modules = [];
|
|
5014
|
+
this.manifests = [];
|
|
5015
|
+
}
|
|
5016
|
+
getSupportedVersions(documentType) {
|
|
5017
|
+
const versions = [];
|
|
5018
|
+
for (const module of this.modules) if (module.documentModel.global.id === documentType) versions.push(module.version ?? 1);
|
|
5019
|
+
if (versions.length === 0) throw new ModuleNotFoundError(documentType);
|
|
5020
|
+
return versions.sort((a, b) => a - b);
|
|
5021
|
+
}
|
|
5022
|
+
getLatestVersion(documentType) {
|
|
5023
|
+
let latest = -1;
|
|
5024
|
+
let found = false;
|
|
5025
|
+
for (const module of this.modules) if (module.documentModel.global.id === documentType) {
|
|
5026
|
+
found = true;
|
|
5027
|
+
const version = module.version ?? 1;
|
|
5028
|
+
if (version > latest) latest = version;
|
|
5029
|
+
}
|
|
5030
|
+
if (!found) throw new ModuleNotFoundError(documentType);
|
|
5031
|
+
return latest;
|
|
5032
|
+
}
|
|
5033
|
+
registerUpgradeManifests(...manifestsToRegister) {
|
|
5034
|
+
return manifestsToRegister.map((manifestToRegister) => {
|
|
5035
|
+
try {
|
|
5036
|
+
if (!manifestToRegister.documentType) throw new Error("Upgrade manifest is missing a documentType");
|
|
5037
|
+
for (const registeredManifest of this.manifests) if (registeredManifest.documentType === manifestToRegister.documentType) throw new DuplicateManifestError(manifestToRegister.documentType);
|
|
5038
|
+
this.manifests.push(manifestToRegister);
|
|
5039
|
+
return {
|
|
5040
|
+
status: "success",
|
|
5041
|
+
item: manifestToRegister
|
|
5042
|
+
};
|
|
5043
|
+
} catch (error) {
|
|
5044
|
+
return {
|
|
5045
|
+
status: "error",
|
|
5046
|
+
item: manifestToRegister,
|
|
5047
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
unregisterUpgradeManifests(...documentTypes) {
|
|
5053
|
+
let allFound = true;
|
|
5054
|
+
for (const documentType of documentTypes) {
|
|
5055
|
+
if (!this.manifests.some((m) => m.documentType === documentType)) allFound = false;
|
|
5056
|
+
this.manifests = this.manifests.filter((m) => m.documentType !== documentType);
|
|
5057
|
+
}
|
|
5058
|
+
return allFound;
|
|
5059
|
+
}
|
|
5060
|
+
getUpgradeManifest(documentType) {
|
|
5061
|
+
for (let i = 0; i < this.manifests.length; i++) if (this.manifests[i].documentType === documentType) return this.manifests[i];
|
|
5062
|
+
throw new ManifestNotFoundError(documentType);
|
|
5063
|
+
}
|
|
5064
|
+
computeUpgradePath(documentType, fromVersion, toVersion) {
|
|
5065
|
+
if (fromVersion === toVersion) return [];
|
|
5066
|
+
if (toVersion < fromVersion) throw new DowngradeNotSupportedError(documentType, fromVersion, toVersion);
|
|
5067
|
+
const manifest = this.getUpgradeManifest(documentType);
|
|
5068
|
+
const path = [];
|
|
5069
|
+
for (let v = fromVersion + 1; v <= toVersion; v++) {
|
|
5070
|
+
const key = `v${v}`;
|
|
5071
|
+
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, v - 1, v);
|
|
5072
|
+
const transition = manifest.upgrades[key];
|
|
5073
|
+
path.push(transition);
|
|
5074
|
+
}
|
|
5075
|
+
return path;
|
|
5076
|
+
}
|
|
5077
|
+
getUpgradeReducer(documentType, fromVersion, toVersion) {
|
|
5078
|
+
if (toVersion !== fromVersion + 1) throw new InvalidUpgradeStepError(documentType, fromVersion, toVersion);
|
|
5079
|
+
const manifest = this.getUpgradeManifest(documentType);
|
|
5080
|
+
const key = `v${toVersion}`;
|
|
5081
|
+
if (!(key in manifest.upgrades)) throw new MissingUpgradeTransitionError(documentType, fromVersion, toVersion);
|
|
5082
|
+
return manifest.upgrades[key].upgradeReducer;
|
|
5083
|
+
}
|
|
5084
|
+
};
|
|
5085
|
+
//#endregion
|
|
4998
5086
|
//#region src/shared/consistency-tracker.ts
|
|
4999
5087
|
/**
|
|
5000
5088
|
* Creates a consistency key from documentId, scope, and branch.
|
|
@@ -5126,6 +5214,7 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
|
5126
5214
|
const actionType = operation.action.type;
|
|
5127
5215
|
if (actionType === "ADD_RELATIONSHIP") await this.handleAddRelationship(trx, operation);
|
|
5128
5216
|
else if (actionType === "REMOVE_RELATIONSHIP") await this.handleRemoveRelationship(trx, operation);
|
|
5217
|
+
else if (actionType === "UPDATE_RELATIONSHIP") await this.handleUpdateRelationship(trx, operation);
|
|
5129
5218
|
}
|
|
5130
5219
|
});
|
|
5131
5220
|
}
|
|
@@ -5340,6 +5429,13 @@ var KyselyDocumentIndexer = class extends BaseReadModel {
|
|
|
5340
5429
|
const input = operation.action.input;
|
|
5341
5430
|
await trx.deleteFrom("DocumentRelationship").where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
|
|
5342
5431
|
}
|
|
5432
|
+
async handleUpdateRelationship(trx, operation) {
|
|
5433
|
+
const input = operation.action.input;
|
|
5434
|
+
await trx.updateTable("DocumentRelationship").set({
|
|
5435
|
+
metadata: input.metadata,
|
|
5436
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
5437
|
+
}).where("sourceId", "=", input.sourceId).where("targetId", "=", input.targetId).where("relationshipType", "=", input.relationshipType).execute();
|
|
5438
|
+
}
|
|
5343
5439
|
};
|
|
5344
5440
|
//#endregion
|
|
5345
5441
|
//#region src/storage/kysely/keyframe-store.ts
|
|
@@ -5403,6 +5499,29 @@ var KyselyKeyframeStore = class KyselyKeyframeStore {
|
|
|
5403
5499
|
}
|
|
5404
5500
|
};
|
|
5405
5501
|
//#endregion
|
|
5502
|
+
//#region src/storage/kysely/pagination.ts
|
|
5503
|
+
const DEFAULT_LIMIT = 100;
|
|
5504
|
+
function paginateRows(rows, paging, cursorOf, toItem, refetch) {
|
|
5505
|
+
let hasMore = false;
|
|
5506
|
+
let items = rows;
|
|
5507
|
+
if (paging?.limit && rows.length > paging.limit) {
|
|
5508
|
+
hasMore = true;
|
|
5509
|
+
items = rows.slice(0, paging.limit);
|
|
5510
|
+
}
|
|
5511
|
+
const nextCursor = hasMore && items.length > 0 ? cursorOf(items[items.length - 1]).toString() : void 0;
|
|
5512
|
+
const cursor = paging?.cursor || "0";
|
|
5513
|
+
const limit = paging?.limit || DEFAULT_LIMIT;
|
|
5514
|
+
return {
|
|
5515
|
+
results: items.map(toItem),
|
|
5516
|
+
options: {
|
|
5517
|
+
cursor,
|
|
5518
|
+
limit
|
|
5519
|
+
},
|
|
5520
|
+
nextCursor,
|
|
5521
|
+
next: hasMore ? () => refetch(nextCursor, limit) : void 0
|
|
5522
|
+
};
|
|
5523
|
+
}
|
|
5524
|
+
//#endregion
|
|
5406
5525
|
//#region src/storage/interfaces.ts
|
|
5407
5526
|
/**
|
|
5408
5527
|
* Thrown when an operation with the same identity already exists in the store.
|
|
@@ -5486,7 +5605,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
5486
5605
|
});
|
|
5487
5606
|
}
|
|
5488
5607
|
async executeApply(trx, documentId, documentType, scope, branch, revision, fn, signal) {
|
|
5489
|
-
|
|
5608
|
+
throwIfAborted(signal);
|
|
5490
5609
|
const latestOp = await trx.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).orderBy("index", "desc").limit(1).executeTakeFirst();
|
|
5491
5610
|
const currentRevision = latestOp ? latestOp.index : -1;
|
|
5492
5611
|
if (currentRevision !== revision - 1) throw new RevisionMismatchError(currentRevision + 1, revision);
|
|
@@ -5514,7 +5633,7 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
5514
5633
|
}
|
|
5515
5634
|
}
|
|
5516
5635
|
async getSince(documentId, scope, branch, revision, filter, paging, signal) {
|
|
5517
|
-
|
|
5636
|
+
throwIfAborted(signal);
|
|
5518
5637
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("index", ">", revision).orderBy("index", "asc");
|
|
5519
5638
|
if (filter) {
|
|
5520
5639
|
if (filter.actionTypes && filter.actionTypes.length > 0) {
|
|
@@ -5530,93 +5649,39 @@ var KyselyOperationStore = class KyselyOperationStore {
|
|
|
5530
5649
|
if (cursorValue > 0) query = query.where("index", ">", cursorValue);
|
|
5531
5650
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
5532
5651
|
}
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
hasMore = true;
|
|
5538
|
-
items = rows.slice(0, paging.limit);
|
|
5539
|
-
}
|
|
5540
|
-
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].index.toString() : void 0;
|
|
5541
|
-
const cursor = paging?.cursor || "0";
|
|
5542
|
-
const limit = paging?.limit || 100;
|
|
5543
|
-
return {
|
|
5544
|
-
results: items.map((row) => this.rowToOperation(row)),
|
|
5545
|
-
options: {
|
|
5546
|
-
cursor,
|
|
5547
|
-
limit
|
|
5548
|
-
},
|
|
5549
|
-
nextCursor,
|
|
5550
|
-
next: hasMore ? () => this.getSince(documentId, scope, branch, revision, filter, {
|
|
5551
|
-
cursor: nextCursor,
|
|
5552
|
-
limit
|
|
5553
|
-
}, signal) : void 0
|
|
5554
|
-
};
|
|
5652
|
+
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getSince(documentId, scope, branch, revision, filter, {
|
|
5653
|
+
cursor,
|
|
5654
|
+
limit
|
|
5655
|
+
}, signal));
|
|
5555
5656
|
}
|
|
5556
5657
|
async getSinceId(id, paging, signal) {
|
|
5557
|
-
|
|
5658
|
+
throwIfAborted(signal);
|
|
5558
5659
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("id", ">", id).orderBy("id", "asc");
|
|
5559
5660
|
if (paging) {
|
|
5560
5661
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
5561
5662
|
if (cursorValue > 0) query = query.where("id", ">", cursorValue);
|
|
5562
5663
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
5563
5664
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
hasMore = true;
|
|
5569
|
-
items = rows.slice(0, paging.limit);
|
|
5570
|
-
}
|
|
5571
|
-
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].id.toString() : void 0;
|
|
5572
|
-
const cursor = paging?.cursor || "0";
|
|
5573
|
-
const limit = paging?.limit || 100;
|
|
5574
|
-
return {
|
|
5575
|
-
results: items.map((row) => this.rowToOperationWithContext(row)),
|
|
5576
|
-
options: {
|
|
5577
|
-
cursor,
|
|
5578
|
-
limit
|
|
5579
|
-
},
|
|
5580
|
-
nextCursor,
|
|
5581
|
-
next: hasMore ? () => this.getSinceId(id, {
|
|
5582
|
-
cursor: nextCursor,
|
|
5583
|
-
limit
|
|
5584
|
-
}, signal) : void 0
|
|
5585
|
-
};
|
|
5665
|
+
return paginateRows(await query.execute(), paging, (row) => row.id, (row) => this.rowToOperationWithContext(row), (cursor, limit) => this.getSinceId(id, {
|
|
5666
|
+
cursor,
|
|
5667
|
+
limit
|
|
5668
|
+
}, signal));
|
|
5586
5669
|
}
|
|
5587
5670
|
async getConflicting(documentId, scope, branch, minTimestamp, paging, signal) {
|
|
5588
|
-
|
|
5671
|
+
throwIfAborted(signal);
|
|
5589
5672
|
let query = this.queryExecutor.selectFrom("Operation").selectAll().where("documentId", "=", documentId).where("scope", "=", scope).where("branch", "=", branch).where("timestampUtcMs", ">=", new Date(minTimestamp)).orderBy("index", "asc");
|
|
5590
5673
|
if (paging) {
|
|
5591
5674
|
const cursorValue = Number.parseInt(paging.cursor, 10);
|
|
5592
5675
|
if (cursorValue > 0) query = query.where("index", ">", cursorValue);
|
|
5593
5676
|
if (paging.limit) query = query.limit(paging.limit + 1);
|
|
5594
5677
|
}
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
hasMore = true;
|
|
5600
|
-
items = rows.slice(0, paging.limit);
|
|
5601
|
-
}
|
|
5602
|
-
const nextCursor = hasMore && items.length > 0 ? items[items.length - 1].index.toString() : void 0;
|
|
5603
|
-
const cursor = paging?.cursor || "0";
|
|
5604
|
-
const limit = paging?.limit || 100;
|
|
5605
|
-
return {
|
|
5606
|
-
results: items.map((row) => this.rowToOperation(row)),
|
|
5607
|
-
options: {
|
|
5608
|
-
cursor,
|
|
5609
|
-
limit
|
|
5610
|
-
},
|
|
5611
|
-
nextCursor,
|
|
5612
|
-
next: hasMore ? () => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
5613
|
-
cursor: nextCursor,
|
|
5614
|
-
limit
|
|
5615
|
-
}, signal) : void 0
|
|
5616
|
-
};
|
|
5678
|
+
return paginateRows(await query.execute(), paging, (row) => row.index, (row) => this.rowToOperation(row), (cursor, limit) => this.getConflicting(documentId, scope, branch, minTimestamp, {
|
|
5679
|
+
cursor,
|
|
5680
|
+
limit
|
|
5681
|
+
}, signal));
|
|
5617
5682
|
}
|
|
5618
5683
|
async getRevisions(documentId, branch, signal) {
|
|
5619
|
-
|
|
5684
|
+
throwIfAborted(signal);
|
|
5620
5685
|
const scopeRevisions = await this.queryExecutor.selectFrom("Operation as o1").select([
|
|
5621
5686
|
"o1.scope",
|
|
5622
5687
|
"o1.index",
|
|
@@ -6610,6 +6675,38 @@ function batchOperationsByDocument(operations) {
|
|
|
6610
6675
|
flushBatch();
|
|
6611
6676
|
return batches;
|
|
6612
6677
|
}
|
|
6678
|
+
/**
|
|
6679
|
+
* Splits a sorted page of operations into a safe-to-emit prefix and a
|
|
6680
|
+
* deferred tail containing the trailing run that shares the same
|
|
6681
|
+
* (documentId, branch, scope, timestampUtcMs) as the last operation.
|
|
6682
|
+
*
|
|
6683
|
+
* The page is assumed to be sorted by (documentId, scope, ordinal), so a
|
|
6684
|
+
* same-(docId, scope, ts) run is contiguous and lives at the end of any
|
|
6685
|
+
* page that contains its last member. Holding that tail back lets callers
|
|
6686
|
+
* prepend it to the next page so a single producer-side execute() call
|
|
6687
|
+
* never gets split across two outbound envelopes.
|
|
6688
|
+
*/
|
|
6689
|
+
function splitTrailingSameTimestampRun(operations) {
|
|
6690
|
+
if (operations.length === 0) return {
|
|
6691
|
+
emit: [],
|
|
6692
|
+
carry: []
|
|
6693
|
+
};
|
|
6694
|
+
const last = operations[operations.length - 1];
|
|
6695
|
+
const lastDocId = last.context.documentId;
|
|
6696
|
+
const lastBranch = last.context.branch;
|
|
6697
|
+
const lastScope = last.context.scope;
|
|
6698
|
+
const lastTs = last.operation.timestampUtcMs;
|
|
6699
|
+
let carryStart = operations.length;
|
|
6700
|
+
for (let i = operations.length - 1; i >= 0; i--) {
|
|
6701
|
+
const op = operations[i];
|
|
6702
|
+
if (op.context.documentId === lastDocId && op.context.branch === lastBranch && op.context.scope === lastScope && op.operation.timestampUtcMs === lastTs) carryStart = i;
|
|
6703
|
+
else break;
|
|
6704
|
+
}
|
|
6705
|
+
return {
|
|
6706
|
+
emit: operations.slice(0, carryStart),
|
|
6707
|
+
carry: operations.slice(carryStart)
|
|
6708
|
+
};
|
|
6709
|
+
}
|
|
6613
6710
|
function toOperationWithContext(entry) {
|
|
6614
6711
|
return {
|
|
6615
6712
|
operation: {
|
|
@@ -6676,25 +6773,6 @@ function consolidateSyncOperations(syncOps) {
|
|
|
6676
6773
|
}
|
|
6677
6774
|
return result;
|
|
6678
6775
|
}
|
|
6679
|
-
function mergeCollectionMemberships(events) {
|
|
6680
|
-
const mergedMemberships = {};
|
|
6681
|
-
for (const event of events) {
|
|
6682
|
-
if (event.collectionMemberships) for (const [docId, collections] of Object.entries(event.collectionMemberships)) {
|
|
6683
|
-
if (!(docId in mergedMemberships)) mergedMemberships[docId] = [];
|
|
6684
|
-
for (const c of collections) if (!mergedMemberships[docId].includes(c)) mergedMemberships[docId].push(c);
|
|
6685
|
-
}
|
|
6686
|
-
for (const op of event.operations) {
|
|
6687
|
-
const action = op.operation.action;
|
|
6688
|
-
if (action.type !== "ADD_RELATIONSHIP") continue;
|
|
6689
|
-
const input = action.input;
|
|
6690
|
-
if (!input?.sourceId || !input.targetId) continue;
|
|
6691
|
-
const collectionId = driveCollectionId(op.context.branch, input.sourceId);
|
|
6692
|
-
if (!(input.targetId in mergedMemberships)) mergedMemberships[input.targetId] = [];
|
|
6693
|
-
if (!mergedMemberships[input.targetId].includes(collectionId)) mergedMemberships[input.targetId].push(collectionId);
|
|
6694
|
-
}
|
|
6695
|
-
}
|
|
6696
|
-
return mergedMemberships;
|
|
6697
|
-
}
|
|
6698
6776
|
/**
|
|
6699
6777
|
* Chunks sync operations into batches that respect dependency-connected
|
|
6700
6778
|
* components. SyncOps linked by jobDependencies are kept in the same chunk.
|
|
@@ -8023,12 +8101,14 @@ var KyselySyncRemoteStorage = class {
|
|
|
8023
8101
|
//#region src/sync/batch-aggregator.ts
|
|
8024
8102
|
var BatchAggregator = class {
|
|
8025
8103
|
logger;
|
|
8104
|
+
driveContainerTypes;
|
|
8026
8105
|
onBatchReady;
|
|
8027
8106
|
queue = [];
|
|
8028
8107
|
processing = false;
|
|
8029
8108
|
pendingBatches = /* @__PURE__ */ new Map();
|
|
8030
|
-
constructor(logger, onBatchReady) {
|
|
8109
|
+
constructor(logger, driveContainerTypes, onBatchReady) {
|
|
8031
8110
|
this.logger = logger;
|
|
8111
|
+
this.driveContainerTypes = driveContainerTypes;
|
|
8032
8112
|
this.onBatchReady = onBatchReady;
|
|
8033
8113
|
}
|
|
8034
8114
|
async enqueueWriteReady(event) {
|
|
@@ -8087,7 +8167,7 @@ var BatchAggregator = class {
|
|
|
8087
8167
|
}
|
|
8088
8168
|
}
|
|
8089
8169
|
prepareBatch(events) {
|
|
8090
|
-
const collectionMemberships = mergeCollectionMemberships(events);
|
|
8170
|
+
const collectionMemberships = this.mergeCollectionMemberships(events);
|
|
8091
8171
|
const isBatch = events.length > 1;
|
|
8092
8172
|
const priorJobIds = [];
|
|
8093
8173
|
const entries = [];
|
|
@@ -8103,6 +8183,26 @@ var BatchAggregator = class {
|
|
|
8103
8183
|
entries
|
|
8104
8184
|
};
|
|
8105
8185
|
}
|
|
8186
|
+
mergeCollectionMemberships(events) {
|
|
8187
|
+
const mergedMemberships = {};
|
|
8188
|
+
for (const event of events) {
|
|
8189
|
+
if (event.collectionMemberships) for (const [docId, collections] of Object.entries(event.collectionMemberships)) {
|
|
8190
|
+
if (!(docId in mergedMemberships)) mergedMemberships[docId] = [];
|
|
8191
|
+
for (const c of collections) if (!mergedMemberships[docId].includes(c)) mergedMemberships[docId].push(c);
|
|
8192
|
+
}
|
|
8193
|
+
for (const op of event.operations) {
|
|
8194
|
+
const action = op.operation.action;
|
|
8195
|
+
if (action.type !== "ADD_RELATIONSHIP") continue;
|
|
8196
|
+
if (!this.driveContainerTypes.has(op.context.documentType)) continue;
|
|
8197
|
+
const input = action.input;
|
|
8198
|
+
if (!input?.sourceId || !input.targetId) continue;
|
|
8199
|
+
const collectionId = driveCollectionId(op.context.branch, input.sourceId);
|
|
8200
|
+
if (!(input.targetId in mergedMemberships)) mergedMemberships[input.targetId] = [];
|
|
8201
|
+
if (!mergedMemberships[input.targetId].includes(collectionId)) mergedMemberships[input.targetId].push(collectionId);
|
|
8202
|
+
}
|
|
8203
|
+
}
|
|
8204
|
+
return mergedMemberships;
|
|
8205
|
+
}
|
|
8106
8206
|
};
|
|
8107
8207
|
//#endregion
|
|
8108
8208
|
//#region src/sync/sync-awaiter.ts
|
|
@@ -8354,7 +8454,7 @@ var SyncManager = class {
|
|
|
8354
8454
|
planKeyToJobUuid = /* @__PURE__ */ new Map();
|
|
8355
8455
|
lastEnqueuedJobIdByKey = /* @__PURE__ */ new Map();
|
|
8356
8456
|
inboxChunkChain = Promise.resolve();
|
|
8357
|
-
constructor(logger, remoteStorage, cursorStorage, deadLetterStorage, channelFactory, operationIndex, reactor, eventBus, config = {}) {
|
|
8457
|
+
constructor(logger, remoteStorage, cursorStorage, deadLetterStorage, channelFactory, operationIndex, reactor, eventBus, driveContainerTypes, config = {}) {
|
|
8358
8458
|
this.logger = logger;
|
|
8359
8459
|
this.remoteStorage = remoteStorage;
|
|
8360
8460
|
this.cursorStorage = cursorStorage;
|
|
@@ -8371,7 +8471,7 @@ var SyncManager = class {
|
|
|
8371
8471
|
this.awaiter = new JobAwaiter(eventBus, (jobId, signal) => reactor.getJobStatus(jobId, signal));
|
|
8372
8472
|
this.syncAwaiter = new SyncAwaiter(eventBus);
|
|
8373
8473
|
this.isShutdown = false;
|
|
8374
|
-
this.batchAggregator = new BatchAggregator(logger, (batch) => this.processCompleteBatch(batch));
|
|
8474
|
+
this.batchAggregator = new BatchAggregator(logger, driveContainerTypes, (batch) => this.processCompleteBatch(batch));
|
|
8375
8475
|
this.syncStatusTracker = new SyncStatusTracker();
|
|
8376
8476
|
}
|
|
8377
8477
|
async startup() {
|
|
@@ -8744,8 +8844,8 @@ var SyncManager = class {
|
|
|
8744
8844
|
}
|
|
8745
8845
|
if (this.isShutdown) return;
|
|
8746
8846
|
for (const plan of jobs) {
|
|
8847
|
+
if (!(plan.key in result.jobs)) continue;
|
|
8747
8848
|
const info = result.jobs[plan.key];
|
|
8748
|
-
if (!info) continue;
|
|
8749
8849
|
this.recordPlanKeyMapping(plan.key, info.id);
|
|
8750
8850
|
const fifoKey = `${plan.documentId}:${plan.scope}:${plan.branch}`;
|
|
8751
8851
|
this.lastEnqueuedJobIdByKey.set(fifoKey, info.id);
|
|
@@ -8785,41 +8885,55 @@ var SyncManager = class {
|
|
|
8785
8885
|
const composedSignal = signal ? AbortSignal.any([signal, this.abortController.signal]) : this.abortController.signal;
|
|
8786
8886
|
let maxOrdinal = ackOrdinal;
|
|
8787
8887
|
const lastJobByDoc = /* @__PURE__ */ new Map();
|
|
8888
|
+
let prevChainJobId;
|
|
8788
8889
|
const sinceTimestamp = remote.options.sinceTimestampUtcMs;
|
|
8890
|
+
const emitBatches = (operations) => {
|
|
8891
|
+
if (operations.length === 0) return;
|
|
8892
|
+
const batches = batchOperationsByDocument(operations);
|
|
8893
|
+
const syncOps = [];
|
|
8894
|
+
for (const batch of batches) {
|
|
8895
|
+
const jobId = crypto.randomUUID();
|
|
8896
|
+
const prevJobId = lastJobByDoc.get(batch.documentId);
|
|
8897
|
+
const deps = [];
|
|
8898
|
+
if (prevJobId) deps.push(prevJobId);
|
|
8899
|
+
if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
|
|
8900
|
+
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
8901
|
+
syncOps.push(syncOp);
|
|
8902
|
+
lastJobByDoc.set(batch.documentId, jobId);
|
|
8903
|
+
if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
|
|
8904
|
+
}
|
|
8905
|
+
remote.channel.outbox.add(...syncOps);
|
|
8906
|
+
};
|
|
8789
8907
|
let page = await this.operationIndex.find(remote.collectionId, ackOrdinal, { excludeSourceRemote: remote.name }, void 0, composedSignal);
|
|
8908
|
+
let carry = [];
|
|
8790
8909
|
let hasMore;
|
|
8791
8910
|
do {
|
|
8792
8911
|
if (composedSignal.aborted) return;
|
|
8793
8912
|
for (const entry of page.results) maxOrdinal = Math.max(maxOrdinal, entry.ordinal ?? 0);
|
|
8794
8913
|
let operations = page.results.map((entry) => toOperationWithContext(entry));
|
|
8914
|
+
if (carry.length > 0) {
|
|
8915
|
+
operations = [...carry, ...operations];
|
|
8916
|
+
carry = [];
|
|
8917
|
+
}
|
|
8795
8918
|
if (sinceTimestamp && sinceTimestamp !== "0") operations = operations.filter((op) => op.operation.timestampUtcMs >= sinceTimestamp);
|
|
8796
8919
|
operations = filterOperations(operations, remote.filter);
|
|
8797
8920
|
operations = operations.filter((op) => !this.quarantinedDocumentIds.has(op.context.documentId));
|
|
8921
|
+
hasMore = !!page.next;
|
|
8798
8922
|
if (operations.length > 0) {
|
|
8799
8923
|
operations.sort((a, b) => {
|
|
8800
8924
|
if (a.context.documentId !== b.context.documentId) return a.context.documentId < b.context.documentId ? -1 : 1;
|
|
8801
8925
|
if (a.context.scope !== b.context.scope) return a.context.scope < b.context.scope ? -1 : 1;
|
|
8802
8926
|
return a.context.ordinal - b.context.ordinal;
|
|
8803
8927
|
});
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
const prevJobId = lastJobByDoc.get(batch.documentId);
|
|
8810
|
-
const deps = [];
|
|
8811
|
-
if (prevJobId) deps.push(prevJobId);
|
|
8812
|
-
if (mode === OutboxMode.BatchTriggered && prevChainJobId && prevChainJobId !== prevJobId) deps.push(prevChainJobId);
|
|
8813
|
-
const syncOp = new SyncOperation(crypto.randomUUID(), jobId, deps, remote.name, batch.documentId, [batch.scope], batch.branch, batch.operations);
|
|
8814
|
-
syncOps.push(syncOp);
|
|
8815
|
-
lastJobByDoc.set(batch.documentId, jobId);
|
|
8816
|
-
if (mode === OutboxMode.BatchTriggered) prevChainJobId = jobId;
|
|
8817
|
-
}
|
|
8818
|
-
remote.channel.outbox.add(...syncOps);
|
|
8928
|
+
if (hasMore) {
|
|
8929
|
+
const split = splitTrailingSameTimestampRun(operations);
|
|
8930
|
+
carry = split.carry;
|
|
8931
|
+
emitBatches(split.emit);
|
|
8932
|
+
} else emitBatches(operations);
|
|
8819
8933
|
}
|
|
8820
|
-
hasMore = !!page.next;
|
|
8821
8934
|
if (hasMore) page = await page.next();
|
|
8822
8935
|
} while (hasMore);
|
|
8936
|
+
if (carry.length > 0) emitBatches(carry);
|
|
8823
8937
|
remote.channel.outbox.advanceOrdinal(maxOrdinal);
|
|
8824
8938
|
}
|
|
8825
8939
|
};
|
|
@@ -8855,15 +8969,15 @@ var SyncBuilder = class {
|
|
|
8855
8969
|
this.config.maxInboxBatchSize = limit;
|
|
8856
8970
|
return this;
|
|
8857
8971
|
}
|
|
8858
|
-
build(reactor, logger, operationIndex, eventBus, db) {
|
|
8859
|
-
return this.buildModule(reactor, logger, operationIndex, eventBus, db).syncManager;
|
|
8972
|
+
build(reactor, logger, operationIndex, eventBus, db, driveContainerTypes) {
|
|
8973
|
+
return this.buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes).syncManager;
|
|
8860
8974
|
}
|
|
8861
|
-
buildModule(reactor, logger, operationIndex, eventBus, db) {
|
|
8975
|
+
buildModule(reactor, logger, operationIndex, eventBus, db, driveContainerTypes) {
|
|
8862
8976
|
if (!this.channelFactory) throw new Error("Channel factory is required");
|
|
8863
8977
|
const remoteStorage = this.remoteStorage ?? new KyselySyncRemoteStorage(db);
|
|
8864
8978
|
const cursorStorage = this.cursorStorage ?? new KyselySyncCursorStorage(db);
|
|
8865
8979
|
const deadLetterStorage = this.deadLetterStorage ?? new KyselySyncDeadLetterStorage(db);
|
|
8866
|
-
const syncManager = new SyncManager(logger, remoteStorage, cursorStorage, deadLetterStorage, this.channelFactory, operationIndex, reactor, eventBus, this.config);
|
|
8980
|
+
const syncManager = new SyncManager(logger, remoteStorage, cursorStorage, deadLetterStorage, this.channelFactory, operationIndex, reactor, eventBus, driveContainerTypes, this.config);
|
|
8867
8981
|
return {
|
|
8868
8982
|
remoteStorage,
|
|
8869
8983
|
cursorStorage,
|
|
@@ -8882,6 +8996,9 @@ async function createDefaultDatabase() {
|
|
|
8882
8996
|
return new Kysely({ dialect: new PGliteDialect(new PGlite()) });
|
|
8883
8997
|
}
|
|
8884
8998
|
//#endregion
|
|
8999
|
+
//#region src/core/drive-container-types.ts
|
|
9000
|
+
const DEFAULT_DRIVE_CONTAINER_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
|
|
9001
|
+
//#endregion
|
|
8885
9002
|
//#region src/shared/factories.ts
|
|
8886
9003
|
/**
|
|
8887
9004
|
* Factory method to create a ShutdownStatus that can be updated
|
|
@@ -8976,7 +9093,7 @@ var Reactor = class {
|
|
|
8976
9093
|
}
|
|
8977
9094
|
getDocumentModels(namespace, paging, signal) {
|
|
8978
9095
|
this.logger.verbose("getDocumentModels(@namespace, @paging)", namespace, paging);
|
|
8979
|
-
|
|
9096
|
+
throwIfAborted(signal, () => new AbortError());
|
|
8980
9097
|
const filteredModels = this.documentModelRegistry.getAllModules().filter((module) => !namespace || module.documentModel.global.id.startsWith(namespace));
|
|
8981
9098
|
const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
|
|
8982
9099
|
const limit = paging?.limit || filteredModels.length;
|
|
@@ -9012,24 +9129,24 @@ var Reactor = class {
|
|
|
9012
9129
|
}
|
|
9013
9130
|
async getOutgoingRelationships(sourceId, relationshipType, consistencyToken, signal) {
|
|
9014
9131
|
const relationships = await this.documentIndexer.getOutgoing(sourceId, [relationshipType], void 0, consistencyToken, signal);
|
|
9015
|
-
|
|
9132
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9016
9133
|
return relationships.results.map((rel) => rel.targetId);
|
|
9017
9134
|
}
|
|
9018
9135
|
async getIncomingRelationships(targetId, relationshipType, consistencyToken, signal) {
|
|
9019
9136
|
const relationships = await this.documentIndexer.getIncoming(targetId, [relationshipType], void 0, consistencyToken, signal);
|
|
9020
|
-
|
|
9137
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9021
9138
|
return relationships.results.map((rel) => rel.sourceId);
|
|
9022
9139
|
}
|
|
9023
9140
|
async getOperations(documentId, view, filter, paging, consistencyToken, signal) {
|
|
9024
9141
|
this.logger.verbose("getOperations(@documentId, @view, @filter, @paging)", documentId, view, filter, paging);
|
|
9025
9142
|
const branch = view?.branch || "main";
|
|
9026
9143
|
const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
|
|
9027
|
-
|
|
9144
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9028
9145
|
const allScopes = Object.keys(revisions.revision);
|
|
9029
9146
|
const result = {};
|
|
9030
9147
|
for (const scope of allScopes) {
|
|
9031
9148
|
if (!matchesScope(view, scope)) continue;
|
|
9032
|
-
|
|
9149
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9033
9150
|
const scopeResult = await this.operationStore.getSince(documentId, scope, branch, -1, filter, paging, signal);
|
|
9034
9151
|
result[scope] = {
|
|
9035
9152
|
results: scopeResult.results,
|
|
@@ -9060,13 +9177,13 @@ var Reactor = class {
|
|
|
9060
9177
|
if (search.type) results = filterByType(results, search.type);
|
|
9061
9178
|
} else if (search.type) results = await this.findByType(search.type, view, paging, consistencyToken, signal);
|
|
9062
9179
|
else throw new Error("No search criteria provided");
|
|
9063
|
-
|
|
9180
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9064
9181
|
return results;
|
|
9065
9182
|
}
|
|
9066
9183
|
async create(document, signer, signal, meta) {
|
|
9067
9184
|
this.logger.verbose("create(@id, @type, @slug)", document.header.id, document.header.documentType, document.header.slug);
|
|
9068
9185
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
9069
|
-
|
|
9186
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9070
9187
|
let actions = [createDocumentAction({
|
|
9071
9188
|
model: document.header.documentType,
|
|
9072
9189
|
version: 0,
|
|
@@ -9126,7 +9243,7 @@ var Reactor = class {
|
|
|
9126
9243
|
async deleteDocument(id, signer, signal, meta) {
|
|
9127
9244
|
this.logger.verbose("deleteDocument(@id)", id);
|
|
9128
9245
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
9129
|
-
|
|
9246
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9130
9247
|
let action = deleteDocumentAction(id);
|
|
9131
9248
|
if (signer) action = await signAction(action, signer, signal);
|
|
9132
9249
|
const jobId = v4();
|
|
@@ -9163,7 +9280,7 @@ var Reactor = class {
|
|
|
9163
9280
|
}
|
|
9164
9281
|
async execute(docId, branch, actions, signal, meta) {
|
|
9165
9282
|
this.logger.verbose("execute(@docId, @branch, @actions)", docId, branch, actions);
|
|
9166
|
-
|
|
9283
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9167
9284
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
9168
9285
|
const scope = getSharedActionScope(actions);
|
|
9169
9286
|
const jobId = v4();
|
|
@@ -9196,12 +9313,12 @@ var Reactor = class {
|
|
|
9196
9313
|
this.jobTracker.registerJob(jobInfo);
|
|
9197
9314
|
this.emitJobPending(jobInfo.id, jobMeta);
|
|
9198
9315
|
await this.queue.enqueue(job);
|
|
9199
|
-
|
|
9316
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9200
9317
|
return jobInfo;
|
|
9201
9318
|
}
|
|
9202
9319
|
async load(docId, branch, operations, signal, meta) {
|
|
9203
9320
|
this.logger.verbose("load(@docId, @branch, @count, @operations)", docId, branch, operations.length, operations);
|
|
9204
|
-
|
|
9321
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9205
9322
|
if (operations.length === 0) throw new Error("load requires at least one operation");
|
|
9206
9323
|
const scope = getSharedOperationScope(operations);
|
|
9207
9324
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9235,12 +9352,12 @@ var Reactor = class {
|
|
|
9235
9352
|
this.jobTracker.registerJob(jobInfo);
|
|
9236
9353
|
this.emitJobPending(jobInfo.id, jobMeta);
|
|
9237
9354
|
await this.queue.enqueue(job);
|
|
9238
|
-
|
|
9355
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9239
9356
|
return jobInfo;
|
|
9240
9357
|
}
|
|
9241
9358
|
async executeBatch(request, signal, meta) {
|
|
9242
9359
|
this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
|
|
9243
|
-
|
|
9360
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9244
9361
|
validateBatchRequest(request.jobs);
|
|
9245
9362
|
for (const jobPlan of request.jobs) validateActionScopes(jobPlan);
|
|
9246
9363
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9274,7 +9391,7 @@ var Reactor = class {
|
|
|
9274
9391
|
const enqueuedKeys = [];
|
|
9275
9392
|
try {
|
|
9276
9393
|
for (const key of sortedKeys) {
|
|
9277
|
-
|
|
9394
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9278
9395
|
const jobPlan = request.jobs.find((j) => j.key === key);
|
|
9279
9396
|
const jobId = planKeyToJobId.get(key);
|
|
9280
9397
|
const queueHint = jobPlan.dependsOn.map((depKey) => planKeyToJobId.get(depKey));
|
|
@@ -9309,7 +9426,7 @@ var Reactor = class {
|
|
|
9309
9426
|
}
|
|
9310
9427
|
async loadBatch(request, signal, meta) {
|
|
9311
9428
|
this.logger.verbose("loadBatch(@count jobs)", request.jobs.length);
|
|
9312
|
-
|
|
9429
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9313
9430
|
validateBatchLoadRequest(request.jobs);
|
|
9314
9431
|
for (const jobPlan of request.jobs) validateOperationScopes(jobPlan);
|
|
9315
9432
|
const createdAtUtcIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9343,7 +9460,7 @@ var Reactor = class {
|
|
|
9343
9460
|
const enqueuedKeys = [];
|
|
9344
9461
|
try {
|
|
9345
9462
|
for (const key of sortedKeys) {
|
|
9346
|
-
|
|
9463
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9347
9464
|
const jobPlan = request.jobs.find((j) => j.key === key);
|
|
9348
9465
|
const jobId = planKeyToJobId.get(key);
|
|
9349
9466
|
const queueHint = [...jobPlan.dependsOn.map((depKey) => planKeyToJobId.get(depKey)), ...jobPlan.externalDeps];
|
|
@@ -9378,21 +9495,21 @@ var Reactor = class {
|
|
|
9378
9495
|
}
|
|
9379
9496
|
async addRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
|
|
9380
9497
|
this.logger.verbose("addRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
|
|
9381
|
-
|
|
9498
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9382
9499
|
let actions = [addRelationshipAction(sourceId, targetId, relationshipType)];
|
|
9383
9500
|
if (signer) actions = await signActions(actions, signer, signal);
|
|
9384
9501
|
return await this.execute(sourceId, branch, actions, signal);
|
|
9385
9502
|
}
|
|
9386
9503
|
async removeRelationship(sourceId, targetId, relationshipType, branch = "main", signer, signal) {
|
|
9387
9504
|
this.logger.verbose("removeRelationship(@sourceId, @targetId, @relationshipType, @branch)", sourceId, targetId, relationshipType, branch);
|
|
9388
|
-
|
|
9505
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9389
9506
|
let actions = [removeRelationshipAction(sourceId, targetId, relationshipType)];
|
|
9390
9507
|
if (signer) actions = await signActions(actions, signer, signal);
|
|
9391
9508
|
return await this.execute(sourceId, branch, actions, signal);
|
|
9392
9509
|
}
|
|
9393
9510
|
getJobStatus(jobId, signal) {
|
|
9394
9511
|
this.logger.verbose("getJobStatus(@jobId)", jobId);
|
|
9395
|
-
|
|
9512
|
+
throwIfAborted(signal, () => new AbortError());
|
|
9396
9513
|
const jobInfo = this.jobTracker.getJobStatus(jobId);
|
|
9397
9514
|
if (!jobInfo) {
|
|
9398
9515
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -9461,6 +9578,7 @@ var ReactorBuilder = class {
|
|
|
9461
9578
|
upgradeManifests = [];
|
|
9462
9579
|
features = { legacyStorageEnabled: false };
|
|
9463
9580
|
readModels = [];
|
|
9581
|
+
readModelFactories = [];
|
|
9464
9582
|
executorManager;
|
|
9465
9583
|
executorConfig = {};
|
|
9466
9584
|
writeCacheConfig;
|
|
@@ -9476,6 +9594,7 @@ var ReactorBuilder = class {
|
|
|
9476
9594
|
jwtHandler;
|
|
9477
9595
|
documentModelLoader;
|
|
9478
9596
|
shutdownHooks = [];
|
|
9597
|
+
driveContainerTypes = DEFAULT_DRIVE_CONTAINER_TYPES;
|
|
9479
9598
|
withLogger(logger) {
|
|
9480
9599
|
this.logger = logger;
|
|
9481
9600
|
return this;
|
|
@@ -9499,6 +9618,17 @@ var ReactorBuilder = class {
|
|
|
9499
9618
|
this.readModels.push(readModel);
|
|
9500
9619
|
return this;
|
|
9501
9620
|
}
|
|
9621
|
+
/**
|
|
9622
|
+
* Register a factory that builds a pre-ready read model after the reactor's
|
|
9623
|
+
* internal `operationIndex`, `writeCache`, and processor-manager consistency
|
|
9624
|
+
* tracker are constructed. Use this for read models (e.g. `BaseReadModel`
|
|
9625
|
+
* subclasses) that need those dependencies and therefore cannot be built
|
|
9626
|
+
* before calling `buildModule()`.
|
|
9627
|
+
*/
|
|
9628
|
+
withReadModelFactory(factory) {
|
|
9629
|
+
this.readModelFactories.push(factory);
|
|
9630
|
+
return this;
|
|
9631
|
+
}
|
|
9502
9632
|
withReadModelCoordinator(readModelCoordinator) {
|
|
9503
9633
|
this.readModelCoordinator = readModelCoordinator;
|
|
9504
9634
|
return this;
|
|
@@ -9518,6 +9648,10 @@ var ReactorBuilder = class {
|
|
|
9518
9648
|
this.writeCacheConfig = config;
|
|
9519
9649
|
return this;
|
|
9520
9650
|
}
|
|
9651
|
+
withDriveContainerTypes(types) {
|
|
9652
|
+
this.driveContainerTypes = new Set(types);
|
|
9653
|
+
return this;
|
|
9654
|
+
}
|
|
9521
9655
|
withMigrationStrategy(strategy) {
|
|
9522
9656
|
this.migrationStrategy = strategy;
|
|
9523
9657
|
return this;
|
|
@@ -9608,7 +9742,7 @@ var ReactorBuilder = class {
|
|
|
9608
9742
|
const collectionMembershipCache = new CollectionMembershipCache(operationIndex);
|
|
9609
9743
|
const executionScope = new KyselyExecutionScope(database, operationStore, operationIndex, keyframeStore, writeCache, documentMetaCache, collectionMembershipCache);
|
|
9610
9744
|
let executorManager = this.executorManager;
|
|
9611
|
-
if (!executorManager) executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.executorConfig, this.signatureVerifier, executionScope), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs);
|
|
9745
|
+
if (!executorManager) executorManager = new SimpleJobExecutorManager(() => new SimpleJobExecutor(this.logger, documentModelRegistry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, this.driveContainerTypes, this.executorConfig, this.signatureVerifier, executionScope), eventBus, queue, jobTracker, this.logger, resolver, this.executorConfig.jobTimeoutMs);
|
|
9612
9746
|
await executorManager.start(this.executorConfig.maxConcurrency ?? 1);
|
|
9613
9747
|
const readModelInstances = Array.from(new Set([...this.readModels]));
|
|
9614
9748
|
const documentViewConsistencyTracker = new ConsistencyTracker();
|
|
@@ -9630,21 +9764,29 @@ var ReactorBuilder = class {
|
|
|
9630
9764
|
const subscriptionManager = new ReactorSubscriptionManager(new DefaultSubscriptionErrorHandler());
|
|
9631
9765
|
const subscriptionNotificationReadModel = new SubscriptionNotificationReadModel(subscriptionManager, documentView);
|
|
9632
9766
|
const processorManagerConsistencyTracker = new ConsistencyTracker();
|
|
9633
|
-
const processorManager = new ProcessorManager(database, operationIndex, writeCache, processorManagerConsistencyTracker, this.logger);
|
|
9767
|
+
const processorManager = new ProcessorManager(database, operationIndex, writeCache, processorManagerConsistencyTracker, this.logger, this.driveContainerTypes);
|
|
9634
9768
|
try {
|
|
9635
9769
|
await processorManager.init();
|
|
9636
9770
|
} catch (error) {
|
|
9637
9771
|
console.error("Error initializing processor manager", error);
|
|
9638
9772
|
}
|
|
9773
|
+
for (const factory of this.readModelFactories) {
|
|
9774
|
+
const readModel = await factory({
|
|
9775
|
+
operationIndex,
|
|
9776
|
+
writeCache,
|
|
9777
|
+
processorManagerConsistencyTracker
|
|
9778
|
+
});
|
|
9779
|
+
readModelInstances.push(readModel);
|
|
9780
|
+
}
|
|
9639
9781
|
const readModelCoordinator = this.readModelCoordinator ? this.readModelCoordinator : new ReadModelCoordinator(eventBus, readModelInstances, [subscriptionNotificationReadModel, processorManager]);
|
|
9640
9782
|
const reactor = new Reactor(this.logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, this.features, documentView, documentIndexer, operationStore, eventBus, executorManager);
|
|
9641
9783
|
let syncModule = void 0;
|
|
9642
9784
|
if (this.channelScheme) {
|
|
9643
9785
|
const factory = this.channelScheme === ChannelScheme.CONNECT ? new GqlRequestChannelFactory(this.logger, this.jwtHandler, queue) : new GqlResponseChannelFactory(this.logger);
|
|
9644
|
-
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database);
|
|
9786
|
+
syncModule = new SyncBuilder().withChannelFactory(factory).buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
9645
9787
|
await syncModule.syncManager.startup();
|
|
9646
9788
|
} else if (this.syncBuilder) {
|
|
9647
|
-
syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database);
|
|
9789
|
+
syncModule = this.syncBuilder.buildModule(reactor, this.logger, operationIndex, eventBus, database, this.driveContainerTypes);
|
|
9648
9790
|
await syncModule.syncManager.startup();
|
|
9649
9791
|
}
|
|
9650
9792
|
const module = {
|
|
@@ -9890,13 +10032,15 @@ const JobExecutorEventTypes = {
|
|
|
9890
10032
|
EXECUTOR_STOPPED: 20004
|
|
9891
10033
|
};
|
|
9892
10034
|
//#endregion
|
|
9893
|
-
//#region src/admin/
|
|
9894
|
-
const
|
|
10035
|
+
//#region src/admin/passthrough-keyframe-store.ts
|
|
10036
|
+
const passthroughKeyframeStore = {
|
|
9895
10037
|
putKeyframe: () => Promise.resolve(),
|
|
9896
10038
|
findNearestKeyframe: () => Promise.resolve(void 0),
|
|
9897
10039
|
listKeyframes: () => Promise.resolve([]),
|
|
9898
10040
|
deleteKeyframes: () => Promise.resolve(0)
|
|
9899
10041
|
};
|
|
10042
|
+
//#endregion
|
|
10043
|
+
//#region src/admin/document-integrity-service.ts
|
|
9900
10044
|
var DocumentIntegrityService = class {
|
|
9901
10045
|
keyframeStore;
|
|
9902
10046
|
operationStore;
|
|
@@ -9913,14 +10057,14 @@ var DocumentIntegrityService = class {
|
|
|
9913
10057
|
async validateDocument(documentId, branch = "main", signal) {
|
|
9914
10058
|
const keyframeIssues = [];
|
|
9915
10059
|
const snapshotIssues = [];
|
|
9916
|
-
const replayCache = new KyselyWriteCache(
|
|
10060
|
+
const replayCache = new KyselyWriteCache(passthroughKeyframeStore, this.operationStore, this.documentModelRegistry, {
|
|
9917
10061
|
maxDocuments: 1,
|
|
9918
10062
|
ringBufferSize: 1,
|
|
9919
10063
|
keyframeInterval: Number.MAX_SAFE_INTEGER
|
|
9920
10064
|
});
|
|
9921
10065
|
const keyframes = await this.keyframeStore.listKeyframes(documentId, void 0, branch, signal);
|
|
9922
10066
|
for (const keyframe of keyframes) {
|
|
9923
|
-
|
|
10067
|
+
throwIfAborted(signal);
|
|
9924
10068
|
replayCache.invalidate(documentId, keyframe.scope, branch);
|
|
9925
10069
|
const replayedDoc = await replayCache.getState(documentId, keyframe.scope, branch, keyframe.revision, signal);
|
|
9926
10070
|
const kfHash = hashDocumentStateForScope(keyframe.document, keyframe.scope);
|
|
@@ -9953,7 +10097,7 @@ var DocumentIntegrityService = class {
|
|
|
9953
10097
|
try {
|
|
9954
10098
|
replayedDoc = await replayCache.getState(documentId, scope, branch, void 0, signal);
|
|
9955
10099
|
} catch {
|
|
9956
|
-
|
|
10100
|
+
throwIfAborted(signal);
|
|
9957
10101
|
continue;
|
|
9958
10102
|
}
|
|
9959
10103
|
const snapshotHash = hashDocumentStateForScope(currentDoc, scope);
|
|
@@ -9982,7 +10126,7 @@ var DocumentIntegrityService = class {
|
|
|
9982
10126
|
async rebuildSnapshots(documentId, branch = "main", signal) {
|
|
9983
10127
|
const scopes = await this.discoverScopes(documentId, branch, signal);
|
|
9984
10128
|
for (const scope of scopes) {
|
|
9985
|
-
|
|
10129
|
+
throwIfAborted(signal);
|
|
9986
10130
|
this.writeCache.invalidate(documentId, scope, branch);
|
|
9987
10131
|
}
|
|
9988
10132
|
return {
|
|
@@ -9997,6 +10141,6 @@ var DocumentIntegrityService = class {
|
|
|
9997
10141
|
}
|
|
9998
10142
|
};
|
|
9999
10143
|
//#endregion
|
|
10000
|
-
export { BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, consolidateSyncOperations, createDocumentAction, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveCollectionId, driveIdFromUrl, envelopesToSyncOperations, getMigrationStatus, makeConsistencyKey, parseDriveUrl, removeRelationshipAction, runMigrations, trimMailboxFromAckOrdinal, upgradeDocumentAction };
|
|
10144
|
+
export { BaseReadModel, ChannelError, ChannelErrorSource, ChannelScheme, ConsistencyTracker, DEFAULT_DRIVE_CONTAINER_TYPES, DefaultSubscriptionErrorHandler, DocumentChangeType, DocumentIntegrityService, DocumentModelRegistry, DocumentModelResolver, DriveClient, DuplicateManifestError, DuplicateModuleError, DuplicateOperationError, EventBus, EventBusAggregateError, GqlRequestChannel, GqlRequestChannelFactory, GqlResponseChannel, GqlResponseChannelFactory, SimpleJobExecutor as InMemoryJobExecutor, SimpleJobExecutor, InMemoryJobTracker, InMemoryQueue, IntervalPollTimer, InvalidModuleError, JobAwaiter, JobExecutorEventTypes, JobStatus, KyselyDocumentIndexer, KyselyDocumentView, KyselyKeyframeStore, KyselyOperationStore, KyselySyncCursorStorage, KyselySyncRemoteStorage, KyselyWriteCache, Mailbox, ModuleNotFoundError, NullDocumentModelResolver, OptimisticLockError, PollBehavior, PollingChannelError, ProcessorManager, PropagationMode, QueueEventTypes, REACTOR_SCHEMA, Reactor, ReactorBuilder, ReactorClient, ReactorClientBuilder, ReactorEventTypes, ReactorSubscriptionManager, ReadModelCoordinator, RelationalDbProcessor, RelationshipChangeType, RevisionMismatchError, SimpleJobExecutorManager, SyncBuilder, SyncEventTypes, SyncOperation, SyncOperationAggregateError, SyncOperationStatus, SyncStatus, SyncStatusTracker, addRelationshipAction, batchOperationsByDocument, consolidateSyncOperations, createDocumentAction, createMutableShutdownStatus, createRelationalDb, deleteDocumentAction, documentActions, driveCollectionId, driveIdFromUrl, envelopesToSyncOperations, getMigrationStatus, makeConsistencyKey, parseDriveUrl, parsePagingOptions, removeRelationshipAction, runMigrations, trimMailboxFromAckOrdinal, updateRelationshipAction, upgradeDocumentAction };
|
|
10001
10145
|
|
|
10002
10146
|
//# sourceMappingURL=index.js.map
|