@powerhousedao/reactor 5.2.0-staging.9 → 6.0.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.d.ts.map +1 -1
- package/dist/src/cache/document-meta-cache.js +2 -1
- package/dist/src/cache/document-meta-cache.js.map +1 -1
- package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
- package/dist/src/cache/kysely-operation-index.js +14 -25
- 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 +18 -5
- package/dist/src/cache/kysely-write-cache.js.map +1 -1
- package/dist/src/cache/operation-index-types.d.ts +1 -1
- package/dist/src/client/reactor-client.d.ts +16 -4
- package/dist/src/client/reactor-client.d.ts.map +1 -1
- package/dist/src/client/reactor-client.js +100 -3
- package/dist/src/client/reactor-client.js.map +1 -1
- package/dist/src/client/types.d.ts +22 -3
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/core/reactor-builder.d.ts +10 -9
- package/dist/src/core/reactor-builder.d.ts.map +1 -1
- package/dist/src/core/reactor-builder.js +52 -14
- package/dist/src/core/reactor-builder.js.map +1 -1
- package/dist/src/core/reactor-client-builder.d.ts +10 -1
- package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
- package/dist/src/core/reactor-client-builder.js +16 -1
- package/dist/src/core/reactor-client-builder.js.map +1 -1
- package/dist/src/core/reactor.d.ts +9 -8
- package/dist/src/core/reactor.d.ts.map +1 -1
- package/dist/src/core/reactor.js +64 -74
- package/dist/src/core/reactor.js.map +1 -1
- package/dist/src/core/types.d.ts +22 -7
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/utils.d.ts +1 -2
- package/dist/src/core/utils.d.ts.map +1 -1
- package/dist/src/core/utils.js +1 -1
- 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 -8
- package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
- package/dist/src/executor/simple-job-executor.d.ts +15 -3
- package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor.js +327 -230
- 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 +39 -9
- package/dist/src/executor/util.js.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging/console.d.ts +11 -1
- package/dist/src/logging/console.d.ts.map +1 -1
- package/dist/src/logging/console.js +45 -14
- package/dist/src/logging/console.js.map +1 -1
- package/dist/src/logging/types.d.ts +1 -0
- package/dist/src/logging/types.d.ts.map +1 -1
- 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/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.map +1 -1
- package/dist/src/read-models/document-view.js +2 -0
- package/dist/src/read-models/document-view.js.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 +16 -0
- package/dist/src/shared/errors.d.ts.map +1 -1
- package/dist/src/shared/errors.js +28 -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 +2 -22
- package/dist/src/signer/types.d.ts.map +1 -1
- package/dist/src/storage/interfaces.d.ts +13 -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 +40 -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.js +8 -8
- 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/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/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 +3 -0
- package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -1
- package/dist/src/sync/channels/composite-channel-factory.js +5 -1
- package/dist/src/sync/channels/composite-channel-factory.js.map +1 -1
- package/dist/src/sync/channels/gql-channel-factory.d.ts +3 -0
- package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel-factory.js +5 -1
- package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
- package/dist/src/sync/channels/gql-channel.d.ts +15 -1
- package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel.js +77 -16
- package/dist/src/sync/channels/gql-channel.js.map +1 -1
- package/dist/src/sync/channels/polling-channel.d.ts.map +1 -1
- package/dist/src/sync/channels/polling-channel.js +7 -5
- package/dist/src/sync/channels/polling-channel.js.map +1 -1
- 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/sync-builder.d.ts +3 -2
- package/dist/src/sync/sync-builder.d.ts.map +1 -1
- package/dist/src/sync/sync-builder.js +4 -4
- package/dist/src/sync/sync-builder.js.map +1 -1
- package/dist/src/sync/sync-manager.d.ts +3 -2
- package/dist/src/sync/sync-manager.d.ts.map +1 -1
- package/dist/src/sync/sync-manager.js +17 -13
- package/dist/src/sync/sync-manager.js.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 +3 -3
|
@@ -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
4
|
import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
|
|
4
|
-
import {
|
|
5
|
+
import { reshuffleByTimestamp } 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;
|
|
@@ -22,7 +31,8 @@ export class SimpleJobExecutor {
|
|
|
22
31
|
documentMetaCache;
|
|
23
32
|
signatureVerifier;
|
|
24
33
|
config;
|
|
25
|
-
constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
|
|
34
|
+
constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
|
|
35
|
+
this.logger = logger;
|
|
26
36
|
this.registry = registry;
|
|
27
37
|
this.documentStorage = documentStorage;
|
|
28
38
|
this.operationStorage = operationStorage;
|
|
@@ -33,6 +43,7 @@ export class SimpleJobExecutor {
|
|
|
33
43
|
this.documentMetaCache = documentMetaCache;
|
|
34
44
|
this.signatureVerifier = signatureVerifier;
|
|
35
45
|
this.config = {
|
|
46
|
+
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
36
47
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
37
48
|
jobTimeoutMs: config.jobTimeoutMs ?? 30000,
|
|
38
49
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
@@ -58,6 +69,7 @@ export class SimpleJobExecutor {
|
|
|
58
69
|
const event = {
|
|
59
70
|
jobId: job.id,
|
|
60
71
|
operations: result.operationsWithContext,
|
|
72
|
+
jobMeta: job.meta,
|
|
61
73
|
};
|
|
62
74
|
this.eventBus
|
|
63
75
|
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
@@ -85,6 +97,7 @@ export class SimpleJobExecutor {
|
|
|
85
97
|
const event = {
|
|
86
98
|
jobId: job.id,
|
|
87
99
|
operations: result.operationsWithContext,
|
|
100
|
+
jobMeta: job.meta,
|
|
88
101
|
};
|
|
89
102
|
this.eventBus
|
|
90
103
|
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
@@ -100,7 +113,7 @@ export class SimpleJobExecutor {
|
|
|
100
113
|
duration: Date.now() - startTime,
|
|
101
114
|
};
|
|
102
115
|
}
|
|
103
|
-
async processActions(job, actions, startTime, indexTxn, skipValues) {
|
|
116
|
+
async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
|
|
104
117
|
const generatedOperations = [];
|
|
105
118
|
const operationsWithContext = [];
|
|
106
119
|
try {
|
|
@@ -114,207 +127,23 @@ export class SimpleJobExecutor {
|
|
|
114
127
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
|
-
let actionIndex = 0;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
error: error.error,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
actionIndex++;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (action.type === "DELETE_DOCUMENT") {
|
|
134
|
-
const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
|
|
135
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
136
|
-
if (error !== null) {
|
|
137
|
-
return {
|
|
138
|
-
success: false,
|
|
139
|
-
generatedOperations,
|
|
140
|
-
operationsWithContext,
|
|
141
|
-
error: error.error,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
actionIndex++;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (action.type === "UPGRADE_DOCUMENT") {
|
|
148
|
-
const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
|
|
149
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
150
|
-
if (error !== null) {
|
|
151
|
-
return {
|
|
152
|
-
success: false,
|
|
153
|
-
generatedOperations,
|
|
154
|
-
operationsWithContext,
|
|
155
|
-
error: error.error,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
actionIndex++;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (action.type === "ADD_RELATIONSHIP") {
|
|
162
|
-
const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
|
|
163
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
164
|
-
if (error !== null) {
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
generatedOperations,
|
|
168
|
-
operationsWithContext,
|
|
169
|
-
error: error.error,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
actionIndex++;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (action.type === "REMOVE_RELATIONSHIP") {
|
|
176
|
-
const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
|
|
177
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
178
|
-
if (error !== null) {
|
|
179
|
-
return {
|
|
180
|
-
success: false,
|
|
181
|
-
generatedOperations,
|
|
182
|
-
operationsWithContext,
|
|
183
|
-
error: error.error,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
actionIndex++;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
let docMeta;
|
|
190
|
-
try {
|
|
191
|
-
docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
return {
|
|
195
|
-
success: false,
|
|
196
|
-
generatedOperations,
|
|
197
|
-
operationsWithContext,
|
|
198
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
if (docMeta.state.isDeleted) {
|
|
202
|
-
return {
|
|
203
|
-
success: false,
|
|
204
|
-
generatedOperations,
|
|
205
|
-
operationsWithContext,
|
|
206
|
-
error: new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
let document;
|
|
210
|
-
try {
|
|
211
|
-
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
generatedOperations,
|
|
217
|
-
operationsWithContext,
|
|
218
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
let module;
|
|
222
|
-
try {
|
|
223
|
-
module = this.registry.getModule(document.header.documentType);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
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) {
|
|
226
140
|
return {
|
|
227
141
|
success: false,
|
|
228
142
|
generatedOperations,
|
|
229
143
|
operationsWithContext,
|
|
230
|
-
error: error
|
|
144
|
+
error: error.error,
|
|
231
145
|
};
|
|
232
146
|
}
|
|
233
|
-
let updatedDocument;
|
|
234
|
-
try {
|
|
235
|
-
updatedDocument = module.reducer(document, action);
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
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)}`;
|
|
239
|
-
const enhancedError = new Error(contextMessage);
|
|
240
|
-
if (error instanceof Error && error.stack) {
|
|
241
|
-
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
242
|
-
}
|
|
243
|
-
return {
|
|
244
|
-
success: false,
|
|
245
|
-
generatedOperations,
|
|
246
|
-
operationsWithContext,
|
|
247
|
-
error: enhancedError,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
const scope = job.scope;
|
|
251
|
-
const operations = updatedDocument.operations[scope];
|
|
252
|
-
if (operations.length === 0) {
|
|
253
|
-
return {
|
|
254
|
-
success: false,
|
|
255
|
-
generatedOperations,
|
|
256
|
-
operationsWithContext,
|
|
257
|
-
error: new Error("No operation generated from action"),
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
const newOperation = operations[operations.length - 1];
|
|
261
|
-
if (skipValues && actionIndex < skipValues.length) {
|
|
262
|
-
newOperation.skip = skipValues[actionIndex];
|
|
263
|
-
}
|
|
264
|
-
generatedOperations.push(newOperation);
|
|
265
|
-
if (this.config.legacyStorageEnabled) {
|
|
266
|
-
try {
|
|
267
|
-
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
return {
|
|
271
|
-
success: false,
|
|
272
|
-
generatedOperations,
|
|
273
|
-
operationsWithContext,
|
|
274
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const resultingState = JSON.stringify(updatedDocument.state);
|
|
279
|
-
try {
|
|
280
|
-
await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
281
|
-
txn.addOperations(newOperation);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
return {
|
|
286
|
-
success: false,
|
|
287
|
-
generatedOperations,
|
|
288
|
-
operationsWithContext,
|
|
289
|
-
error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
updatedDocument.header.revision = {
|
|
293
|
-
...updatedDocument.header.revision,
|
|
294
|
-
[scope]: newOperation.index + 1,
|
|
295
|
-
};
|
|
296
|
-
this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
|
|
297
|
-
indexTxn.write([
|
|
298
|
-
{
|
|
299
|
-
...newOperation,
|
|
300
|
-
documentId: job.documentId,
|
|
301
|
-
documentType: document.header.documentType,
|
|
302
|
-
branch: job.branch,
|
|
303
|
-
scope,
|
|
304
|
-
},
|
|
305
|
-
]);
|
|
306
|
-
operationsWithContext.push({
|
|
307
|
-
operation: newOperation,
|
|
308
|
-
context: {
|
|
309
|
-
documentId: job.documentId,
|
|
310
|
-
scope,
|
|
311
|
-
branch: job.branch,
|
|
312
|
-
documentType: document.header.documentType,
|
|
313
|
-
resultingState,
|
|
314
|
-
ordinal: 0,
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
actionIndex++;
|
|
318
147
|
}
|
|
319
148
|
return {
|
|
320
149
|
success: true,
|
|
@@ -322,6 +151,26 @@ export class SimpleJobExecutor {
|
|
|
322
151
|
operationsWithContext,
|
|
323
152
|
};
|
|
324
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
|
+
}
|
|
325
174
|
/**
|
|
326
175
|
* Execute a CREATE_DOCUMENT system action.
|
|
327
176
|
* This creates a new document in storage along with its initial operation.
|
|
@@ -337,6 +186,15 @@ export class SimpleJobExecutor {
|
|
|
337
186
|
};
|
|
338
187
|
}
|
|
339
188
|
const document = createDocumentFromAction(action);
|
|
189
|
+
// DEBUG: Log protocolVersions during document creation
|
|
190
|
+
const createInput = action.input;
|
|
191
|
+
this.logger.info("DEBUG: CREATE_DOCUMENT: @Data", {
|
|
192
|
+
Data: {
|
|
193
|
+
documentId: document.header.id,
|
|
194
|
+
inputProtocolVersions: createInput.protocolVersions,
|
|
195
|
+
headerProtocolVersions: document.header.protocolVersions,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
340
198
|
// Legacy: Store the document in storage
|
|
341
199
|
if (this.config.legacyStorageEnabled) {
|
|
342
200
|
try {
|
|
@@ -346,7 +204,11 @@ export class SimpleJobExecutor {
|
|
|
346
204
|
return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
347
205
|
}
|
|
348
206
|
}
|
|
349
|
-
const operation = this.createOperation(action, 0, skip
|
|
207
|
+
const operation = this.createOperation(action, 0, skip, {
|
|
208
|
+
documentId: document.header.id,
|
|
209
|
+
scope: job.scope,
|
|
210
|
+
branch: job.branch,
|
|
211
|
+
});
|
|
350
212
|
// Legacy: Write the CREATE_DOCUMENT operation to legacy storage
|
|
351
213
|
if (this.config.legacyStorageEnabled) {
|
|
352
214
|
try {
|
|
@@ -417,7 +279,11 @@ export class SimpleJobExecutor {
|
|
|
417
279
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
418
280
|
}
|
|
419
281
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
420
|
-
const operation = this.createOperation(action, nextIndex
|
|
282
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
283
|
+
documentId,
|
|
284
|
+
scope: job.scope,
|
|
285
|
+
branch: job.branch,
|
|
286
|
+
});
|
|
421
287
|
if (this.config.legacyStorageEnabled) {
|
|
422
288
|
try {
|
|
423
289
|
await this.documentStorage.delete(documentId);
|
|
@@ -457,7 +323,7 @@ export class SimpleJobExecutor {
|
|
|
457
323
|
}
|
|
458
324
|
/**
|
|
459
325
|
* Execute an UPGRADE_DOCUMENT system action.
|
|
460
|
-
*
|
|
326
|
+
* Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
|
|
461
327
|
* The operation index is determined from the document's current operation count.
|
|
462
328
|
*/
|
|
463
329
|
async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
@@ -466,6 +332,8 @@ export class SimpleJobExecutor {
|
|
|
466
332
|
return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
467
333
|
}
|
|
468
334
|
const documentId = input.documentId;
|
|
335
|
+
const fromVersion = input.fromVersion;
|
|
336
|
+
const toVersion = input.toVersion;
|
|
469
337
|
let document;
|
|
470
338
|
try {
|
|
471
339
|
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
@@ -478,8 +346,35 @@ export class SimpleJobExecutor {
|
|
|
478
346
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
479
347
|
}
|
|
480
348
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
481
|
-
|
|
482
|
-
|
|
349
|
+
let upgradePath;
|
|
350
|
+
if (fromVersion > 0 && fromVersion < toVersion) {
|
|
351
|
+
try {
|
|
352
|
+
upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (fromVersion === toVersion && fromVersion > 0) {
|
|
359
|
+
return {
|
|
360
|
+
job,
|
|
361
|
+
success: true,
|
|
362
|
+
operations: [],
|
|
363
|
+
operationsWithContext: [],
|
|
364
|
+
duration: Date.now() - startTime,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
document = applyUpgradeDocumentAction(document, action, upgradePath);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
372
|
+
}
|
|
373
|
+
const operation = this.createOperation(action, nextIndex, skip, {
|
|
374
|
+
documentId,
|
|
375
|
+
scope: job.scope,
|
|
376
|
+
branch: job.branch,
|
|
377
|
+
});
|
|
483
378
|
// Write the updated document to legacy storage
|
|
484
379
|
if (this.config.legacyStorageEnabled) {
|
|
485
380
|
try {
|
|
@@ -535,19 +430,12 @@ export class SimpleJobExecutor {
|
|
|
535
430
|
catch (error) {
|
|
536
431
|
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
537
432
|
}
|
|
538
|
-
let targetDoc;
|
|
539
|
-
try {
|
|
540
|
-
targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
544
|
-
}
|
|
545
|
-
const targetDocState = targetDoc.state.document;
|
|
546
|
-
if (targetDocState.isDeleted) {
|
|
547
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
|
|
548
|
-
}
|
|
549
433
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
550
|
-
const operation = this.createOperation(action, nextIndex
|
|
434
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
435
|
+
documentId: input.sourceId,
|
|
436
|
+
scope: job.scope,
|
|
437
|
+
branch: job.branch,
|
|
438
|
+
});
|
|
551
439
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
552
440
|
if (writeError !== null) {
|
|
553
441
|
return writeError;
|
|
@@ -581,6 +469,11 @@ export class SimpleJobExecutor {
|
|
|
581
469
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
582
470
|
indexTxn.addToCollection(collectionId, input.targetId);
|
|
583
471
|
}
|
|
472
|
+
this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
473
|
+
state: sourceDoc.state.document,
|
|
474
|
+
documentType: sourceDoc.header.documentType,
|
|
475
|
+
documentScopeRevision: operation.index + 1,
|
|
476
|
+
});
|
|
584
477
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
585
478
|
}
|
|
586
479
|
async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
|
|
@@ -599,7 +492,11 @@ export class SimpleJobExecutor {
|
|
|
599
492
|
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
600
493
|
}
|
|
601
494
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
602
|
-
const operation = this.createOperation(action, nextIndex
|
|
495
|
+
const operation = this.createOperation(action, nextIndex, 0, {
|
|
496
|
+
documentId: input.sourceId,
|
|
497
|
+
scope: job.scope,
|
|
498
|
+
branch: job.branch,
|
|
499
|
+
});
|
|
603
500
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
604
501
|
if (writeError !== null) {
|
|
605
502
|
return writeError;
|
|
@@ -633,10 +530,149 @@ export class SimpleJobExecutor {
|
|
|
633
530
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
634
531
|
indexTxn.removeFromCollection(collectionId, input.targetId);
|
|
635
532
|
}
|
|
533
|
+
this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
|
|
534
|
+
state: sourceDoc.state.document,
|
|
535
|
+
documentType: sourceDoc.header.documentType,
|
|
536
|
+
documentScopeRevision: operation.index + 1,
|
|
537
|
+
});
|
|
636
538
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
637
539
|
}
|
|
638
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Execute a regular document action by applying it through the document model reducer.
|
|
542
|
+
* If sourceOperation is provided (for load jobs), its id and timestamp are preserved.
|
|
543
|
+
*/
|
|
544
|
+
async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
|
|
545
|
+
let docMeta;
|
|
546
|
+
try {
|
|
547
|
+
docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
551
|
+
}
|
|
552
|
+
if (docMeta.state.isDeleted) {
|
|
553
|
+
return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
554
|
+
}
|
|
555
|
+
let document;
|
|
556
|
+
try {
|
|
557
|
+
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
561
|
+
}
|
|
562
|
+
// DEBUG: Log document state from cache before reducer
|
|
563
|
+
const docOpsFromCache = document.operations[job.scope] || [];
|
|
564
|
+
const protocolVersionFromHeader = document.header.protocolVersions?.["base-reducer"];
|
|
565
|
+
this.logger.info("DEBUG: Document from cache: @Document", {
|
|
566
|
+
documentId: job.documentId,
|
|
567
|
+
scope: job.scope,
|
|
568
|
+
actionType: action.type,
|
|
569
|
+
protocolVersionFromHeader,
|
|
570
|
+
operationCount: docOpsFromCache.length,
|
|
571
|
+
operationIndices: docOpsFromCache.map((op) => op.index),
|
|
572
|
+
operationTypes: docOpsFromCache.map((op) => op.action.type),
|
|
573
|
+
});
|
|
574
|
+
let module;
|
|
575
|
+
try {
|
|
576
|
+
// Use document version to get the correct module
|
|
577
|
+
// Version 0 means not yet upgraded - use latest version
|
|
578
|
+
const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
|
|
579
|
+
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
583
|
+
}
|
|
584
|
+
let updatedDocument;
|
|
585
|
+
try {
|
|
586
|
+
const protocolVersion = document.header.protocolVersions?.["base-reducer"] ?? 1;
|
|
587
|
+
const reducerOptions = sourceOperation
|
|
588
|
+
? {
|
|
589
|
+
skip,
|
|
590
|
+
branch: job.branch,
|
|
591
|
+
replayOptions: { operation: sourceOperation },
|
|
592
|
+
protocolVersion,
|
|
593
|
+
}
|
|
594
|
+
: { skip, branch: job.branch, protocolVersion };
|
|
595
|
+
updatedDocument = module.reducer(document, action, undefined, reducerOptions);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
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)}`;
|
|
599
|
+
const enhancedError = new Error(contextMessage);
|
|
600
|
+
if (error instanceof Error && error.stack) {
|
|
601
|
+
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
602
|
+
}
|
|
603
|
+
return this.buildErrorResult(job, enhancedError, startTime);
|
|
604
|
+
}
|
|
605
|
+
const scope = job.scope;
|
|
606
|
+
const operations = updatedDocument.operations[scope];
|
|
607
|
+
// DEBUG: Log document state after reducer
|
|
608
|
+
this.logger.info("DEBUG: Document after reducer: @Document", {
|
|
609
|
+
documentId: job.documentId,
|
|
610
|
+
scope,
|
|
611
|
+
operationCount: operations.length,
|
|
612
|
+
operationIndices: operations.map((op) => op.index),
|
|
613
|
+
operationTypes: operations.map((op) => op.action.type),
|
|
614
|
+
});
|
|
615
|
+
if (operations.length === 0) {
|
|
616
|
+
return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
|
|
617
|
+
}
|
|
618
|
+
const newOperation = operations[operations.length - 1];
|
|
619
|
+
if (!isUndoRedo(action)) {
|
|
620
|
+
newOperation.skip = skip;
|
|
621
|
+
}
|
|
622
|
+
if (this.config.legacyStorageEnabled) {
|
|
623
|
+
try {
|
|
624
|
+
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const resultingState = JSON.stringify({
|
|
631
|
+
...updatedDocument.state,
|
|
632
|
+
header: updatedDocument.header,
|
|
633
|
+
});
|
|
634
|
+
const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
|
|
635
|
+
if (writeFailResult !== null) {
|
|
636
|
+
return writeFailResult;
|
|
637
|
+
}
|
|
638
|
+
updatedDocument.header.revision = {
|
|
639
|
+
...updatedDocument.header.revision,
|
|
640
|
+
[scope]: newOperation.index + 1,
|
|
641
|
+
};
|
|
642
|
+
this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
|
|
643
|
+
indexTxn.write([
|
|
644
|
+
{
|
|
645
|
+
...newOperation,
|
|
646
|
+
documentId: job.documentId,
|
|
647
|
+
documentType: document.header.documentType,
|
|
648
|
+
branch: job.branch,
|
|
649
|
+
scope,
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
return {
|
|
653
|
+
job,
|
|
654
|
+
success: true,
|
|
655
|
+
operations: [newOperation],
|
|
656
|
+
operationsWithContext: [
|
|
657
|
+
{
|
|
658
|
+
operation: newOperation,
|
|
659
|
+
context: {
|
|
660
|
+
documentId: job.documentId,
|
|
661
|
+
scope,
|
|
662
|
+
branch: job.branch,
|
|
663
|
+
documentType: document.header.documentType,
|
|
664
|
+
resultingState,
|
|
665
|
+
ordinal: 0,
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
duration: Date.now() - startTime,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
createOperation(action, index, skip = 0, context) {
|
|
673
|
+
const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
|
|
639
674
|
return {
|
|
675
|
+
id,
|
|
640
676
|
index: index,
|
|
641
677
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
642
678
|
hash: "",
|
|
@@ -657,29 +693,87 @@ export class SimpleJobExecutor {
|
|
|
657
693
|
catch {
|
|
658
694
|
latestRevision = 0;
|
|
659
695
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
696
|
+
let minIncomingIndex = Number.POSITIVE_INFINITY;
|
|
697
|
+
let minIncomingTimestamp = job.operations[0]?.timestampUtcMs || "";
|
|
698
|
+
for (const operation of job.operations) {
|
|
699
|
+
minIncomingIndex = Math.min(minIncomingIndex, operation.index);
|
|
700
|
+
const ts = operation.timestampUtcMs || "";
|
|
701
|
+
if (ts < minIncomingTimestamp) {
|
|
702
|
+
minIncomingTimestamp = ts;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
let conflictingOps = [];
|
|
706
|
+
try {
|
|
707
|
+
const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
|
|
708
|
+
if (conflictingResult.hasMore) {
|
|
709
|
+
return {
|
|
710
|
+
job,
|
|
711
|
+
success: false,
|
|
712
|
+
error: new Error(`Excessive reshuffle detected: more than ${this.config.maxSkipThreshold} conflicting operations found. ` +
|
|
713
|
+
`This indicates a significant divergence between local and incoming operations.`),
|
|
714
|
+
duration: Date.now() - startTime,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
conflictingOps = conflictingResult.items;
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
conflictingOps = [];
|
|
721
|
+
}
|
|
722
|
+
// Filter out operations that have been superseded by later operations with skip values.
|
|
723
|
+
// An operation at index N is superseded if there exists an operation at index M > N
|
|
724
|
+
// where (M - skip_M) <= N, meaning the later operation's logical index covers N.
|
|
725
|
+
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
726
|
+
for (const laterOp of conflictingOps) {
|
|
727
|
+
if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
728
|
+
const logicalIndex = laterOp.index - laterOp.skip;
|
|
729
|
+
if (logicalIndex <= op.index) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return true;
|
|
735
|
+
});
|
|
736
|
+
// All non-superseded conflicting operations need to be reshuffled
|
|
737
|
+
const existingOpsToReshuffle = nonSupersededOps;
|
|
738
|
+
// Skip count is the number of existing operations that need to be rewound
|
|
739
|
+
const skipCount = existingOpsToReshuffle.length;
|
|
740
|
+
if (skipCount > this.config.maxSkipThreshold) {
|
|
665
741
|
return {
|
|
666
742
|
job,
|
|
667
743
|
success: false,
|
|
668
|
-
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${
|
|
669
|
-
`This indicates
|
|
744
|
+
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${this.config.maxSkipThreshold}. ` +
|
|
745
|
+
`This indicates a significant divergence between local and incoming operations.`),
|
|
670
746
|
duration: Date.now() - startTime,
|
|
671
747
|
};
|
|
672
748
|
}
|
|
673
|
-
|
|
749
|
+
// Filter out incoming operations that are duplicates (action already exists locally
|
|
750
|
+
// or appears multiple times in incoming)
|
|
751
|
+
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
752
|
+
const seenIncomingActionIds = new Set();
|
|
753
|
+
const incomingOpsToApply = job.operations.filter((op) => {
|
|
754
|
+
if (existingActionIds.has(op.action.id))
|
|
755
|
+
return false;
|
|
756
|
+
if (seenIncomingActionIds.has(op.action.id))
|
|
757
|
+
return false;
|
|
758
|
+
seenIncomingActionIds.add(op.action.id);
|
|
759
|
+
return true;
|
|
760
|
+
});
|
|
761
|
+
const reshuffledOperations = reshuffleByTimestamp({
|
|
674
762
|
index: latestRevision,
|
|
675
763
|
skip: skipCount,
|
|
676
|
-
},
|
|
764
|
+
}, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
|
|
677
765
|
...operation,
|
|
678
766
|
id: operation.id,
|
|
679
767
|
})));
|
|
768
|
+
// For v2, all NOOPs have skip=1 - consecutive NOOPs are handled during state rebuild
|
|
769
|
+
for (const operation of reshuffledOperations) {
|
|
770
|
+
if (operation.action.type === "NOOP") {
|
|
771
|
+
operation.skip = 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
680
774
|
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
681
775
|
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
682
|
-
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
|
|
776
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
|
|
683
777
|
if (!result.success) {
|
|
684
778
|
return {
|
|
685
779
|
job,
|
|
@@ -708,6 +802,8 @@ export class SimpleJobExecutor {
|
|
|
708
802
|
return null;
|
|
709
803
|
}
|
|
710
804
|
catch (error) {
|
|
805
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
806
|
+
this.writeCache.invalidate(documentId, scope, branch);
|
|
711
807
|
return {
|
|
712
808
|
job,
|
|
713
809
|
success: false,
|
|
@@ -765,7 +861,7 @@ export class SimpleJobExecutor {
|
|
|
765
861
|
continue;
|
|
766
862
|
}
|
|
767
863
|
if (signer.signatures.length === 0) {
|
|
768
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
864
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
769
865
|
}
|
|
770
866
|
const publicKey = signer.app.key;
|
|
771
867
|
let isValid = false;
|
|
@@ -774,10 +870,10 @@ export class SimpleJobExecutor {
|
|
|
774
870
|
}
|
|
775
871
|
catch (error) {
|
|
776
872
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
777
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
873
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
778
874
|
}
|
|
779
875
|
if (!isValid) {
|
|
780
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
876
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
781
877
|
}
|
|
782
878
|
}
|
|
783
879
|
}
|
|
@@ -797,6 +893,7 @@ export class SimpleJobExecutor {
|
|
|
797
893
|
let isValid = false;
|
|
798
894
|
try {
|
|
799
895
|
const tempOperation = {
|
|
896
|
+
id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
|
|
800
897
|
index: 0,
|
|
801
898
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
802
899
|
hash: "",
|