@powerhousedao/reactor 5.0.3 → 5.0.5
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/src/cache/index.d.ts +3 -0
- package/dist/src/cache/index.d.ts.map +1 -0
- package/dist/src/cache/index.js +2 -0
- package/dist/src/cache/index.js.map +1 -0
- package/dist/src/cache/kysely-operation-index.d.ts +13 -0
- package/dist/src/cache/kysely-operation-index.d.ts.map +1 -0
- package/dist/src/cache/kysely-operation-index.js +207 -0
- package/dist/src/cache/kysely-operation-index.js.map +1 -0
- package/dist/src/cache/kysely-write-cache.d.ts +5 -4
- package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
- package/dist/src/cache/kysely-write-cache.js +12 -12
- package/dist/src/cache/kysely-write-cache.js.map +1 -1
- package/dist/src/cache/operation-index-types.d.ts +49 -0
- package/dist/src/cache/operation-index-types.d.ts.map +1 -0
- package/dist/src/cache/operation-index-types.js +4 -0
- package/dist/src/cache/operation-index-types.js.map +1 -0
- package/dist/src/cache/{types.d.ts → write-cache-types.d.ts} +1 -1
- package/dist/src/cache/write-cache-types.d.ts.map +1 -0
- package/dist/src/cache/write-cache-types.js +2 -0
- package/dist/src/cache/write-cache-types.js.map +1 -0
- package/dist/src/client/reactor-client.d.ts +6 -4
- package/dist/src/client/reactor-client.d.ts.map +1 -1
- package/dist/src/client/reactor-client.js +118 -37
- package/dist/src/client/reactor-client.js.map +1 -1
- package/dist/src/client/types.d.ts +4 -4
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/core/builder.d.ts +15 -2
- package/dist/src/core/builder.d.ts.map +1 -1
- package/dist/src/core/builder.js +48 -7
- package/dist/src/core/builder.js.map +1 -1
- package/dist/src/core/reactor-builder.d.ts +40 -0
- package/dist/src/core/reactor-builder.d.ts.map +1 -0
- package/dist/src/core/reactor-builder.js +141 -0
- package/dist/src/core/reactor-builder.js.map +1 -0
- package/dist/src/core/reactor.d.ts +36 -11
- package/dist/src/core/reactor.d.ts.map +1 -1
- package/dist/src/core/reactor.js +609 -279
- package/dist/src/core/reactor.js.map +1 -1
- package/dist/src/core/types.d.ts +84 -7
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/utils.d.ts +44 -4
- package/dist/src/core/utils.d.ts.map +1 -1
- package/dist/src/core/utils.js +116 -6
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/events/types.d.ts +28 -0
- package/dist/src/events/types.d.ts.map +1 -1
- package/dist/src/events/types.js +2 -0
- package/dist/src/events/types.js.map +1 -1
- package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor-manager.js +19 -1
- package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
- package/dist/src/executor/simple-job-executor.d.ts +16 -2
- package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor.js +458 -252
- package/dist/src/executor/simple-job-executor.js.map +1 -1
- package/dist/src/executor/types.d.ts +2 -0
- package/dist/src/executor/types.d.ts.map +1 -1
- package/dist/src/executor/types.js.map +1 -1
- package/dist/src/executor/util.d.ts +18 -0
- package/dist/src/executor/util.d.ts.map +1 -1
- package/dist/src/executor/util.js +42 -1
- package/dist/src/executor/util.js.map +1 -1
- package/dist/src/index.d.ts +12 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/job-tracker/in-memory-job-tracker.d.ts +10 -1
- package/dist/src/job-tracker/in-memory-job-tracker.d.ts.map +1 -1
- package/dist/src/job-tracker/in-memory-job-tracker.js +57 -23
- package/dist/src/job-tracker/in-memory-job-tracker.js.map +1 -1
- package/dist/src/job-tracker/interfaces.d.ts +5 -7
- package/dist/src/job-tracker/interfaces.d.ts.map +1 -1
- package/dist/src/queue/types.d.ts +6 -1
- package/dist/src/queue/types.d.ts.map +1 -1
- package/dist/src/queue/types.js.map +1 -1
- package/dist/src/read-models/coordinator.d.ts.map +1 -1
- package/dist/src/read-models/coordinator.js +11 -0
- package/dist/src/read-models/coordinator.js.map +1 -1
- package/dist/src/read-models/document-view.d.ts +11 -6
- package/dist/src/read-models/document-view.d.ts.map +1 -1
- package/dist/src/read-models/document-view.js +156 -113
- package/dist/src/read-models/document-view.js.map +1 -1
- package/dist/src/read-models/types.d.ts +3 -3
- package/dist/src/read-models/types.d.ts.map +1 -1
- package/dist/src/shared/awaiter.d.ts +11 -8
- package/dist/src/shared/awaiter.d.ts.map +1 -1
- package/dist/src/shared/awaiter.js +66 -75
- package/dist/src/shared/awaiter.js.map +1 -1
- package/dist/src/shared/consistency-tracker.d.ts +48 -0
- package/dist/src/shared/consistency-tracker.d.ts.map +1 -0
- package/dist/src/shared/consistency-tracker.js +123 -0
- package/dist/src/shared/consistency-tracker.js.map +1 -0
- package/dist/src/shared/types.d.ts +30 -2
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/src/shared/types.js +4 -2
- package/dist/src/shared/types.js.map +1 -1
- package/dist/src/storage/index.d.ts +4 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +3 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +227 -2
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/interfaces.js.map +1 -1
- package/dist/src/storage/kysely/document-indexer.d.ts +28 -0
- package/dist/src/storage/kysely/document-indexer.d.ts.map +1 -0
- package/dist/src/storage/kysely/document-indexer.js +350 -0
- package/dist/src/storage/kysely/document-indexer.js.map +1 -0
- package/dist/src/storage/kysely/keyframe-store.d.ts.map +1 -1
- package/dist/src/storage/kysely/keyframe-store.js +6 -13
- package/dist/src/storage/kysely/keyframe-store.js.map +1 -1
- package/dist/src/storage/kysely/store.js +1 -1
- package/dist/src/storage/kysely/store.js.map +1 -1
- package/dist/src/storage/kysely/sync-cursor-storage.d.ts +13 -0
- package/dist/src/storage/kysely/sync-cursor-storage.d.ts.map +1 -0
- package/dist/src/storage/kysely/sync-cursor-storage.js +93 -0
- package/dist/src/storage/kysely/sync-cursor-storage.js.map +1 -0
- package/dist/src/storage/kysely/sync-remote-storage.d.ts +13 -0
- package/dist/src/storage/kysely/sync-remote-storage.d.ts.map +1 -0
- package/dist/src/storage/kysely/sync-remote-storage.js +134 -0
- package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -0
- package/dist/src/storage/kysely/types.d.ts +98 -2
- package/dist/src/storage/kysely/types.d.ts.map +1 -1
- package/dist/src/storage/migrations/001_create_operation_table.d.ts +3 -0
- package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/001_create_operation_table.js +40 -0
- package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -0
- package/dist/src/storage/migrations/002_create_keyframe_table.d.ts +3 -0
- package/dist/src/storage/migrations/002_create_keyframe_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/002_create_keyframe_table.js +27 -0
- package/dist/src/storage/migrations/002_create_keyframe_table.js.map +1 -0
- package/dist/src/storage/migrations/003_create_document_table.d.ts +3 -0
- package/dist/src/storage/migrations/003_create_document_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/003_create_document_table.js +10 -0
- package/dist/src/storage/migrations/003_create_document_table.js.map +1 -0
- package/dist/src/storage/migrations/004_create_document_relationship_table.d.ts +3 -0
- package/dist/src/storage/migrations/004_create_document_relationship_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/004_create_document_relationship_table.js +35 -0
- package/dist/src/storage/migrations/004_create_document_relationship_table.js.map +1 -0
- package/dist/src/storage/migrations/005_create_indexer_state_table.d.ts +3 -0
- package/dist/src/storage/migrations/005_create_indexer_state_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/005_create_indexer_state_table.js +10 -0
- package/dist/src/storage/migrations/005_create_indexer_state_table.js.map +1 -0
- package/dist/src/storage/migrations/006_create_document_snapshot_table.d.ts +3 -0
- package/dist/src/storage/migrations/006_create_document_snapshot_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/006_create_document_snapshot_table.js +49 -0
- package/dist/src/storage/migrations/006_create_document_snapshot_table.js.map +1 -0
- package/dist/src/storage/migrations/007_create_slug_mapping_table.d.ts +3 -0
- package/dist/src/storage/migrations/007_create_slug_mapping_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/007_create_slug_mapping_table.js +24 -0
- package/dist/src/storage/migrations/007_create_slug_mapping_table.js.map +1 -0
- package/dist/src/storage/migrations/008_create_view_state_table.d.ts +3 -0
- package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -0
- package/dist/src/storage/migrations/008_create_view_state_table.js +9 -0
- package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -0
- package/dist/src/storage/migrations/009_create_operation_index_tables.d.ts +3 -0
- package/dist/src/storage/migrations/009_create_operation_index_tables.d.ts.map +1 -0
- package/dist/src/storage/migrations/009_create_operation_index_tables.js +50 -0
- package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -0
- package/dist/src/storage/migrations/010_create_sync_tables.d.ts +3 -0
- package/dist/src/storage/migrations/010_create_sync_tables.d.ts.map +1 -0
- package/dist/src/storage/migrations/010_create_sync_tables.js +43 -0
- package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -0
- package/dist/src/storage/migrations/index.d.ts +3 -0
- package/dist/src/storage/migrations/index.d.ts.map +1 -0
- package/dist/src/storage/migrations/index.js +3 -0
- package/dist/src/storage/migrations/index.js.map +1 -0
- package/dist/src/storage/migrations/migrator.d.ts +5 -0
- package/dist/src/storage/migrations/migrator.d.ts.map +1 -0
- package/dist/src/storage/migrations/migrator.js +55 -0
- package/dist/src/storage/migrations/migrator.js.map +1 -0
- package/dist/src/storage/migrations/run-migrations.d.ts +2 -0
- package/dist/src/storage/migrations/run-migrations.d.ts.map +1 -0
- package/dist/src/storage/migrations/run-migrations.js +58 -0
- package/dist/src/storage/migrations/run-migrations.js.map +1 -0
- package/dist/src/storage/migrations/types.d.ts +9 -0
- package/dist/src/storage/migrations/types.d.ts.map +1 -0
- package/dist/src/storage/migrations/types.js.map +1 -0
- package/dist/src/storage/txn.d.ts.map +1 -1
- package/dist/src/storage/txn.js +2 -0
- package/dist/src/storage/txn.js.map +1 -1
- package/dist/src/sync/channels/index.d.ts +3 -0
- package/dist/src/sync/channels/index.d.ts.map +1 -0
- package/dist/src/sync/channels/index.js +3 -0
- package/dist/src/sync/channels/index.js.map +1 -0
- package/dist/src/sync/channels/internal-channel.d.ts +57 -0
- package/dist/src/sync/channels/internal-channel.d.ts.map +1 -0
- package/dist/src/sync/channels/internal-channel.js +106 -0
- package/dist/src/sync/channels/internal-channel.js.map +1 -0
- package/dist/src/sync/channels/utils.d.ts +15 -0
- package/dist/src/sync/channels/utils.d.ts.map +1 -0
- package/dist/src/sync/channels/utils.js +26 -0
- package/dist/src/sync/channels/utils.js.map +1 -0
- package/dist/src/sync/errors.d.ts +10 -0
- package/dist/src/sync/errors.d.ts.map +1 -0
- package/dist/src/sync/errors.js +17 -0
- package/dist/src/sync/errors.js.map +1 -0
- package/dist/src/sync/index.d.ts +12 -0
- package/dist/src/sync/index.d.ts.map +1 -0
- package/dist/src/sync/index.js +9 -0
- package/dist/src/sync/index.js.map +1 -0
- package/dist/src/sync/interfaces.d.ts +150 -0
- package/dist/src/sync/interfaces.d.ts.map +1 -0
- package/dist/src/sync/interfaces.js +2 -0
- package/dist/src/sync/interfaces.js.map +1 -0
- package/dist/src/sync/mailbox.d.ts +21 -0
- package/dist/src/sync/mailbox.d.ts.map +1 -0
- package/dist/src/sync/mailbox.js +59 -0
- package/dist/src/sync/mailbox.js.map +1 -0
- package/dist/src/sync/sync-builder.d.ts +17 -0
- package/dist/src/sync/sync-builder.d.ts.map +1 -0
- package/dist/src/sync/sync-builder.js +29 -0
- package/dist/src/sync/sync-builder.js.map +1 -0
- package/dist/src/sync/sync-manager.d.ts +33 -0
- package/dist/src/sync/sync-manager.d.ts.map +1 -0
- package/dist/src/sync/sync-manager.js +197 -0
- package/dist/src/sync/sync-manager.js.map +1 -0
- package/dist/src/sync/sync-operation.d.ts +28 -0
- package/dist/src/sync/sync-operation.d.ts.map +1 -0
- package/dist/src/sync/sync-operation.js +63 -0
- package/dist/src/sync/sync-operation.js.map +1 -0
- package/dist/src/sync/types.d.ts +61 -0
- package/dist/src/sync/types.d.ts.map +1 -0
- package/dist/src/sync/types.js +16 -0
- package/dist/src/sync/types.js.map +1 -0
- package/dist/src/sync/utils.d.ts +17 -0
- package/dist/src/sync/utils.d.ts.map +1 -0
- package/dist/src/sync/utils.js +34 -0
- package/dist/src/sync/utils.js.map +1 -0
- package/package.json +9 -5
- package/dist/src/cache/types.d.ts.map +0 -1
- package/dist/src/cache/types.js.map +0 -1
- /package/dist/src/{cache → storage/migrations}/types.js +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { driveCollectionId } from "../cache/operation-index-types.js";
|
|
2
|
+
import { OperationEventTypes, } from "../events/types.js";
|
|
2
3
|
import { DocumentDeletedError } from "../shared/errors.js";
|
|
4
|
+
import { reshuffleByTimestampAndIndex } from "../utils/reshuffle.js";
|
|
3
5
|
import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
|
|
6
|
+
const MAX_SKIP_THRESHOLD = 100;
|
|
4
7
|
/**
|
|
5
8
|
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
6
9
|
*
|
|
@@ -15,13 +18,23 @@ export class SimpleJobExecutor {
|
|
|
15
18
|
operationStore;
|
|
16
19
|
eventBus;
|
|
17
20
|
writeCache;
|
|
18
|
-
|
|
21
|
+
operationIndex;
|
|
22
|
+
config;
|
|
23
|
+
constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, config) {
|
|
19
24
|
this.registry = registry;
|
|
20
25
|
this.documentStorage = documentStorage;
|
|
21
26
|
this.operationStorage = operationStorage;
|
|
22
27
|
this.operationStore = operationStore;
|
|
23
28
|
this.eventBus = eventBus;
|
|
24
29
|
this.writeCache = writeCache;
|
|
30
|
+
this.operationIndex = operationIndex;
|
|
31
|
+
this.config = {
|
|
32
|
+
maxConcurrency: config.maxConcurrency ?? 1,
|
|
33
|
+
jobTimeoutMs: config.jobTimeoutMs ?? 30000,
|
|
34
|
+
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
35
|
+
retryMaxDelayMs: config.retryMaxDelayMs ?? 5000,
|
|
36
|
+
legacyStorageEnabled: config.legacyStorageEnabled ?? true,
|
|
37
|
+
};
|
|
25
38
|
}
|
|
26
39
|
/**
|
|
27
40
|
* Execute a single job by applying all its actions through the appropriate reducers.
|
|
@@ -29,71 +42,137 @@ export class SimpleJobExecutor {
|
|
|
29
42
|
*/
|
|
30
43
|
async executeJob(job) {
|
|
31
44
|
const startTime = Date.now();
|
|
45
|
+
const indexTxn = this.operationIndex.start();
|
|
46
|
+
if (job.kind === "load") {
|
|
47
|
+
const result = await this.executeLoadJob(job, startTime, indexTxn);
|
|
48
|
+
if (result.success) {
|
|
49
|
+
await this.operationIndex.commit(indexTxn);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
const result = await this.processActions(job, job.actions, startTime, indexTxn);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
return {
|
|
56
|
+
job,
|
|
57
|
+
success: false,
|
|
58
|
+
error: result.error,
|
|
59
|
+
duration: Date.now() - startTime,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
await this.operationIndex.commit(indexTxn);
|
|
63
|
+
if (result.operationsWithContext.length > 0) {
|
|
64
|
+
const event = {
|
|
65
|
+
jobId: job.id,
|
|
66
|
+
operations: result.operationsWithContext,
|
|
67
|
+
};
|
|
68
|
+
this.eventBus
|
|
69
|
+
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
70
|
+
.catch(() => {
|
|
71
|
+
// TODO: Log error
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
job,
|
|
76
|
+
success: true,
|
|
77
|
+
operations: result.generatedOperations,
|
|
78
|
+
operationsWithContext: result.operationsWithContext,
|
|
79
|
+
duration: Date.now() - startTime,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async processActions(job, actions, startTime, indexTxn, skipValues) {
|
|
32
83
|
const generatedOperations = [];
|
|
33
84
|
const operationsWithContext = [];
|
|
34
|
-
|
|
35
|
-
for (const action of
|
|
36
|
-
// Handle system actions specially (CREATE_DOCUMENT, DELETE_DOCUMENT, etc.)
|
|
85
|
+
let actionIndex = 0;
|
|
86
|
+
for (const action of actions) {
|
|
37
87
|
if (action.type === "CREATE_DOCUMENT") {
|
|
38
|
-
const result = await this.executeCreateDocumentAction(job, action, startTime);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
const result = await this.executeCreateDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
|
|
89
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
90
|
+
if (error !== null) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
generatedOperations,
|
|
94
|
+
operationsWithContext,
|
|
95
|
+
error: error.error,
|
|
96
|
+
};
|
|
47
97
|
}
|
|
98
|
+
actionIndex++;
|
|
48
99
|
continue;
|
|
49
100
|
}
|
|
50
101
|
if (action.type === "DELETE_DOCUMENT") {
|
|
51
|
-
const result = await this.executeDeleteDocumentAction(job, action, startTime);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
102
|
+
const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
|
|
103
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
104
|
+
if (error !== null) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
generatedOperations,
|
|
108
|
+
operationsWithContext,
|
|
109
|
+
error: error.error,
|
|
110
|
+
};
|
|
60
111
|
}
|
|
112
|
+
actionIndex++;
|
|
61
113
|
continue;
|
|
62
114
|
}
|
|
63
115
|
if (action.type === "UPGRADE_DOCUMENT") {
|
|
64
|
-
const result = await this.executeUpgradeDocumentAction(job, action, startTime);
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
|
|
117
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
118
|
+
if (error !== null) {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
generatedOperations,
|
|
122
|
+
operationsWithContext,
|
|
123
|
+
error: error.error,
|
|
124
|
+
};
|
|
67
125
|
}
|
|
68
|
-
|
|
69
|
-
|
|
126
|
+
actionIndex++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (action.type === "ADD_RELATIONSHIP") {
|
|
130
|
+
const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
|
|
131
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
132
|
+
if (error !== null) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
generatedOperations,
|
|
136
|
+
operationsWithContext,
|
|
137
|
+
error: error.error,
|
|
138
|
+
};
|
|
70
139
|
}
|
|
71
|
-
|
|
72
|
-
|
|
140
|
+
actionIndex++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (action.type === "REMOVE_RELATIONSHIP") {
|
|
144
|
+
const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
|
|
145
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
146
|
+
if (error !== null) {
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
generatedOperations,
|
|
150
|
+
operationsWithContext,
|
|
151
|
+
error: error.error,
|
|
152
|
+
};
|
|
73
153
|
}
|
|
154
|
+
actionIndex++;
|
|
74
155
|
continue;
|
|
75
156
|
}
|
|
76
|
-
// For regular actions, load the document and apply through reducer
|
|
77
157
|
let document;
|
|
78
158
|
try {
|
|
79
159
|
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
80
160
|
}
|
|
81
161
|
catch (error) {
|
|
82
162
|
return {
|
|
83
|
-
job,
|
|
84
163
|
success: false,
|
|
164
|
+
generatedOperations,
|
|
165
|
+
operationsWithContext,
|
|
85
166
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
86
|
-
duration: Date.now() - startTime,
|
|
87
167
|
};
|
|
88
168
|
}
|
|
89
|
-
// Check if document is deleted
|
|
90
169
|
const documentState = document.state.document;
|
|
91
170
|
if (documentState.isDeleted) {
|
|
92
171
|
return {
|
|
93
|
-
job,
|
|
94
172
|
success: false,
|
|
173
|
+
generatedOperations,
|
|
174
|
+
operationsWithContext,
|
|
95
175
|
error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
|
|
96
|
-
duration: Date.now() - startTime,
|
|
97
176
|
};
|
|
98
177
|
}
|
|
99
178
|
let module;
|
|
@@ -102,10 +181,10 @@ export class SimpleJobExecutor {
|
|
|
102
181
|
}
|
|
103
182
|
catch (error) {
|
|
104
183
|
return {
|
|
105
|
-
job,
|
|
106
184
|
success: false,
|
|
185
|
+
generatedOperations,
|
|
186
|
+
operationsWithContext,
|
|
107
187
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
108
|
-
duration: Date.now() - startTime,
|
|
109
188
|
};
|
|
110
189
|
}
|
|
111
190
|
let updatedDocument;
|
|
@@ -119,34 +198,41 @@ export class SimpleJobExecutor {
|
|
|
119
198
|
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
120
199
|
}
|
|
121
200
|
return {
|
|
122
|
-
job,
|
|
123
201
|
success: false,
|
|
202
|
+
generatedOperations,
|
|
203
|
+
operationsWithContext,
|
|
124
204
|
error: enhancedError,
|
|
125
|
-
duration: Date.now() - startTime,
|
|
126
205
|
};
|
|
127
206
|
}
|
|
128
207
|
const scope = job.scope;
|
|
129
208
|
const operations = updatedDocument.operations[scope];
|
|
130
209
|
if (operations.length === 0) {
|
|
131
|
-
throw new Error("No operation generated from action");
|
|
132
|
-
}
|
|
133
|
-
const newOperation = operations[operations.length - 1];
|
|
134
|
-
generatedOperations.push(newOperation);
|
|
135
|
-
// Write the operation to legacy storage
|
|
136
|
-
try {
|
|
137
|
-
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
210
|
return {
|
|
141
|
-
job,
|
|
142
211
|
success: false,
|
|
143
|
-
|
|
144
|
-
|
|
212
|
+
generatedOperations,
|
|
213
|
+
operationsWithContext,
|
|
214
|
+
error: new Error("No operation generated from action"),
|
|
145
215
|
};
|
|
146
216
|
}
|
|
147
|
-
|
|
217
|
+
const newOperation = operations[operations.length - 1];
|
|
218
|
+
if (skipValues && actionIndex < skipValues.length) {
|
|
219
|
+
newOperation.skip = skipValues[actionIndex];
|
|
220
|
+
}
|
|
221
|
+
generatedOperations.push(newOperation);
|
|
222
|
+
if (this.config.legacyStorageEnabled) {
|
|
223
|
+
try {
|
|
224
|
+
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
generatedOperations,
|
|
230
|
+
operationsWithContext,
|
|
231
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
148
235
|
const resultingState = JSON.stringify(updatedDocument.state);
|
|
149
|
-
// Write the operation to new IOperationStore (dual-writing)
|
|
150
236
|
try {
|
|
151
237
|
await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
152
238
|
txn.addOperations(newOperation);
|
|
@@ -154,10 +240,10 @@ export class SimpleJobExecutor {
|
|
|
154
240
|
}
|
|
155
241
|
catch (error) {
|
|
156
242
|
return {
|
|
157
|
-
job,
|
|
158
243
|
success: false,
|
|
244
|
+
generatedOperations,
|
|
245
|
+
operationsWithContext,
|
|
159
246
|
error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
160
|
-
duration: Date.now() - startTime,
|
|
161
247
|
};
|
|
162
248
|
}
|
|
163
249
|
updatedDocument.header.revision = {
|
|
@@ -172,25 +258,15 @@ export class SimpleJobExecutor {
|
|
|
172
258
|
scope,
|
|
173
259
|
branch: job.branch,
|
|
174
260
|
documentType: document.header.documentType,
|
|
175
|
-
resultingState,
|
|
261
|
+
resultingState,
|
|
176
262
|
},
|
|
177
263
|
});
|
|
178
|
-
|
|
179
|
-
// Emit event for read models with all operations - non-blocking
|
|
180
|
-
if (operationsWithContext.length > 0) {
|
|
181
|
-
this.eventBus
|
|
182
|
-
.emit(OperationEventTypes.OPERATION_WRITTEN, {
|
|
183
|
-
operations: operationsWithContext,
|
|
184
|
-
})
|
|
185
|
-
.catch(() => {
|
|
186
|
-
// TODO: Log error
|
|
187
|
-
});
|
|
264
|
+
actionIndex++;
|
|
188
265
|
}
|
|
189
266
|
return {
|
|
190
|
-
job,
|
|
191
267
|
success: true,
|
|
192
|
-
|
|
193
|
-
|
|
268
|
+
generatedOperations,
|
|
269
|
+
operationsWithContext,
|
|
194
270
|
};
|
|
195
271
|
}
|
|
196
272
|
/**
|
|
@@ -198,7 +274,7 @@ export class SimpleJobExecutor {
|
|
|
198
274
|
* This creates a new document in storage along with its initial operation.
|
|
199
275
|
* For a new document, the operation index is always 0.
|
|
200
276
|
*/
|
|
201
|
-
async executeCreateDocumentAction(job, action, startTime) {
|
|
277
|
+
async executeCreateDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
202
278
|
if (job.scope !== "document") {
|
|
203
279
|
return {
|
|
204
280
|
job,
|
|
@@ -209,36 +285,23 @@ export class SimpleJobExecutor {
|
|
|
209
285
|
}
|
|
210
286
|
const document = createDocumentFromAction(action);
|
|
211
287
|
// Legacy: Store the document in storage
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
job,
|
|
218
|
-
|
|
219
|
-
error: new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`),
|
|
220
|
-
duration: Date.now() - startTime,
|
|
221
|
-
};
|
|
288
|
+
if (this.config.legacyStorageEnabled) {
|
|
289
|
+
try {
|
|
290
|
+
await this.documentStorage.create(document);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
294
|
+
}
|
|
222
295
|
}
|
|
223
|
-
|
|
224
|
-
const operation = {
|
|
225
|
-
index: 0,
|
|
226
|
-
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
227
|
-
hash: "", // Will be computed later
|
|
228
|
-
skip: 0, // Always 0 for new operations; skip > 0 only during reshuffle
|
|
229
|
-
action: action,
|
|
230
|
-
};
|
|
296
|
+
const operation = this.createOperation(action, 0, skip);
|
|
231
297
|
// Legacy: Write the CREATE_DOCUMENT operation to legacy storage
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
job,
|
|
238
|
-
|
|
239
|
-
error: new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`),
|
|
240
|
-
duration: Date.now() - startTime,
|
|
241
|
-
};
|
|
298
|
+
if (this.config.legacyStorageEnabled) {
|
|
299
|
+
try {
|
|
300
|
+
await this.operationStorage.addDocumentOperations(document.header.id, [operation], document);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return this.buildErrorResult(job, new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
304
|
+
}
|
|
242
305
|
}
|
|
243
306
|
// Compute resultingState for passing via context (not persisted)
|
|
244
307
|
// Include header and all scopes present in the document state (auth, document, etc.)
|
|
@@ -248,59 +311,39 @@ export class SimpleJobExecutor {
|
|
|
248
311
|
...document.state,
|
|
249
312
|
};
|
|
250
313
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await this.operationStore.apply(document.header.id, document.header.documentType, job.scope, job.branch, operation.index, (txn) => {
|
|
255
|
-
txn.addOperations(operation);
|
|
256
|
-
});
|
|
314
|
+
const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
315
|
+
if (writeError !== null) {
|
|
316
|
+
return writeError;
|
|
257
317
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
318
|
+
this.updateDocumentRevision(document, job.scope, operation.index);
|
|
319
|
+
this.writeCacheState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
320
|
+
indexTxn.write([
|
|
321
|
+
{
|
|
322
|
+
...operation,
|
|
323
|
+
documentId: document.header.id,
|
|
324
|
+
documentType: document.header.documentType,
|
|
325
|
+
branch: job.branch,
|
|
326
|
+
scope: job.scope,
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
// collection membership has to be _after_ the write, as it requires the
|
|
330
|
+
// ordinal of the operation to be set
|
|
331
|
+
if (document.header.documentType === "powerhouse/document-drive") {
|
|
332
|
+
const collectionId = driveCollectionId(job.branch, document.header.id);
|
|
333
|
+
indexTxn.createCollection(collectionId);
|
|
334
|
+
indexTxn.addToCollection(collectionId, document.header.id);
|
|
265
335
|
}
|
|
266
|
-
document.header.
|
|
267
|
-
...document.header.revision,
|
|
268
|
-
[job.scope]: operation.index + 1,
|
|
269
|
-
};
|
|
270
|
-
this.writeCache.putState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
271
|
-
return {
|
|
272
|
-
job,
|
|
273
|
-
success: true,
|
|
274
|
-
operations: [operation],
|
|
275
|
-
operationsWithContext: [
|
|
276
|
-
{
|
|
277
|
-
operation,
|
|
278
|
-
context: {
|
|
279
|
-
documentId: document.header.id,
|
|
280
|
-
scope: job.scope,
|
|
281
|
-
branch: job.branch,
|
|
282
|
-
documentType: document.header.documentType,
|
|
283
|
-
resultingState,
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
],
|
|
287
|
-
duration: Date.now() - startTime,
|
|
288
|
-
};
|
|
336
|
+
return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
289
337
|
}
|
|
290
338
|
/**
|
|
291
339
|
* Execute a DELETE_DOCUMENT system action.
|
|
292
340
|
* This deletes a document from legacy storage and writes the operation to IOperationStore.
|
|
293
341
|
* The operation index is determined from the document's current operation count.
|
|
294
342
|
*/
|
|
295
|
-
async executeDeleteDocumentAction(job, action, startTime) {
|
|
343
|
+
async executeDeleteDocumentAction(job, action, startTime, indexTxn) {
|
|
296
344
|
const input = action.input;
|
|
297
345
|
if (!input.documentId) {
|
|
298
|
-
return
|
|
299
|
-
job,
|
|
300
|
-
success: false,
|
|
301
|
-
error: new Error("DELETE_DOCUMENT action requires a documentId in input"),
|
|
302
|
-
duration: Date.now() - startTime,
|
|
303
|
-
};
|
|
346
|
+
return this.buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
|
|
304
347
|
}
|
|
305
348
|
const documentId = input.documentId;
|
|
306
349
|
let document;
|
|
@@ -308,43 +351,22 @@ export class SimpleJobExecutor {
|
|
|
308
351
|
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
309
352
|
}
|
|
310
353
|
catch (error) {
|
|
311
|
-
return {
|
|
312
|
-
job,
|
|
313
|
-
success: false,
|
|
314
|
-
error: new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`),
|
|
315
|
-
duration: Date.now() - startTime,
|
|
316
|
-
};
|
|
354
|
+
return this.buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
317
355
|
}
|
|
318
356
|
// Check if document is already deleted
|
|
319
357
|
const documentState = document.state.document;
|
|
320
358
|
if (documentState.isDeleted) {
|
|
321
|
-
return
|
|
322
|
-
job,
|
|
323
|
-
success: false,
|
|
324
|
-
error: new DocumentDeletedError(documentId, documentState.deletedAtUtcIso),
|
|
325
|
-
duration: Date.now() - startTime,
|
|
326
|
-
};
|
|
359
|
+
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
327
360
|
}
|
|
328
|
-
// Determine the next operation index for this scope only (per-scope indexing)
|
|
329
361
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
await this.documentStorage.delete(documentId);
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
return {
|
|
343
|
-
job,
|
|
344
|
-
success: false,
|
|
345
|
-
error: new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`),
|
|
346
|
-
duration: Date.now() - startTime,
|
|
347
|
-
};
|
|
362
|
+
const operation = this.createOperation(action, nextIndex);
|
|
363
|
+
if (this.config.legacyStorageEnabled) {
|
|
364
|
+
try {
|
|
365
|
+
await this.documentStorage.delete(documentId);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
return this.buildErrorResult(job, new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
369
|
+
}
|
|
348
370
|
}
|
|
349
371
|
// Mark the document as deleted in the state for read model indexing
|
|
350
372
|
applyDeleteDocumentAction(document, action);
|
|
@@ -355,131 +377,295 @@ export class SimpleJobExecutor {
|
|
|
355
377
|
document: document.state.document,
|
|
356
378
|
};
|
|
357
379
|
const resultingState = JSON.stringify(resultingStateObj);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await this.operationStore.apply(documentId, document.header.documentType, job.scope, job.branch, operation.index, (txn) => {
|
|
362
|
-
txn.addOperations(operation);
|
|
363
|
-
});
|
|
380
|
+
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
381
|
+
if (writeError !== null) {
|
|
382
|
+
return writeError;
|
|
364
383
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
success: true,
|
|
376
|
-
operations: [operation],
|
|
377
|
-
operationsWithContext: [
|
|
378
|
-
{
|
|
379
|
-
operation,
|
|
380
|
-
context: {
|
|
381
|
-
documentId,
|
|
382
|
-
scope: job.scope,
|
|
383
|
-
branch: job.branch,
|
|
384
|
-
documentType: document.header.documentType,
|
|
385
|
-
resultingState,
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
duration: Date.now() - startTime,
|
|
390
|
-
};
|
|
384
|
+
indexTxn.write([
|
|
385
|
+
{
|
|
386
|
+
...operation,
|
|
387
|
+
documentId: documentId,
|
|
388
|
+
documentType: document.header.documentType,
|
|
389
|
+
branch: job.branch,
|
|
390
|
+
scope: job.scope,
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
391
394
|
}
|
|
392
395
|
/**
|
|
393
396
|
* Execute an UPGRADE_DOCUMENT system action.
|
|
394
397
|
* This sets the document's initial state from the upgrade action.
|
|
395
398
|
* The operation index is determined from the document's current operation count.
|
|
396
399
|
*/
|
|
397
|
-
async executeUpgradeDocumentAction(job, action, startTime) {
|
|
400
|
+
async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
398
401
|
const input = action.input;
|
|
399
402
|
if (!input.documentId) {
|
|
400
|
-
return
|
|
401
|
-
job,
|
|
402
|
-
success: false,
|
|
403
|
-
error: new Error("UPGRADE_DOCUMENT action requires a documentId in input"),
|
|
404
|
-
duration: Date.now() - startTime,
|
|
405
|
-
};
|
|
403
|
+
return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
406
404
|
}
|
|
407
405
|
const documentId = input.documentId;
|
|
408
|
-
// Load the document from write cache
|
|
409
406
|
let document;
|
|
410
407
|
try {
|
|
411
408
|
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
412
409
|
}
|
|
413
410
|
catch (error) {
|
|
414
|
-
return {
|
|
415
|
-
job,
|
|
416
|
-
success: false,
|
|
417
|
-
error: new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`),
|
|
418
|
-
duration: Date.now() - startTime,
|
|
419
|
-
};
|
|
411
|
+
return this.buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
420
412
|
}
|
|
421
|
-
// Check if document is deleted
|
|
422
413
|
const documentState = document.state.document;
|
|
423
414
|
if (documentState.isDeleted) {
|
|
424
|
-
return
|
|
425
|
-
job,
|
|
426
|
-
success: false,
|
|
427
|
-
error: new DocumentDeletedError(documentId, documentState.deletedAtUtcIso),
|
|
428
|
-
duration: Date.now() - startTime,
|
|
429
|
-
};
|
|
415
|
+
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
430
416
|
}
|
|
431
|
-
// Determine the next operation index for this scope only (per-scope indexing)
|
|
432
417
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
433
|
-
// Apply the initialState from the upgrade action
|
|
434
|
-
// The initialState from UPGRADE_DOCUMENT should be merged with the existing base state
|
|
435
|
-
// to preserve auth and document scopes while adding model-specific scopes (global, local, etc.)
|
|
436
418
|
applyUpgradeDocumentAction(document, action);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
419
|
+
const operation = this.createOperation(action, nextIndex, skip);
|
|
420
|
+
// Write the updated document to legacy storage
|
|
421
|
+
if (this.config.legacyStorageEnabled) {
|
|
422
|
+
try {
|
|
423
|
+
await this.operationStorage.addDocumentOperations(documentId, [operation], document);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return this.buildErrorResult(job, new Error(`Failed to write UPGRADE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Compute resultingState for passing via context (not persisted)
|
|
430
|
+
const resultingStateObj = {
|
|
431
|
+
header: document.header,
|
|
432
|
+
...document.state,
|
|
433
|
+
};
|
|
434
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
435
|
+
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
436
|
+
if (writeError !== null) {
|
|
437
|
+
return writeError;
|
|
438
|
+
}
|
|
439
|
+
this.updateDocumentRevision(document, job.scope, operation.index);
|
|
440
|
+
this.writeCacheState(documentId, job.scope, job.branch, operation.index, document);
|
|
441
|
+
indexTxn.write([
|
|
442
|
+
{
|
|
443
|
+
...operation,
|
|
444
|
+
documentId: documentId,
|
|
445
|
+
documentType: document.header.documentType,
|
|
446
|
+
branch: job.branch,
|
|
447
|
+
scope: job.scope,
|
|
448
|
+
},
|
|
449
|
+
]);
|
|
450
|
+
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
451
|
+
}
|
|
452
|
+
async executeAddRelationshipAction(job, action, startTime, indexTxn) {
|
|
453
|
+
if (job.scope !== "document") {
|
|
454
|
+
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
|
|
455
|
+
}
|
|
456
|
+
const input = action.input;
|
|
457
|
+
if (!input.sourceId || !input.targetId || !input.relationshipType) {
|
|
458
|
+
return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
|
|
459
|
+
}
|
|
460
|
+
if (input.sourceId === input.targetId) {
|
|
461
|
+
return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)"), startTime);
|
|
462
|
+
}
|
|
463
|
+
let sourceDoc;
|
|
464
|
+
try {
|
|
465
|
+
sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
469
|
+
}
|
|
470
|
+
let targetDoc;
|
|
471
|
+
try {
|
|
472
|
+
targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
476
|
+
}
|
|
477
|
+
const targetDocState = targetDoc.state.document;
|
|
478
|
+
if (targetDocState.isDeleted) {
|
|
479
|
+
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
|
|
480
|
+
}
|
|
481
|
+
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
482
|
+
const operation = this.createOperation(action, nextIndex);
|
|
483
|
+
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
484
|
+
if (writeError !== null) {
|
|
485
|
+
return writeError;
|
|
486
|
+
}
|
|
487
|
+
sourceDoc.header.lastModifiedAtUtcIso =
|
|
488
|
+
operation.timestampUtcMs || new Date().toISOString();
|
|
489
|
+
this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
|
|
490
|
+
sourceDoc.operations = {
|
|
491
|
+
...sourceDoc.operations,
|
|
492
|
+
[job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
|
|
493
|
+
};
|
|
494
|
+
const scopeState = sourceDoc.state[job.scope];
|
|
495
|
+
const resultingStateObj = {
|
|
496
|
+
header: structuredClone(sourceDoc.header),
|
|
497
|
+
[job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
|
|
498
|
+
};
|
|
499
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
500
|
+
this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
501
|
+
indexTxn.write([
|
|
502
|
+
{
|
|
503
|
+
...operation,
|
|
504
|
+
documentId: input.sourceId,
|
|
505
|
+
documentType: sourceDoc.header.documentType,
|
|
506
|
+
branch: job.branch,
|
|
507
|
+
scope: job.scope,
|
|
508
|
+
},
|
|
509
|
+
]);
|
|
510
|
+
// collection membership has to be _after_ the write, as it requires the
|
|
511
|
+
// ordinal of the operation to be set
|
|
512
|
+
if (sourceDoc.header.documentType === "powerhouse/document-drive") {
|
|
513
|
+
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
514
|
+
indexTxn.addToCollection(collectionId, input.targetId);
|
|
515
|
+
}
|
|
516
|
+
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
517
|
+
}
|
|
518
|
+
async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
|
|
519
|
+
if (job.scope !== "document") {
|
|
520
|
+
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
|
|
521
|
+
}
|
|
522
|
+
const input = action.input;
|
|
523
|
+
if (!input.sourceId || !input.targetId || !input.relationshipType) {
|
|
524
|
+
return this.buildErrorResult(job, new Error("REMOVE_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
|
|
525
|
+
}
|
|
526
|
+
let sourceDoc;
|
|
527
|
+
try {
|
|
528
|
+
sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
532
|
+
}
|
|
533
|
+
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
534
|
+
const operation = this.createOperation(action, nextIndex);
|
|
535
|
+
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
536
|
+
if (writeError !== null) {
|
|
537
|
+
return writeError;
|
|
538
|
+
}
|
|
539
|
+
sourceDoc.header.lastModifiedAtUtcIso =
|
|
540
|
+
operation.timestampUtcMs || new Date().toISOString();
|
|
541
|
+
this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
|
|
542
|
+
sourceDoc.operations = {
|
|
543
|
+
...sourceDoc.operations,
|
|
544
|
+
[job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
|
|
545
|
+
};
|
|
546
|
+
const scopeState = sourceDoc.state[job.scope];
|
|
547
|
+
const resultingStateObj = {
|
|
548
|
+
header: structuredClone(sourceDoc.header),
|
|
549
|
+
[job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
|
|
550
|
+
};
|
|
551
|
+
const resultingState = JSON.stringify(resultingStateObj);
|
|
552
|
+
this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
553
|
+
indexTxn.write([
|
|
554
|
+
{
|
|
555
|
+
...operation,
|
|
556
|
+
documentId: input.sourceId,
|
|
557
|
+
documentType: sourceDoc.header.documentType,
|
|
558
|
+
branch: job.branch,
|
|
559
|
+
scope: job.scope,
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
// collection membership has to be _after_ the write, as it requires the
|
|
563
|
+
// ordinal of the operation to be set
|
|
564
|
+
if (sourceDoc.header.documentType === "powerhouse/document-drive") {
|
|
565
|
+
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
566
|
+
indexTxn.removeFromCollection(collectionId, input.targetId);
|
|
567
|
+
}
|
|
568
|
+
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
569
|
+
}
|
|
570
|
+
createOperation(action, index, skip = 0) {
|
|
571
|
+
return {
|
|
572
|
+
index: index,
|
|
440
573
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
441
|
-
hash: "",
|
|
442
|
-
skip:
|
|
574
|
+
hash: "",
|
|
575
|
+
skip: skip,
|
|
443
576
|
action: action,
|
|
444
577
|
};
|
|
445
|
-
|
|
578
|
+
}
|
|
579
|
+
async executeLoadJob(job, startTime, indexTxn) {
|
|
580
|
+
if (job.operations.length === 0) {
|
|
581
|
+
return this.buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
|
|
582
|
+
}
|
|
583
|
+
const scope = job.scope;
|
|
584
|
+
let latestRevision = 0;
|
|
446
585
|
try {
|
|
447
|
-
await this.
|
|
586
|
+
const revisions = await this.operationStore.getRevisions(job.documentId, job.branch);
|
|
587
|
+
latestRevision = revisions.revision[scope] ?? 0;
|
|
448
588
|
}
|
|
449
|
-
catch
|
|
589
|
+
catch {
|
|
590
|
+
latestRevision = 0;
|
|
591
|
+
}
|
|
592
|
+
const minIncomingIndex = job.operations.reduce((min, operation) => Math.min(min, operation.index), Number.POSITIVE_INFINITY);
|
|
593
|
+
const skipCount = minIncomingIndex === Number.POSITIVE_INFINITY
|
|
594
|
+
? 0
|
|
595
|
+
: Math.max(0, latestRevision - minIncomingIndex);
|
|
596
|
+
if (skipCount > MAX_SKIP_THRESHOLD) {
|
|
450
597
|
return {
|
|
451
598
|
job,
|
|
452
599
|
success: false,
|
|
453
|
-
error: new Error(`
|
|
600
|
+
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${MAX_SKIP_THRESHOLD}. ` +
|
|
601
|
+
`This indicates an attempt to insert an operation at index ${minIncomingIndex} when the latest revision is ${latestRevision}.`),
|
|
454
602
|
duration: Date.now() - startTime,
|
|
455
603
|
};
|
|
456
604
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
605
|
+
const reshuffledOperations = reshuffleByTimestampAndIndex({
|
|
606
|
+
index: latestRevision,
|
|
607
|
+
skip: skipCount,
|
|
608
|
+
}, [], job.operations.map((operation) => ({
|
|
609
|
+
...operation,
|
|
610
|
+
id: operation.id,
|
|
611
|
+
})));
|
|
612
|
+
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
613
|
+
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
614
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
return {
|
|
617
|
+
job,
|
|
618
|
+
success: false,
|
|
619
|
+
error: result.error,
|
|
620
|
+
duration: Date.now() - startTime,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (result.operationsWithContext.length > 0) {
|
|
624
|
+
const event = {
|
|
625
|
+
jobId: job.id,
|
|
626
|
+
operations: result.operationsWithContext,
|
|
627
|
+
};
|
|
628
|
+
this.eventBus
|
|
629
|
+
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
630
|
+
.catch(() => {
|
|
631
|
+
// TODO: log error channel once logging is wired
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
this.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
635
|
+
return {
|
|
636
|
+
job,
|
|
637
|
+
success: true,
|
|
638
|
+
operations: result.generatedOperations,
|
|
639
|
+
operationsWithContext: result.operationsWithContext,
|
|
640
|
+
duration: Date.now() - startTime,
|
|
461
641
|
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// Note: resultingState is NOT persisted in IOperationStore
|
|
642
|
+
}
|
|
643
|
+
async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
|
|
465
644
|
try {
|
|
466
|
-
await this.operationStore.apply(documentId,
|
|
645
|
+
await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
|
|
467
646
|
txn.addOperations(operation);
|
|
468
647
|
});
|
|
648
|
+
return null;
|
|
469
649
|
}
|
|
470
650
|
catch (error) {
|
|
471
651
|
return {
|
|
472
652
|
job,
|
|
473
653
|
success: false,
|
|
474
|
-
error: new Error(`Failed to write
|
|
654
|
+
error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
475
655
|
duration: Date.now() - startTime,
|
|
476
656
|
};
|
|
477
657
|
}
|
|
658
|
+
}
|
|
659
|
+
updateDocumentRevision(document, scope, operationIndex) {
|
|
478
660
|
document.header.revision = {
|
|
479
661
|
...document.header.revision,
|
|
480
|
-
[
|
|
662
|
+
[scope]: operationIndex + 1,
|
|
481
663
|
};
|
|
482
|
-
|
|
664
|
+
}
|
|
665
|
+
writeCacheState(documentId, scope, branch, operationIndex, document) {
|
|
666
|
+
this.writeCache.putState(documentId, scope, branch, operationIndex, document);
|
|
667
|
+
}
|
|
668
|
+
buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
|
|
483
669
|
return {
|
|
484
670
|
job,
|
|
485
671
|
success: true,
|
|
@@ -488,10 +674,10 @@ export class SimpleJobExecutor {
|
|
|
488
674
|
{
|
|
489
675
|
operation,
|
|
490
676
|
context: {
|
|
491
|
-
documentId,
|
|
677
|
+
documentId: documentId,
|
|
492
678
|
scope: job.scope,
|
|
493
679
|
branch: job.branch,
|
|
494
|
-
documentType:
|
|
680
|
+
documentType: documentType,
|
|
495
681
|
resultingState,
|
|
496
682
|
},
|
|
497
683
|
},
|
|
@@ -499,5 +685,25 @@ export class SimpleJobExecutor {
|
|
|
499
685
|
duration: Date.now() - startTime,
|
|
500
686
|
};
|
|
501
687
|
}
|
|
688
|
+
buildErrorResult(job, error, startTime) {
|
|
689
|
+
return {
|
|
690
|
+
job,
|
|
691
|
+
success: false,
|
|
692
|
+
error: error,
|
|
693
|
+
duration: Date.now() - startTime,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
697
|
+
if (!result.success) {
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
if (result.operations && result.operations.length > 0) {
|
|
701
|
+
generatedOperations.push(...result.operations);
|
|
702
|
+
}
|
|
703
|
+
if (result.operationsWithContext) {
|
|
704
|
+
operationsWithContext.push(...result.operationsWithContext);
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
502
708
|
}
|
|
503
709
|
//# sourceMappingURL=simple-job-executor.js.map
|