@powerhousedao/reactor 5.1.0 → 5.2.0-dev.1
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/actions/index.d.ts +24 -0
- package/dist/src/actions/index.d.ts.map +1 -0
- package/dist/src/actions/index.js +76 -0
- package/dist/src/actions/index.js.map +1 -0
- package/dist/src/cache/document-meta-cache-types.d.ts +114 -0
- package/dist/src/cache/document-meta-cache-types.d.ts.map +1 -0
- package/dist/src/cache/document-meta-cache-types.js +2 -0
- package/dist/src/cache/document-meta-cache-types.js.map +1 -0
- package/dist/src/cache/document-meta-cache.d.ts +30 -0
- package/dist/src/cache/document-meta-cache.d.ts.map +1 -0
- package/dist/src/cache/document-meta-cache.js +128 -0
- package/dist/src/cache/document-meta-cache.js.map +1 -0
- package/dist/src/cache/kysely-operation-index.d.ts +4 -2
- package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
- package/dist/src/cache/kysely-operation-index.js +67 -24
- package/dist/src/cache/kysely-operation-index.js.map +1 -1
- package/dist/src/cache/kysely-write-cache.d.ts.map +1 -1
- package/dist/src/cache/kysely-write-cache.js +3 -2
- package/dist/src/cache/kysely-write-cache.js.map +1 -1
- package/dist/src/cache/operation-index-types.d.ts +4 -3
- package/dist/src/cache/operation-index-types.d.ts.map +1 -1
- package/dist/src/cache/operation-index-types.js.map +1 -1
- package/dist/src/client/reactor-client.d.ts +22 -10
- package/dist/src/client/reactor-client.d.ts.map +1 -1
- package/dist/src/client/reactor-client.js +145 -48
- package/dist/src/client/reactor-client.js.map +1 -1
- package/dist/src/client/types.d.ts +32 -13
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/core/reactor-builder.d.ts +19 -12
- package/dist/src/core/reactor-builder.d.ts.map +1 -1
- package/dist/src/core/reactor-builder.js +127 -37
- package/dist/src/core/reactor-builder.js.map +1 -1
- package/dist/src/core/{builder.d.ts → reactor-client-builder.d.ts} +20 -4
- package/dist/src/core/reactor-client-builder.d.ts.map +1 -0
- package/dist/src/core/reactor-client-builder.js +123 -0
- package/dist/src/core/reactor-client-builder.js.map +1 -0
- package/dist/src/core/reactor.d.ts +14 -16
- package/dist/src/core/reactor.d.ts.map +1 -1
- package/dist/src/core/reactor.js +101 -110
- package/dist/src/core/reactor.js.map +1 -1
- package/dist/src/core/types.d.ts +101 -22
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/utils.d.ts +9 -1
- package/dist/src/core/utils.d.ts.map +1 -1
- package/dist/src/core/utils.js +30 -0
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/events/types.d.ts +1 -0
- package/dist/src/events/types.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor-manager.d.ts +3 -1
- package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor-manager.js +10 -7
- package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
- package/dist/src/executor/simple-job-executor.d.ts +20 -2
- package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor.js +400 -219
- 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 +14 -5
- package/dist/src/executor/util.d.ts.map +1 -1
- package/dist/src/executor/util.js +36 -9
- package/dist/src/executor/util.js.map +1 -1
- package/dist/src/index.d.ts +17 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging/console.d.ts +23 -0
- package/dist/src/logging/console.d.ts.map +1 -0
- package/dist/src/logging/console.js +108 -0
- package/dist/src/logging/console.js.map +1 -0
- package/dist/src/logging/types.d.ts +12 -0
- package/dist/src/logging/types.d.ts.map +1 -0
- package/dist/src/logging/types.js +2 -0
- package/dist/src/logging/types.js.map +1 -0
- package/dist/src/processors/index.d.ts +3 -0
- package/dist/src/processors/index.d.ts.map +1 -0
- package/dist/src/processors/index.js +2 -0
- package/dist/src/processors/index.js.map +1 -0
- package/dist/src/processors/processor-manager.d.ts +38 -0
- package/dist/src/processors/processor-manager.d.ts.map +1 -0
- package/dist/src/processors/processor-manager.js +165 -0
- package/dist/src/processors/processor-manager.js.map +1 -0
- package/dist/src/processors/types.d.ts +63 -0
- package/dist/src/processors/types.d.ts.map +1 -0
- package/dist/src/processors/types.js +2 -0
- package/dist/src/processors/types.js.map +1 -0
- package/dist/src/processors/utils.d.ts +10 -0
- package/dist/src/processors/utils.d.ts.map +1 -0
- package/dist/src/processors/utils.js +58 -0
- package/dist/src/processors/utils.js.map +1 -0
- package/dist/src/queue/types.d.ts +2 -0
- package/dist/src/queue/types.d.ts.map +1 -1
- package/dist/src/queue/types.js.map +1 -1
- package/dist/src/read-models/base-read-model.d.ts +60 -0
- package/dist/src/read-models/base-read-model.d.ts.map +1 -0
- package/dist/src/read-models/base-read-model.js +143 -0
- package/dist/src/read-models/base-read-model.js.map +1 -0
- package/dist/src/read-models/coordinator.d.ts +3 -2
- package/dist/src/read-models/coordinator.d.ts.map +1 -1
- package/dist/src/read-models/coordinator.js +12 -13
- package/dist/src/read-models/coordinator.js.map +1 -1
- package/dist/src/read-models/document-view.d.ts +6 -7
- package/dist/src/read-models/document-view.d.ts.map +1 -1
- package/dist/src/read-models/document-view.js +18 -81
- package/dist/src/read-models/document-view.js.map +1 -1
- package/dist/src/read-models/types.d.ts +2 -1
- package/dist/src/read-models/types.d.ts.map +1 -1
- package/dist/src/registry/implementation.d.ts +42 -34
- package/dist/src/registry/implementation.d.ts.map +1 -1
- package/dist/src/registry/implementation.js +168 -48
- package/dist/src/registry/implementation.js.map +1 -1
- package/dist/src/registry/interfaces.d.ts +69 -8
- package/dist/src/registry/interfaces.d.ts.map +1 -1
- package/dist/src/shared/errors.d.ts +24 -0
- package/dist/src/shared/errors.d.ts.map +1 -1
- package/dist/src/shared/errors.js +42 -0
- package/dist/src/shared/errors.js.map +1 -1
- package/dist/src/shared/types.d.ts +4 -0
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/src/shared/types.js.map +1 -1
- package/dist/src/signer/passthrough-signer.d.ts +9 -3
- package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
- package/dist/src/signer/passthrough-signer.js +13 -0
- package/dist/src/signer/passthrough-signer.js.map +1 -1
- package/dist/src/signer/types.d.ts +12 -10
- package/dist/src/signer/types.d.ts.map +1 -1
- package/dist/src/storage/consistency-aware-legacy-storage.d.ts +33 -0
- package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +1 -0
- package/dist/src/storage/consistency-aware-legacy-storage.js +65 -0
- package/dist/src/storage/consistency-aware-legacy-storage.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +94 -1
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/interfaces.js +2 -2
- package/dist/src/storage/interfaces.js.map +1 -1
- package/dist/src/storage/kysely/store.d.ts +1 -0
- package/dist/src/storage/kysely/store.d.ts.map +1 -1
- package/dist/src/storage/kysely/store.js +41 -4
- package/dist/src/storage/kysely/store.js.map +1 -1
- package/dist/src/storage/kysely/sync-cursor-storage.js +2 -2
- package/dist/src/storage/kysely/sync-cursor-storage.js.map +1 -1
- package/dist/src/storage/kysely/sync-remote-storage.d.ts.map +1 -1
- package/dist/src/storage/kysely/sync-remote-storage.js +11 -12
- package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
- package/dist/src/storage/kysely/types.d.ts +6 -6
- package/dist/src/storage/migrations/001_create_operation_table.d.ts.map +1 -1
- package/dist/src/storage/migrations/001_create_operation_table.js +2 -1
- package/dist/src/storage/migrations/001_create_operation_table.js.map +1 -1
- package/dist/src/storage/migrations/008_create_view_state_table.d.ts +1 -1
- package/dist/src/storage/migrations/008_create_view_state_table.d.ts.map +1 -1
- package/dist/src/storage/migrations/008_create_view_state_table.js +2 -1
- package/dist/src/storage/migrations/008_create_view_state_table.js.map +1 -1
- package/dist/src/storage/migrations/009_create_operation_index_tables.js +1 -1
- package/dist/src/storage/migrations/009_create_operation_index_tables.js.map +1 -1
- package/dist/src/storage/migrations/010_create_sync_tables.js +5 -5
- package/dist/src/storage/migrations/010_create_sync_tables.js.map +1 -1
- package/dist/src/storage/migrations/migrator.d.ts +3 -2
- package/dist/src/storage/migrations/migrator.d.ts.map +1 -1
- package/dist/src/storage/migrations/migrator.js +29 -6
- package/dist/src/storage/migrations/migrator.js.map +1 -1
- package/dist/src/storage/migrations/run-migrations.js +3 -3
- package/dist/src/storage/migrations/run-migrations.js.map +1 -1
- package/dist/src/storage/txn.d.ts.map +1 -1
- package/dist/src/storage/txn.js +2 -3
- package/dist/src/storage/txn.js.map +1 -1
- package/dist/src/subs/subscription-notification-read-model.d.ts +17 -0
- package/dist/src/subs/subscription-notification-read-model.d.ts.map +1 -0
- package/dist/src/subs/subscription-notification-read-model.js +62 -0
- package/dist/src/subs/subscription-notification-read-model.js.map +1 -0
- package/dist/src/sync/channels/composite-channel-factory.d.ts +30 -0
- package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -0
- package/dist/src/sync/channels/composite-channel-factory.js +87 -0
- package/dist/src/sync/channels/composite-channel-factory.js.map +1 -0
- package/dist/src/sync/channels/gql-channel-factory.d.ts +25 -0
- package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -0
- package/dist/src/sync/channels/gql-channel-factory.js +76 -0
- package/dist/src/sync/channels/gql-channel-factory.js.map +1 -0
- package/dist/src/sync/channels/gql-channel.d.ts +118 -0
- package/dist/src/sync/channels/gql-channel.d.ts.map +1 -0
- package/dist/src/sync/channels/gql-channel.js +423 -0
- package/dist/src/sync/channels/gql-channel.js.map +1 -0
- package/dist/src/sync/channels/index.d.ts +4 -1
- package/dist/src/sync/channels/index.d.ts.map +1 -1
- package/dist/src/sync/channels/index.js +4 -1
- package/dist/src/sync/channels/index.js.map +1 -1
- package/dist/src/sync/channels/polling-channel.d.ts +39 -0
- package/dist/src/sync/channels/polling-channel.d.ts.map +1 -0
- package/dist/src/sync/channels/polling-channel.js +72 -0
- package/dist/src/sync/channels/polling-channel.js.map +1 -0
- package/dist/src/sync/channels/utils.d.ts +17 -2
- package/dist/src/sync/channels/utils.d.ts.map +1 -1
- package/dist/src/sync/channels/utils.js +76 -6
- package/dist/src/sync/channels/utils.js.map +1 -1
- package/dist/src/sync/errors.d.ts +1 -1
- package/dist/src/sync/errors.d.ts.map +1 -1
- package/dist/src/sync/errors.js +2 -2
- package/dist/src/sync/errors.js.map +1 -1
- package/dist/src/sync/index.d.ts +2 -2
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +2 -2
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/interfaces.d.ts +33 -3
- package/dist/src/sync/interfaces.d.ts.map +1 -1
- package/dist/src/sync/sync-builder.d.ts +4 -2
- package/dist/src/sync/sync-builder.d.ts.map +1 -1
- package/dist/src/sync/sync-builder.js +12 -2
- package/dist/src/sync/sync-builder.js.map +1 -1
- package/dist/src/sync/sync-manager.d.ts +7 -3
- package/dist/src/sync/sync-manager.d.ts.map +1 -1
- package/dist/src/sync/sync-manager.js +79 -10
- package/dist/src/sync/sync-manager.js.map +1 -1
- package/dist/src/sync/types.d.ts +1 -2
- package/dist/src/sync/types.d.ts.map +1 -1
- package/dist/src/sync/utils.d.ts +19 -0
- package/dist/src/sync/utils.d.ts.map +1 -1
- package/dist/src/sync/utils.js +44 -0
- package/dist/src/sync/utils.js.map +1 -1
- package/package.json +6 -5
- package/dist/src/core/builder.d.ts.map +0 -1
- package/dist/src/core/builder.js +0 -88
- package/dist/src/core/builder.js.map +0 -1
- package/dist/src/sync/channels/internal-channel.d.ts +0 -57
- package/dist/src/sync/channels/internal-channel.d.ts.map +0 -1
- package/dist/src/sync/channels/internal-channel.js +0 -106
- package/dist/src/sync/channels/internal-channel.js.map +0 -1
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { deriveOperationId, isUndoRedo } from "document-model/core";
|
|
1
2
|
import { driveCollectionId } from "../cache/operation-index-types.js";
|
|
2
3
|
import { OperationEventTypes, } from "../events/types.js";
|
|
3
|
-
import { DocumentDeletedError } from "../shared/errors.js";
|
|
4
|
+
import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
|
|
4
5
|
import { reshuffleByTimestampAndIndex } from "../utils/reshuffle.js";
|
|
5
6
|
import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
|
|
6
7
|
const MAX_SKIP_THRESHOLD = 100;
|
|
8
|
+
const documentScopeActions = [
|
|
9
|
+
"CREATE_DOCUMENT",
|
|
10
|
+
"DELETE_DOCUMENT",
|
|
11
|
+
"UPGRADE_DOCUMENT",
|
|
12
|
+
"ADD_RELATIONSHIP",
|
|
13
|
+
"REMOVE_RELATIONSHIP",
|
|
14
|
+
];
|
|
7
15
|
/**
|
|
8
16
|
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
9
17
|
*
|
|
@@ -12,6 +20,7 @@ const MAX_SKIP_THRESHOLD = 100;
|
|
|
12
20
|
* @see docs/planning/Jobs/reshuffle.md for skip mechanism details
|
|
13
21
|
*/
|
|
14
22
|
export class SimpleJobExecutor {
|
|
23
|
+
logger;
|
|
15
24
|
registry;
|
|
16
25
|
documentStorage;
|
|
17
26
|
operationStorage;
|
|
@@ -19,8 +28,11 @@ export class SimpleJobExecutor {
|
|
|
19
28
|
eventBus;
|
|
20
29
|
writeCache;
|
|
21
30
|
operationIndex;
|
|
31
|
+
documentMetaCache;
|
|
32
|
+
signatureVerifier;
|
|
22
33
|
config;
|
|
23
|
-
constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, config) {
|
|
34
|
+
constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
|
|
35
|
+
this.logger = logger;
|
|
24
36
|
this.registry = registry;
|
|
25
37
|
this.documentStorage = documentStorage;
|
|
26
38
|
this.operationStorage = operationStorage;
|
|
@@ -28,7 +40,10 @@ export class SimpleJobExecutor {
|
|
|
28
40
|
this.eventBus = eventBus;
|
|
29
41
|
this.writeCache = writeCache;
|
|
30
42
|
this.operationIndex = operationIndex;
|
|
43
|
+
this.documentMetaCache = documentMetaCache;
|
|
44
|
+
this.signatureVerifier = signatureVerifier;
|
|
31
45
|
this.config = {
|
|
46
|
+
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
32
47
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
33
48
|
jobTimeoutMs: config.jobTimeoutMs ?? 30000,
|
|
34
49
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
@@ -45,8 +60,23 @@ export class SimpleJobExecutor {
|
|
|
45
60
|
const indexTxn = this.operationIndex.start();
|
|
46
61
|
if (job.kind === "load") {
|
|
47
62
|
const result = await this.executeLoadJob(job, startTime, indexTxn);
|
|
48
|
-
if (result.success) {
|
|
49
|
-
await this.operationIndex.commit(indexTxn);
|
|
63
|
+
if (result.success && result.operationsWithContext) {
|
|
64
|
+
const ordinals = await this.operationIndex.commit(indexTxn);
|
|
65
|
+
for (let i = 0; i < result.operationsWithContext.length; i++) {
|
|
66
|
+
result.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
67
|
+
}
|
|
68
|
+
if (result.operationsWithContext.length > 0) {
|
|
69
|
+
const event = {
|
|
70
|
+
jobId: job.id,
|
|
71
|
+
operations: result.operationsWithContext,
|
|
72
|
+
jobMeta: job.meta,
|
|
73
|
+
};
|
|
74
|
+
this.eventBus
|
|
75
|
+
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// TODO: Log error
|
|
78
|
+
});
|
|
79
|
+
}
|
|
50
80
|
}
|
|
51
81
|
return result;
|
|
52
82
|
}
|
|
@@ -59,11 +89,15 @@ export class SimpleJobExecutor {
|
|
|
59
89
|
duration: Date.now() - startTime,
|
|
60
90
|
};
|
|
61
91
|
}
|
|
62
|
-
await this.operationIndex.commit(indexTxn);
|
|
92
|
+
const ordinals = await this.operationIndex.commit(indexTxn);
|
|
63
93
|
if (result.operationsWithContext.length > 0) {
|
|
94
|
+
for (let i = 0; i < result.operationsWithContext.length; i++) {
|
|
95
|
+
result.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
96
|
+
}
|
|
64
97
|
const event = {
|
|
65
98
|
jobId: job.id,
|
|
66
99
|
operations: result.operationsWithContext,
|
|
100
|
+
jobMeta: job.meta,
|
|
67
101
|
};
|
|
68
102
|
this.eventBus
|
|
69
103
|
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
@@ -79,189 +113,37 @@ export class SimpleJobExecutor {
|
|
|
79
113
|
duration: Date.now() - startTime,
|
|
80
114
|
};
|
|
81
115
|
}
|
|
82
|
-
async processActions(job, actions, startTime, indexTxn, skipValues) {
|
|
116
|
+
async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
|
|
83
117
|
const generatedOperations = [];
|
|
84
118
|
const operationsWithContext = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
success: false,
|
|
107
|
-
generatedOperations,
|
|
108
|
-
operationsWithContext,
|
|
109
|
-
error: error.error,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
actionIndex++;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (action.type === "UPGRADE_DOCUMENT") {
|
|
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
|
-
};
|
|
125
|
-
}
|
|
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
|
-
};
|
|
139
|
-
}
|
|
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
|
-
};
|
|
153
|
-
}
|
|
154
|
-
actionIndex++;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
let document;
|
|
158
|
-
try {
|
|
159
|
-
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
160
|
-
}
|
|
161
|
-
catch (error) {
|
|
162
|
-
return {
|
|
163
|
-
success: false,
|
|
164
|
-
generatedOperations,
|
|
165
|
-
operationsWithContext,
|
|
166
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
const documentState = document.state.document;
|
|
170
|
-
if (documentState.isDeleted) {
|
|
171
|
-
return {
|
|
172
|
-
success: false,
|
|
173
|
-
generatedOperations,
|
|
174
|
-
operationsWithContext,
|
|
175
|
-
error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
let module;
|
|
179
|
-
try {
|
|
180
|
-
module = this.registry.getModule(document.header.documentType);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
return {
|
|
184
|
-
success: false,
|
|
185
|
-
generatedOperations,
|
|
186
|
-
operationsWithContext,
|
|
187
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
let updatedDocument;
|
|
191
|
-
try {
|
|
192
|
-
updatedDocument = module.reducer(document, action);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
|
|
196
|
-
const enhancedError = new Error(contextMessage);
|
|
197
|
-
if (error instanceof Error && error.stack) {
|
|
198
|
-
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
199
|
-
}
|
|
200
|
-
return {
|
|
201
|
-
success: false,
|
|
202
|
-
generatedOperations,
|
|
203
|
-
operationsWithContext,
|
|
204
|
-
error: enhancedError,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
const scope = job.scope;
|
|
208
|
-
const operations = updatedDocument.operations[scope];
|
|
209
|
-
if (operations.length === 0) {
|
|
210
|
-
return {
|
|
211
|
-
success: false,
|
|
212
|
-
generatedOperations,
|
|
213
|
-
operationsWithContext,
|
|
214
|
-
error: new Error("No operation generated from action"),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
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
|
-
}
|
|
235
|
-
const resultingState = JSON.stringify(updatedDocument.state);
|
|
236
|
-
try {
|
|
237
|
-
await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
238
|
-
txn.addOperations(newOperation);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
119
|
+
try {
|
|
120
|
+
await this.verifyActionSignatures(job, actions);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
generatedOperations,
|
|
126
|
+
operationsWithContext,
|
|
127
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
131
|
+
const action = actions[actionIndex];
|
|
132
|
+
const skip = skipValues?.[actionIndex] ?? 0;
|
|
133
|
+
const sourceOperation = sourceOperations?.[actionIndex];
|
|
134
|
+
const isDocumentAction = documentScopeActions.includes(action.type);
|
|
135
|
+
const result = isDocumentAction
|
|
136
|
+
? await this.executeDocumentAction(job, action, startTime, indexTxn, skip)
|
|
137
|
+
: await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation);
|
|
138
|
+
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
139
|
+
if (error !== null) {
|
|
242
140
|
return {
|
|
243
141
|
success: false,
|
|
244
142
|
generatedOperations,
|
|
245
143
|
operationsWithContext,
|
|
246
|
-
error:
|
|
144
|
+
error: error.error,
|
|
247
145
|
};
|
|
248
146
|
}
|
|
249
|
-
updatedDocument.header.revision = {
|
|
250
|
-
...updatedDocument.header.revision,
|
|
251
|
-
[scope]: newOperation.index + 1,
|
|
252
|
-
};
|
|
253
|
-
this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
|
|
254
|
-
operationsWithContext.push({
|
|
255
|
-
operation: newOperation,
|
|
256
|
-
context: {
|
|
257
|
-
documentId: job.documentId,
|
|
258
|
-
scope,
|
|
259
|
-
branch: job.branch,
|
|
260
|
-
documentType: document.header.documentType,
|
|
261
|
-
resultingState,
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
actionIndex++;
|
|
265
147
|
}
|
|
266
148
|
return {
|
|
267
149
|
success: true,
|
|
@@ -269,6 +151,26 @@ export class SimpleJobExecutor {
|
|
|
269
151
|
operationsWithContext,
|
|
270
152
|
};
|
|
271
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Execute a document scope action (CREATE_DOCUMENT, DELETE_DOCUMENT, UPGRADE_DOCUMENT,
|
|
156
|
+
* ADD_RELATIONSHIP, REMOVE_RELATIONSHIP) by dispatching to the appropriate handler.
|
|
157
|
+
*/
|
|
158
|
+
async executeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
159
|
+
switch (action.type) {
|
|
160
|
+
case "CREATE_DOCUMENT":
|
|
161
|
+
return this.executeCreateDocumentAction(job, action, startTime, indexTxn, skip);
|
|
162
|
+
case "DELETE_DOCUMENT":
|
|
163
|
+
return this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
|
|
164
|
+
case "UPGRADE_DOCUMENT":
|
|
165
|
+
return this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip);
|
|
166
|
+
case "ADD_RELATIONSHIP":
|
|
167
|
+
return this.executeAddRelationshipAction(job, action, startTime, indexTxn);
|
|
168
|
+
case "REMOVE_RELATIONSHIP":
|
|
169
|
+
return this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
|
|
170
|
+
default:
|
|
171
|
+
return this.buildErrorResult(job, new Error(`Unknown document action type: ${action.type}`), startTime);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
272
174
|
/**
|
|
273
175
|
* Execute a CREATE_DOCUMENT system action.
|
|
274
176
|
* This creates a new document in storage along with its initial operation.
|
|
@@ -293,7 +195,11 @@ export class SimpleJobExecutor {
|
|
|
293
195
|
return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
294
196
|
}
|
|
295
197
|
}
|
|
296
|
-
const operation = this.createOperation(action, 0, skip
|
|
198
|
+
const operation = this.createOperation(action, 0, skip, {
|
|
199
|
+
documentId: document.header.id,
|
|
200
|
+
scope: job.scope,
|
|
201
|
+
branch: job.branch,
|
|
202
|
+
});
|
|
297
203
|
// Legacy: Write the CREATE_DOCUMENT operation to legacy storage
|
|
298
204
|
if (this.config.legacyStorageEnabled) {
|
|
299
205
|
try {
|
|
@@ -333,6 +239,11 @@ export class SimpleJobExecutor {
|
|
|
333
239
|
indexTxn.createCollection(collectionId);
|
|
334
240
|
indexTxn.addToCollection(collectionId, document.header.id);
|
|
335
241
|
}
|
|
242
|
+
this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
243
|
+
state: document.state.document,
|
|
244
|
+
documentType: document.header.documentType,
|
|
245
|
+
documentScopeRevision: 1,
|
|
246
|
+
});
|
|
336
247
|
return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
337
248
|
}
|
|
338
249
|
/**
|
|
@@ -359,7 +270,11 @@ export class SimpleJobExecutor {
|
|
|
359
270
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
360
271
|
}
|
|
361
272
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
362
|
-
const operation = this.createOperation(action, nextIndex
|
|
273
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
274
|
+
documentId,
|
|
275
|
+
scope: job.scope,
|
|
276
|
+
branch: job.branch,
|
|
277
|
+
});
|
|
363
278
|
if (this.config.legacyStorageEnabled) {
|
|
364
279
|
try {
|
|
365
280
|
await this.documentStorage.delete(documentId);
|
|
@@ -390,11 +305,16 @@ export class SimpleJobExecutor {
|
|
|
390
305
|
scope: job.scope,
|
|
391
306
|
},
|
|
392
307
|
]);
|
|
308
|
+
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
309
|
+
state: document.state.document,
|
|
310
|
+
documentType: document.header.documentType,
|
|
311
|
+
documentScopeRevision: operation.index + 1,
|
|
312
|
+
});
|
|
393
313
|
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
394
314
|
}
|
|
395
315
|
/**
|
|
396
316
|
* Execute an UPGRADE_DOCUMENT system action.
|
|
397
|
-
*
|
|
317
|
+
* Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
|
|
398
318
|
* The operation index is determined from the document's current operation count.
|
|
399
319
|
*/
|
|
400
320
|
async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
@@ -403,6 +323,8 @@ export class SimpleJobExecutor {
|
|
|
403
323
|
return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
404
324
|
}
|
|
405
325
|
const documentId = input.documentId;
|
|
326
|
+
const fromVersion = input.fromVersion;
|
|
327
|
+
const toVersion = input.toVersion;
|
|
406
328
|
let document;
|
|
407
329
|
try {
|
|
408
330
|
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
@@ -415,8 +337,35 @@ export class SimpleJobExecutor {
|
|
|
415
337
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
416
338
|
}
|
|
417
339
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
418
|
-
|
|
419
|
-
|
|
340
|
+
let upgradePath;
|
|
341
|
+
if (fromVersion > 0 && fromVersion < toVersion) {
|
|
342
|
+
try {
|
|
343
|
+
upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (fromVersion === toVersion && fromVersion > 0) {
|
|
350
|
+
return {
|
|
351
|
+
job,
|
|
352
|
+
success: true,
|
|
353
|
+
operations: [],
|
|
354
|
+
operationsWithContext: [],
|
|
355
|
+
duration: Date.now() - startTime,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
document = applyUpgradeDocumentAction(document, action, upgradePath);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
363
|
+
}
|
|
364
|
+
const operation = this.createOperation(action, nextIndex, skip, {
|
|
365
|
+
documentId,
|
|
366
|
+
scope: job.scope,
|
|
367
|
+
branch: job.branch,
|
|
368
|
+
});
|
|
420
369
|
// Write the updated document to legacy storage
|
|
421
370
|
if (this.config.legacyStorageEnabled) {
|
|
422
371
|
try {
|
|
@@ -447,6 +396,11 @@ export class SimpleJobExecutor {
|
|
|
447
396
|
scope: job.scope,
|
|
448
397
|
},
|
|
449
398
|
]);
|
|
399
|
+
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
400
|
+
state: document.state.document,
|
|
401
|
+
documentType: document.header.documentType,
|
|
402
|
+
documentScopeRevision: operation.index + 1,
|
|
403
|
+
});
|
|
450
404
|
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
451
405
|
}
|
|
452
406
|
async executeAddRelationshipAction(job, action, startTime, indexTxn) {
|
|
@@ -467,19 +421,12 @@ export class SimpleJobExecutor {
|
|
|
467
421
|
catch (error) {
|
|
468
422
|
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
469
423
|
}
|
|
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
424
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
482
|
-
const operation = this.createOperation(action, nextIndex
|
|
425
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
426
|
+
documentId: input.sourceId,
|
|
427
|
+
scope: job.scope,
|
|
428
|
+
branch: job.branch,
|
|
429
|
+
});
|
|
483
430
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
484
431
|
if (writeError !== null) {
|
|
485
432
|
return writeError;
|
|
@@ -513,6 +460,11 @@ export class SimpleJobExecutor {
|
|
|
513
460
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
514
461
|
indexTxn.addToCollection(collectionId, input.targetId);
|
|
515
462
|
}
|
|
463
|
+
this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
464
|
+
state: sourceDoc.state.document,
|
|
465
|
+
documentType: sourceDoc.header.documentType,
|
|
466
|
+
documentScopeRevision: operation.index + 1,
|
|
467
|
+
});
|
|
516
468
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
517
469
|
}
|
|
518
470
|
async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
|
|
@@ -531,7 +483,11 @@ export class SimpleJobExecutor {
|
|
|
531
483
|
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
532
484
|
}
|
|
533
485
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
534
|
-
const operation = this.createOperation(action, nextIndex
|
|
486
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
487
|
+
documentId: input.sourceId,
|
|
488
|
+
scope: job.scope,
|
|
489
|
+
branch: job.branch,
|
|
490
|
+
});
|
|
535
491
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
536
492
|
if (writeError !== null) {
|
|
537
493
|
return writeError;
|
|
@@ -565,10 +521,127 @@ export class SimpleJobExecutor {
|
|
|
565
521
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
566
522
|
indexTxn.removeFromCollection(collectionId, input.targetId);
|
|
567
523
|
}
|
|
524
|
+
this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
525
|
+
state: sourceDoc.state.document,
|
|
526
|
+
documentType: sourceDoc.header.documentType,
|
|
527
|
+
documentScopeRevision: operation.index + 1,
|
|
528
|
+
});
|
|
568
529
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
569
530
|
}
|
|
570
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Execute a regular document action by applying it through the document model reducer.
|
|
533
|
+
* If sourceOperation is provided (for load jobs), its id and timestamp are preserved.
|
|
534
|
+
*/
|
|
535
|
+
async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
|
|
536
|
+
let docMeta;
|
|
537
|
+
try {
|
|
538
|
+
docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
542
|
+
}
|
|
543
|
+
if (docMeta.state.isDeleted) {
|
|
544
|
+
return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
545
|
+
}
|
|
546
|
+
let document;
|
|
547
|
+
try {
|
|
548
|
+
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
552
|
+
}
|
|
553
|
+
let module;
|
|
554
|
+
try {
|
|
555
|
+
// Use document version to get the correct module
|
|
556
|
+
// Version 0 means not yet upgraded - use latest version
|
|
557
|
+
const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
|
|
558
|
+
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
562
|
+
}
|
|
563
|
+
let updatedDocument;
|
|
564
|
+
try {
|
|
565
|
+
const reducerOptions = sourceOperation
|
|
566
|
+
? {
|
|
567
|
+
skip,
|
|
568
|
+
branch: job.branch,
|
|
569
|
+
replayOptions: { operation: sourceOperation },
|
|
570
|
+
}
|
|
571
|
+
: { skip, branch: job.branch };
|
|
572
|
+
updatedDocument = module.reducer(document, action, undefined, reducerOptions);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
const contextMessage = `Failed to apply action to document:\n Action type: ${action.type}\n Document ID: ${job.documentId}\n Document type: ${document.header.documentType}\n Scope: ${job.scope}\n Original error: ${error instanceof Error ? error.message : String(error)}`;
|
|
576
|
+
const enhancedError = new Error(contextMessage);
|
|
577
|
+
if (error instanceof Error && error.stack) {
|
|
578
|
+
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
579
|
+
}
|
|
580
|
+
return this.buildErrorResult(job, enhancedError, startTime);
|
|
581
|
+
}
|
|
582
|
+
const scope = job.scope;
|
|
583
|
+
const operations = updatedDocument.operations[scope];
|
|
584
|
+
if (operations.length === 0) {
|
|
585
|
+
return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
|
|
586
|
+
}
|
|
587
|
+
const newOperation = operations[operations.length - 1];
|
|
588
|
+
if (!isUndoRedo(action)) {
|
|
589
|
+
newOperation.skip = skip;
|
|
590
|
+
}
|
|
591
|
+
if (this.config.legacyStorageEnabled) {
|
|
592
|
+
try {
|
|
593
|
+
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const resultingState = JSON.stringify({
|
|
600
|
+
...updatedDocument.state,
|
|
601
|
+
header: updatedDocument.header,
|
|
602
|
+
});
|
|
603
|
+
const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
|
|
604
|
+
if (writeFailResult !== null) {
|
|
605
|
+
return writeFailResult;
|
|
606
|
+
}
|
|
607
|
+
updatedDocument.header.revision = {
|
|
608
|
+
...updatedDocument.header.revision,
|
|
609
|
+
[scope]: newOperation.index + 1,
|
|
610
|
+
};
|
|
611
|
+
this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
|
|
612
|
+
indexTxn.write([
|
|
613
|
+
{
|
|
614
|
+
...newOperation,
|
|
615
|
+
documentId: job.documentId,
|
|
616
|
+
documentType: document.header.documentType,
|
|
617
|
+
branch: job.branch,
|
|
618
|
+
scope,
|
|
619
|
+
},
|
|
620
|
+
]);
|
|
621
|
+
return {
|
|
622
|
+
job,
|
|
623
|
+
success: true,
|
|
624
|
+
operations: [newOperation],
|
|
625
|
+
operationsWithContext: [
|
|
626
|
+
{
|
|
627
|
+
operation: newOperation,
|
|
628
|
+
context: {
|
|
629
|
+
documentId: job.documentId,
|
|
630
|
+
scope,
|
|
631
|
+
branch: job.branch,
|
|
632
|
+
documentType: document.header.documentType,
|
|
633
|
+
resultingState,
|
|
634
|
+
ordinal: 0,
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
duration: Date.now() - startTime,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
createOperation(action, index, skip = 0, context) {
|
|
642
|
+
const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
|
|
571
643
|
return {
|
|
644
|
+
id,
|
|
572
645
|
index: index,
|
|
573
646
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
574
647
|
hash: "",
|
|
@@ -589,29 +662,81 @@ export class SimpleJobExecutor {
|
|
|
589
662
|
catch {
|
|
590
663
|
latestRevision = 0;
|
|
591
664
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
665
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
666
|
+
let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
|
|
667
|
+
for (const operation of job.operations) {
|
|
668
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
669
|
+
const ts = operation.timestampUtcMs || "";
|
|
670
|
+
if (ts < minIncomingTimestamp) {
|
|
671
|
+
minIncomingTimestamp = ts;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
let conflictingOps = [];
|
|
675
|
+
try {
|
|
676
|
+
const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
|
|
677
|
+
if (conflictingResult.hasMore) {
|
|
678
|
+
return {
|
|
679
|
+
job,
|
|
680
|
+
success: false,
|
|
681
|
+
error: new Error(`Excessive reshuffle detected: more than ${this.config.maxSkipThreshold} conflicting operations found. ` +
|
|
682
|
+
`This indicates a significant divergence between local and incoming operations.`),
|
|
683
|
+
duration: Date.now() - startTime,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
conflictingOps = conflictingResult.items;
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
conflictingOps = [];
|
|
690
|
+
}
|
|
691
|
+
// Filter out operations that have been superseded by later operations with skip values.
|
|
692
|
+
// An operation at index N is superseded if there exists an operation at index M > N
|
|
693
|
+
// where (M - skip_M) <= N, meaning the later operation's logical index covers N.
|
|
694
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
695
|
+
for (const laterOp of conflictingOps) {
|
|
696
|
+
if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
697
|
+
const logicalIndex = laterOp.index - laterOp.skip;
|
|
698
|
+
if (logicalIndex <= op.index) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
});
|
|
705
|
+
// All non-superseded conflicting operations need to be reshuffled
|
|
706
|
+
const existingOpsToReshuffle = nonSupersededOps;
|
|
707
|
+
// Skip count is the number of existing operations that need to be rewound
|
|
708
|
+
const skipCount = existingOpsToReshuffle.length;
|
|
709
|
+
if (skipCount > this.config.maxSkipThreshold) {
|
|
597
710
|
return {
|
|
598
711
|
job,
|
|
599
712
|
success: false,
|
|
600
|
-
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${
|
|
601
|
-
`This indicates
|
|
713
|
+
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${this.config.maxSkipThreshold}. ` +
|
|
714
|
+
`This indicates a significant divergence between local and incoming operations.`),
|
|
602
715
|
duration: Date.now() - startTime,
|
|
603
716
|
};
|
|
604
717
|
}
|
|
718
|
+
// Filter out incoming operations that are duplicates (action already exists locally
|
|
719
|
+
// or appears multiple times in incoming)
|
|
720
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
721
|
+
const seenIncomingActionIds = new Set();
|
|
722
|
+
const incomingOpsToApply = job.operations.filter((op) => {
|
|
723
|
+
if (existingActionIds.has(op.action.id))
|
|
724
|
+
return false;
|
|
725
|
+
if (seenIncomingActionIds.has(op.action.id))
|
|
726
|
+
return false;
|
|
727
|
+
seenIncomingActionIds.add(op.action.id);
|
|
728
|
+
return true;
|
|
729
|
+
});
|
|
605
730
|
const reshuffledOperations = reshuffleByTimestampAndIndex({
|
|
606
731
|
index: latestRevision,
|
|
607
732
|
skip: skipCount,
|
|
608
|
-
},
|
|
733
|
+
}, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
|
|
609
734
|
...operation,
|
|
610
735
|
id: operation.id,
|
|
611
736
|
})));
|
|
612
737
|
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
613
738
|
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
614
|
-
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
|
|
739
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
|
|
615
740
|
if (!result.success) {
|
|
616
741
|
return {
|
|
617
742
|
job,
|
|
@@ -620,18 +745,10 @@ export class SimpleJobExecutor {
|
|
|
620
745
|
duration: Date.now() - startTime,
|
|
621
746
|
};
|
|
622
747
|
}
|
|
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
748
|
this.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
749
|
+
if (scope === "document") {
|
|
750
|
+
this.documentMetaCache.invalidate(job.documentId, job.branch);
|
|
751
|
+
}
|
|
635
752
|
return {
|
|
636
753
|
job,
|
|
637
754
|
success: true,
|
|
@@ -648,6 +765,8 @@ export class SimpleJobExecutor {
|
|
|
648
765
|
return null;
|
|
649
766
|
}
|
|
650
767
|
catch (error) {
|
|
768
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
769
|
+
this.writeCache.invalidate(documentId, scope, branch);
|
|
651
770
|
return {
|
|
652
771
|
job,
|
|
653
772
|
success: false,
|
|
@@ -679,6 +798,7 @@ export class SimpleJobExecutor {
|
|
|
679
798
|
branch: job.branch,
|
|
680
799
|
documentType: documentType,
|
|
681
800
|
resultingState,
|
|
801
|
+
ordinal: 0,
|
|
682
802
|
},
|
|
683
803
|
},
|
|
684
804
|
],
|
|
@@ -693,6 +813,67 @@ export class SimpleJobExecutor {
|
|
|
693
813
|
duration: Date.now() - startTime,
|
|
694
814
|
};
|
|
695
815
|
}
|
|
816
|
+
async verifyOperationSignatures(job, operations) {
|
|
817
|
+
if (!this.signatureVerifier) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
for (let i = 0; i < operations.length; i++) {
|
|
821
|
+
const operation = operations[i];
|
|
822
|
+
const signer = operation.action.context?.signer;
|
|
823
|
+
if (!signer) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (signer.signatures.length === 0) {
|
|
827
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
828
|
+
}
|
|
829
|
+
const publicKey = signer.app.key;
|
|
830
|
+
let isValid = false;
|
|
831
|
+
try {
|
|
832
|
+
isValid = await this.signatureVerifier(operation, publicKey);
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
836
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
837
|
+
}
|
|
838
|
+
if (!isValid) {
|
|
839
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async verifyActionSignatures(job, actions) {
|
|
844
|
+
if (!this.signatureVerifier) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
for (const action of actions) {
|
|
848
|
+
const signer = action.context?.signer;
|
|
849
|
+
if (!signer) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (signer.signatures.length === 0) {
|
|
853
|
+
throw new InvalidSignatureError(job.documentId, `Action ${action.id} has signer but no signatures`);
|
|
854
|
+
}
|
|
855
|
+
const publicKey = signer.app.key;
|
|
856
|
+
let isValid = false;
|
|
857
|
+
try {
|
|
858
|
+
const tempOperation = {
|
|
859
|
+
id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
|
|
860
|
+
index: 0,
|
|
861
|
+
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
862
|
+
hash: "",
|
|
863
|
+
skip: 0,
|
|
864
|
+
action: action,
|
|
865
|
+
};
|
|
866
|
+
isValid = await this.signatureVerifier(tempOperation, publicKey);
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
870
|
+
throw new InvalidSignatureError(job.documentId, `Action ${action.id} verification failed: ${errorMessage}`);
|
|
871
|
+
}
|
|
872
|
+
if (!isValid) {
|
|
873
|
+
throw new InvalidSignatureError(job.documentId, `Action ${action.id} signature verification returned false`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
696
877
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
697
878
|
if (!result.success) {
|
|
698
879
|
return result;
|