@powerhousedao/reactor 5.1.0-staging.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 +16 -4
- package/dist/src/client/reactor-client.d.ts.map +1 -1
- package/dist/src/client/reactor-client.js +122 -5
- 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 +73 -17
- 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 +11 -10
- package/dist/src/core/reactor.d.ts.map +1 -1
- package/dist/src/core/reactor.js +78 -91
- 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 +2 -2
- 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 +17 -3
- package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor.js +329 -221
- 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 +12 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -2
- 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/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 +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/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.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/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/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 +5 -2
- package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel-factory.js +8 -2
- package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
- package/dist/src/sync/channels/gql-channel.d.ts +28 -1
- package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel.js +150 -22
- package/dist/src/sync/channels/gql-channel.js.map +1 -1
- package/dist/src/sync/channels/index.d.ts +2 -1
- package/dist/src/sync/channels/index.d.ts.map +1 -1
- package/dist/src/sync/channels/index.js +2 -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 +16 -1
- package/dist/src/sync/interfaces.d.ts.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 +4 -1
- package/dist/src/sync/sync-manager.d.ts.map +1 -1
- package/dist/src/sync/sync-manager.js +65 -8
- 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
- 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
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,9 +28,11 @@ export class SimpleJobExecutor {
|
|
|
19
28
|
eventBus;
|
|
20
29
|
writeCache;
|
|
21
30
|
operationIndex;
|
|
31
|
+
documentMetaCache;
|
|
22
32
|
signatureVerifier;
|
|
23
33
|
config;
|
|
24
|
-
constructor(registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, config, signatureVerifier) {
|
|
34
|
+
constructor(logger, registry, documentStorage, operationStorage, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, config, signatureVerifier) {
|
|
35
|
+
this.logger = logger;
|
|
25
36
|
this.registry = registry;
|
|
26
37
|
this.documentStorage = documentStorage;
|
|
27
38
|
this.operationStorage = operationStorage;
|
|
@@ -29,8 +40,10 @@ export class SimpleJobExecutor {
|
|
|
29
40
|
this.eventBus = eventBus;
|
|
30
41
|
this.writeCache = writeCache;
|
|
31
42
|
this.operationIndex = operationIndex;
|
|
43
|
+
this.documentMetaCache = documentMetaCache;
|
|
32
44
|
this.signatureVerifier = signatureVerifier;
|
|
33
45
|
this.config = {
|
|
46
|
+
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
34
47
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
35
48
|
jobTimeoutMs: config.jobTimeoutMs ?? 30000,
|
|
36
49
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
@@ -47,8 +60,23 @@ export class SimpleJobExecutor {
|
|
|
47
60
|
const indexTxn = this.operationIndex.start();
|
|
48
61
|
if (job.kind === "load") {
|
|
49
62
|
const result = await this.executeLoadJob(job, startTime, indexTxn);
|
|
50
|
-
if (result.success) {
|
|
51
|
-
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
|
+
}
|
|
52
80
|
}
|
|
53
81
|
return result;
|
|
54
82
|
}
|
|
@@ -61,11 +89,15 @@ export class SimpleJobExecutor {
|
|
|
61
89
|
duration: Date.now() - startTime,
|
|
62
90
|
};
|
|
63
91
|
}
|
|
64
|
-
await this.operationIndex.commit(indexTxn);
|
|
92
|
+
const ordinals = await this.operationIndex.commit(indexTxn);
|
|
65
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
|
+
}
|
|
66
97
|
const event = {
|
|
67
98
|
jobId: job.id,
|
|
68
99
|
operations: result.operationsWithContext,
|
|
100
|
+
jobMeta: job.meta,
|
|
69
101
|
};
|
|
70
102
|
this.eventBus
|
|
71
103
|
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
@@ -81,7 +113,7 @@ export class SimpleJobExecutor {
|
|
|
81
113
|
duration: Date.now() - startTime,
|
|
82
114
|
};
|
|
83
115
|
}
|
|
84
|
-
async processActions(job, actions, startTime, indexTxn, skipValues) {
|
|
116
|
+
async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
|
|
85
117
|
const generatedOperations = [];
|
|
86
118
|
const operationsWithContext = [];
|
|
87
119
|
try {
|
|
@@ -95,186 +127,23 @@ export class SimpleJobExecutor {
|
|
|
95
127
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
96
128
|
};
|
|
97
129
|
}
|
|
98
|
-
let actionIndex = 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
error: error.error,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
actionIndex++;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (action.type === "DELETE_DOCUMENT") {
|
|
115
|
-
const result = await this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
|
|
116
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
117
|
-
if (error !== null) {
|
|
118
|
-
return {
|
|
119
|
-
success: false,
|
|
120
|
-
generatedOperations,
|
|
121
|
-
operationsWithContext,
|
|
122
|
-
error: error.error,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
actionIndex++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (action.type === "UPGRADE_DOCUMENT") {
|
|
129
|
-
const result = await this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skipValues?.[actionIndex]);
|
|
130
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
131
|
-
if (error !== null) {
|
|
132
|
-
return {
|
|
133
|
-
success: false,
|
|
134
|
-
generatedOperations,
|
|
135
|
-
operationsWithContext,
|
|
136
|
-
error: error.error,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
actionIndex++;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (action.type === "ADD_RELATIONSHIP") {
|
|
143
|
-
const result = await this.executeAddRelationshipAction(job, action, startTime, indexTxn);
|
|
144
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
145
|
-
if (error !== null) {
|
|
146
|
-
return {
|
|
147
|
-
success: false,
|
|
148
|
-
generatedOperations,
|
|
149
|
-
operationsWithContext,
|
|
150
|
-
error: error.error,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
actionIndex++;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (action.type === "REMOVE_RELATIONSHIP") {
|
|
157
|
-
const result = await this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
|
|
158
|
-
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
159
|
-
if (error !== null) {
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
generatedOperations,
|
|
163
|
-
operationsWithContext,
|
|
164
|
-
error: error.error,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
actionIndex++;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
let document;
|
|
171
|
-
try {
|
|
172
|
-
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
return {
|
|
176
|
-
success: false,
|
|
177
|
-
generatedOperations,
|
|
178
|
-
operationsWithContext,
|
|
179
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
const documentState = document.state.document;
|
|
183
|
-
if (documentState.isDeleted) {
|
|
184
|
-
return {
|
|
185
|
-
success: false,
|
|
186
|
-
generatedOperations,
|
|
187
|
-
operationsWithContext,
|
|
188
|
-
error: new DocumentDeletedError(job.documentId, documentState.deletedAtUtcIso),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
let module;
|
|
192
|
-
try {
|
|
193
|
-
module = this.registry.getModule(document.header.documentType);
|
|
194
|
-
}
|
|
195
|
-
catch (error) {
|
|
196
|
-
return {
|
|
197
|
-
success: false,
|
|
198
|
-
generatedOperations,
|
|
199
|
-
operationsWithContext,
|
|
200
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
let updatedDocument;
|
|
204
|
-
try {
|
|
205
|
-
updatedDocument = module.reducer(document, action);
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
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)}`;
|
|
209
|
-
const enhancedError = new Error(contextMessage);
|
|
210
|
-
if (error instanceof Error && error.stack) {
|
|
211
|
-
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
success: false,
|
|
215
|
-
generatedOperations,
|
|
216
|
-
operationsWithContext,
|
|
217
|
-
error: enhancedError,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
const scope = job.scope;
|
|
221
|
-
const operations = updatedDocument.operations[scope];
|
|
222
|
-
if (operations.length === 0) {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
generatedOperations,
|
|
226
|
-
operationsWithContext,
|
|
227
|
-
error: new Error("No operation generated from action"),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
const newOperation = operations[operations.length - 1];
|
|
231
|
-
if (skipValues && actionIndex < skipValues.length) {
|
|
232
|
-
newOperation.skip = skipValues[actionIndex];
|
|
233
|
-
}
|
|
234
|
-
generatedOperations.push(newOperation);
|
|
235
|
-
if (this.config.legacyStorageEnabled) {
|
|
236
|
-
try {
|
|
237
|
-
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
return {
|
|
241
|
-
success: false,
|
|
242
|
-
generatedOperations,
|
|
243
|
-
operationsWithContext,
|
|
244
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const resultingState = JSON.stringify(updatedDocument.state);
|
|
249
|
-
try {
|
|
250
|
-
await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
251
|
-
txn.addOperations(newOperation);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
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) {
|
|
255
140
|
return {
|
|
256
141
|
success: false,
|
|
257
142
|
generatedOperations,
|
|
258
143
|
operationsWithContext,
|
|
259
|
-
error:
|
|
144
|
+
error: error.error,
|
|
260
145
|
};
|
|
261
146
|
}
|
|
262
|
-
updatedDocument.header.revision = {
|
|
263
|
-
...updatedDocument.header.revision,
|
|
264
|
-
[scope]: newOperation.index + 1,
|
|
265
|
-
};
|
|
266
|
-
this.writeCache.putState(job.documentId, scope, job.branch, newOperation.index, updatedDocument);
|
|
267
|
-
operationsWithContext.push({
|
|
268
|
-
operation: newOperation,
|
|
269
|
-
context: {
|
|
270
|
-
documentId: job.documentId,
|
|
271
|
-
scope,
|
|
272
|
-
branch: job.branch,
|
|
273
|
-
documentType: document.header.documentType,
|
|
274
|
-
resultingState,
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
actionIndex++;
|
|
278
147
|
}
|
|
279
148
|
return {
|
|
280
149
|
success: true,
|
|
@@ -282,6 +151,26 @@ export class SimpleJobExecutor {
|
|
|
282
151
|
operationsWithContext,
|
|
283
152
|
};
|
|
284
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
|
+
}
|
|
285
174
|
/**
|
|
286
175
|
* Execute a CREATE_DOCUMENT system action.
|
|
287
176
|
* This creates a new document in storage along with its initial operation.
|
|
@@ -306,7 +195,11 @@ export class SimpleJobExecutor {
|
|
|
306
195
|
return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
307
196
|
}
|
|
308
197
|
}
|
|
309
|
-
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
|
+
});
|
|
310
203
|
// Legacy: Write the CREATE_DOCUMENT operation to legacy storage
|
|
311
204
|
if (this.config.legacyStorageEnabled) {
|
|
312
205
|
try {
|
|
@@ -346,6 +239,11 @@ export class SimpleJobExecutor {
|
|
|
346
239
|
indexTxn.createCollection(collectionId);
|
|
347
240
|
indexTxn.addToCollection(collectionId, document.header.id);
|
|
348
241
|
}
|
|
242
|
+
this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
243
|
+
state: document.state.document,
|
|
244
|
+
documentType: document.header.documentType,
|
|
245
|
+
documentScopeRevision: 1,
|
|
246
|
+
});
|
|
349
247
|
return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
350
248
|
}
|
|
351
249
|
/**
|
|
@@ -372,7 +270,11 @@ export class SimpleJobExecutor {
|
|
|
372
270
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
373
271
|
}
|
|
374
272
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
375
|
-
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
|
+
});
|
|
376
278
|
if (this.config.legacyStorageEnabled) {
|
|
377
279
|
try {
|
|
378
280
|
await this.documentStorage.delete(documentId);
|
|
@@ -403,11 +305,16 @@ export class SimpleJobExecutor {
|
|
|
403
305
|
scope: job.scope,
|
|
404
306
|
},
|
|
405
307
|
]);
|
|
308
|
+
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
309
|
+
state: document.state.document,
|
|
310
|
+
documentType: document.header.documentType,
|
|
311
|
+
documentScopeRevision: operation.index + 1,
|
|
312
|
+
});
|
|
406
313
|
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
407
314
|
}
|
|
408
315
|
/**
|
|
409
316
|
* Execute an UPGRADE_DOCUMENT system action.
|
|
410
|
-
*
|
|
317
|
+
* Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
|
|
411
318
|
* The operation index is determined from the document's current operation count.
|
|
412
319
|
*/
|
|
413
320
|
async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
@@ -416,6 +323,8 @@ export class SimpleJobExecutor {
|
|
|
416
323
|
return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
417
324
|
}
|
|
418
325
|
const documentId = input.documentId;
|
|
326
|
+
const fromVersion = input.fromVersion;
|
|
327
|
+
const toVersion = input.toVersion;
|
|
419
328
|
let document;
|
|
420
329
|
try {
|
|
421
330
|
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
@@ -428,8 +337,35 @@ export class SimpleJobExecutor {
|
|
|
428
337
|
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
429
338
|
}
|
|
430
339
|
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
});
|
|
433
369
|
// Write the updated document to legacy storage
|
|
434
370
|
if (this.config.legacyStorageEnabled) {
|
|
435
371
|
try {
|
|
@@ -460,6 +396,11 @@ export class SimpleJobExecutor {
|
|
|
460
396
|
scope: job.scope,
|
|
461
397
|
},
|
|
462
398
|
]);
|
|
399
|
+
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
400
|
+
state: document.state.document,
|
|
401
|
+
documentType: document.header.documentType,
|
|
402
|
+
documentScopeRevision: operation.index + 1,
|
|
403
|
+
});
|
|
463
404
|
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
464
405
|
}
|
|
465
406
|
async executeAddRelationshipAction(job, action, startTime, indexTxn) {
|
|
@@ -480,19 +421,12 @@ export class SimpleJobExecutor {
|
|
|
480
421
|
catch (error) {
|
|
481
422
|
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
482
423
|
}
|
|
483
|
-
let targetDoc;
|
|
484
|
-
try {
|
|
485
|
-
targetDoc = await this.writeCache.getState(input.targetId, "document", job.branch);
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
489
|
-
}
|
|
490
|
-
const targetDocState = targetDoc.state.document;
|
|
491
|
-
if (targetDocState.isDeleted) {
|
|
492
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: target document ${input.targetId} is deleted`), startTime);
|
|
493
|
-
}
|
|
494
424
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
495
|
-
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
|
+
});
|
|
496
430
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
497
431
|
if (writeError !== null) {
|
|
498
432
|
return writeError;
|
|
@@ -526,6 +460,11 @@ export class SimpleJobExecutor {
|
|
|
526
460
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
527
461
|
indexTxn.addToCollection(collectionId, input.targetId);
|
|
528
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
|
+
});
|
|
529
468
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
530
469
|
}
|
|
531
470
|
async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
|
|
@@ -544,7 +483,11 @@ export class SimpleJobExecutor {
|
|
|
544
483
|
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
545
484
|
}
|
|
546
485
|
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
547
|
-
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
|
+
});
|
|
548
491
|
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
549
492
|
if (writeError !== null) {
|
|
550
493
|
return writeError;
|
|
@@ -578,10 +521,127 @@ export class SimpleJobExecutor {
|
|
|
578
521
|
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
579
522
|
indexTxn.removeFromCollection(collectionId, input.targetId);
|
|
580
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
|
+
});
|
|
581
529
|
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
582
530
|
}
|
|
583
|
-
|
|
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);
|
|
584
643
|
return {
|
|
644
|
+
id,
|
|
585
645
|
index: index,
|
|
586
646
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
587
647
|
hash: "",
|
|
@@ -602,29 +662,81 @@ export class SimpleJobExecutor {
|
|
|
602
662
|
catch {
|
|
603
663
|
latestRevision = 0;
|
|
604
664
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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) {
|
|
610
710
|
return {
|
|
611
711
|
job,
|
|
612
712
|
success: false,
|
|
613
|
-
error: new Error(`Excessive reshuffle detected: skip count of ${skipCount} exceeds threshold of ${
|
|
614
|
-
`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.`),
|
|
615
715
|
duration: Date.now() - startTime,
|
|
616
716
|
};
|
|
617
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
|
+
});
|
|
618
730
|
const reshuffledOperations = reshuffleByTimestampAndIndex({
|
|
619
731
|
index: latestRevision,
|
|
620
732
|
skip: skipCount,
|
|
621
|
-
},
|
|
733
|
+
}, existingOpsToReshuffle, incomingOpsToApply.map((operation) => ({
|
|
622
734
|
...operation,
|
|
623
735
|
id: operation.id,
|
|
624
736
|
})));
|
|
625
737
|
const actions = reshuffledOperations.map((operation) => operation.action);
|
|
626
738
|
const skipValues = reshuffledOperations.map((operation) => operation.skip);
|
|
627
|
-
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues);
|
|
739
|
+
const result = await this.processActions(job, actions, startTime, indexTxn, skipValues, reshuffledOperations);
|
|
628
740
|
if (!result.success) {
|
|
629
741
|
return {
|
|
630
742
|
job,
|
|
@@ -633,18 +745,10 @@ export class SimpleJobExecutor {
|
|
|
633
745
|
duration: Date.now() - startTime,
|
|
634
746
|
};
|
|
635
747
|
}
|
|
636
|
-
if (result.operationsWithContext.length > 0) {
|
|
637
|
-
const event = {
|
|
638
|
-
jobId: job.id,
|
|
639
|
-
operations: result.operationsWithContext,
|
|
640
|
-
};
|
|
641
|
-
this.eventBus
|
|
642
|
-
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
643
|
-
.catch(() => {
|
|
644
|
-
// TODO: log error channel once logging is wired
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
748
|
this.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
749
|
+
if (scope === "document") {
|
|
750
|
+
this.documentMetaCache.invalidate(job.documentId, job.branch);
|
|
751
|
+
}
|
|
648
752
|
return {
|
|
649
753
|
job,
|
|
650
754
|
success: true,
|
|
@@ -661,6 +765,8 @@ export class SimpleJobExecutor {
|
|
|
661
765
|
return null;
|
|
662
766
|
}
|
|
663
767
|
catch (error) {
|
|
768
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
769
|
+
this.writeCache.invalidate(documentId, scope, branch);
|
|
664
770
|
return {
|
|
665
771
|
job,
|
|
666
772
|
success: false,
|
|
@@ -692,6 +798,7 @@ export class SimpleJobExecutor {
|
|
|
692
798
|
branch: job.branch,
|
|
693
799
|
documentType: documentType,
|
|
694
800
|
resultingState,
|
|
801
|
+
ordinal: 0,
|
|
695
802
|
},
|
|
696
803
|
},
|
|
697
804
|
],
|
|
@@ -717,7 +824,7 @@ export class SimpleJobExecutor {
|
|
|
717
824
|
continue;
|
|
718
825
|
}
|
|
719
826
|
if (signer.signatures.length === 0) {
|
|
720
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
827
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
721
828
|
}
|
|
722
829
|
const publicKey = signer.app.key;
|
|
723
830
|
let isValid = false;
|
|
@@ -726,10 +833,10 @@ export class SimpleJobExecutor {
|
|
|
726
833
|
}
|
|
727
834
|
catch (error) {
|
|
728
835
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
729
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
836
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
730
837
|
}
|
|
731
838
|
if (!isValid) {
|
|
732
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id
|
|
839
|
+
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
733
840
|
}
|
|
734
841
|
}
|
|
735
842
|
}
|
|
@@ -749,6 +856,7 @@ export class SimpleJobExecutor {
|
|
|
749
856
|
let isValid = false;
|
|
750
857
|
try {
|
|
751
858
|
const tempOperation = {
|
|
859
|
+
id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
|
|
752
860
|
index: 0,
|
|
753
861
|
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
754
862
|
hash: "",
|