@powerhousedao/reactor 6.0.0-dev.4 → 6.0.0-dev.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cache/collection-membership-cache.d.ts +13 -0
- package/dist/src/cache/collection-membership-cache.d.ts.map +1 -0
- package/dist/src/cache/collection-membership-cache.js +33 -0
- package/dist/src/cache/collection-membership-cache.js.map +1 -0
- package/dist/src/cache/document-meta-cache.d.ts.map +1 -1
- package/dist/src/cache/document-meta-cache.js +4 -4
- package/dist/src/cache/document-meta-cache.js.map +1 -1
- package/dist/src/cache/kysely-operation-index.d.ts +5 -1
- package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
- package/dist/src/cache/kysely-operation-index.js +96 -6
- 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 +11 -11
- package/dist/src/cache/kysely-write-cache.js.map +1 -1
- package/dist/src/cache/operation-index-types.d.ts +13 -1
- 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 +13 -10
- package/dist/src/client/reactor-client.d.ts.map +1 -1
- package/dist/src/client/reactor-client.js +134 -43
- package/dist/src/client/reactor-client.js.map +1 -1
- package/dist/src/client/types.d.ts +25 -6
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/client/types.js.map +1 -1
- package/dist/src/core/reactor-builder.d.ts +11 -7
- package/dist/src/core/reactor-builder.d.ts.map +1 -1
- package/dist/src/core/reactor-builder.js +61 -22
- package/dist/src/core/reactor-builder.js.map +1 -1
- package/dist/src/core/reactor-client-builder.d.ts +5 -4
- package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
- package/dist/src/core/reactor-client-builder.js +14 -5
- package/dist/src/core/reactor-client-builder.js.map +1 -1
- package/dist/src/core/reactor.d.ts +20 -80
- package/dist/src/core/reactor.d.ts.map +1 -1
- package/dist/src/core/reactor.js +235 -576
- package/dist/src/core/reactor.js.map +1 -1
- package/dist/src/core/types.d.ts +63 -28
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/utils.d.ts +37 -2
- package/dist/src/core/utils.d.ts.map +1 -1
- package/dist/src/core/utils.js +57 -8
- package/dist/src/core/utils.js.map +1 -1
- package/dist/src/events/types.d.ts +35 -10
- package/dist/src/events/types.d.ts.map +1 -1
- package/dist/src/events/types.js +7 -5
- package/dist/src/events/types.js.map +1 -1
- package/dist/src/executor/document-action-handler.d.ts +37 -0
- package/dist/src/executor/document-action-handler.d.ts.map +1 -0
- package/dist/src/executor/document-action-handler.js +349 -0
- package/dist/src/executor/document-action-handler.js.map +1 -0
- package/dist/src/executor/signature-verifier.d.ts +9 -0
- package/dist/src/executor/signature-verifier.d.ts.map +1 -0
- package/dist/src/executor/signature-verifier.js +70 -0
- package/dist/src/executor/signature-verifier.js.map +1 -0
- package/dist/src/executor/simple-job-executor-manager.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor-manager.js +20 -10
- package/dist/src/executor/simple-job-executor-manager.js.map +1 -1
- package/dist/src/executor/simple-job-executor.d.ts +6 -46
- package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
- package/dist/src/executor/simple-job-executor.js +64 -565
- package/dist/src/executor/simple-job-executor.js.map +1 -1
- package/dist/src/executor/types.d.ts +0 -2
- 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 +11 -1
- package/dist/src/executor/util.d.ts.map +1 -1
- package/dist/src/executor/util.js +47 -1
- package/dist/src/executor/util.js.map +1 -1
- package/dist/src/index.d.ts +10 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/job-tracker/in-memory-job-tracker.d.ts +4 -3
- package/dist/src/job-tracker/in-memory-job-tracker.d.ts.map +1 -1
- package/dist/src/job-tracker/in-memory-job-tracker.js +20 -18
- package/dist/src/job-tracker/in-memory-job-tracker.js.map +1 -1
- package/dist/src/job-tracker/interfaces.d.ts +3 -1
- package/dist/src/job-tracker/interfaces.d.ts.map +1 -1
- package/dist/src/logging/console.d.ts +1 -22
- package/dist/src/logging/console.d.ts.map +1 -1
- package/dist/src/logging/console.js +1 -107
- package/dist/src/logging/console.js.map +1 -1
- package/dist/src/logging/types.d.ts +1 -11
- package/dist/src/logging/types.d.ts.map +1 -1
- package/dist/src/processors/index.d.ts.map +1 -1
- package/dist/src/processors/relational/relational-db-processor.d.ts +47 -0
- package/dist/src/processors/relational/relational-db-processor.d.ts.map +1 -0
- package/dist/src/processors/relational/relational-db-processor.js +45 -0
- package/dist/src/processors/relational/relational-db-processor.js.map +1 -0
- package/dist/src/processors/relational/types.d.ts +27 -0
- package/dist/src/processors/relational/types.d.ts.map +1 -0
- package/dist/src/processors/relational/types.js +2 -0
- package/dist/src/processors/relational/types.js.map +1 -0
- package/dist/src/processors/relational/utils.d.ts +29 -0
- package/dist/src/processors/relational/utils.d.ts.map +1 -0
- package/dist/src/processors/relational/utils.js +67 -0
- package/dist/src/processors/relational/utils.js.map +1 -0
- package/dist/src/processors/types.d.ts +10 -2
- package/dist/src/processors/types.d.ts.map +1 -1
- package/dist/src/processors/utils.d.ts.map +1 -1
- package/dist/src/processors/utils.js +2 -1
- package/dist/src/processors/utils.js.map +1 -1
- package/dist/src/queue/queue.d.ts +25 -0
- package/dist/src/queue/queue.d.ts.map +1 -1
- package/dist/src/queue/queue.js +56 -0
- package/dist/src/queue/queue.js.map +1 -1
- package/dist/src/queue/types.d.ts +3 -3
- package/dist/src/queue/types.d.ts.map +1 -1
- package/dist/src/read-models/base-read-model.js +4 -4
- package/dist/src/read-models/base-read-model.js.map +1 -1
- package/dist/src/read-models/coordinator.d.ts +2 -2
- package/dist/src/read-models/coordinator.d.ts.map +1 -1
- package/dist/src/read-models/coordinator.js +8 -8
- package/dist/src/read-models/coordinator.js.map +1 -1
- package/dist/src/read-models/document-view.d.ts +5 -2
- package/dist/src/read-models/document-view.d.ts.map +1 -1
- package/dist/src/read-models/document-view.js +130 -48
- package/dist/src/read-models/document-view.js.map +1 -1
- package/dist/src/shared/awaiter.d.ts +2 -2
- package/dist/src/shared/awaiter.d.ts.map +1 -1
- package/dist/src/shared/awaiter.js +11 -11
- package/dist/src/shared/awaiter.js.map +1 -1
- package/dist/src/shared/collect-all-pages.d.ts +7 -0
- package/dist/src/shared/collect-all-pages.d.ts.map +1 -0
- package/dist/src/shared/collect-all-pages.js +17 -0
- package/dist/src/shared/collect-all-pages.js.map +1 -0
- package/dist/src/shared/drive-url.d.ts +15 -0
- package/dist/src/shared/drive-url.d.ts.map +1 -0
- package/dist/src/shared/drive-url.js +17 -0
- package/dist/src/shared/drive-url.js.map +1 -0
- package/dist/src/shared/factories.d.ts +6 -2
- package/dist/src/shared/factories.d.ts.map +1 -1
- package/dist/src/shared/factories.js +10 -2
- package/dist/src/shared/factories.js.map +1 -1
- package/dist/src/shared/types.d.ts +32 -6
- package/dist/src/shared/types.d.ts.map +1 -1
- package/dist/src/shared/types.js +4 -4
- package/dist/src/shared/types.js.map +1 -1
- package/dist/src/signer/passthrough-signer.d.ts +1 -1
- package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
- package/dist/src/signer/passthrough-signer.js +1 -3
- package/dist/src/signer/passthrough-signer.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +52 -94
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/interfaces.js.map +1 -1
- package/dist/src/storage/kysely/document-indexer.d.ts +6 -6
- package/dist/src/storage/kysely/document-indexer.d.ts.map +1 -1
- package/dist/src/storage/kysely/document-indexer.js +123 -52
- package/dist/src/storage/kysely/document-indexer.js.map +1 -1
- package/dist/src/storage/kysely/store.d.ts +4 -3
- package/dist/src/storage/kysely/store.d.ts.map +1 -1
- package/dist/src/storage/kysely/store.js +52 -21
- package/dist/src/storage/kysely/store.js.map +1 -1
- package/dist/src/storage/kysely/sync-remote-storage.js +1 -1
- package/dist/src/storage/kysely/sync-remote-storage.js.map +1 -1
- package/dist/src/subs/subscription-notification-read-model.d.ts +1 -1
- package/dist/src/subs/subscription-notification-read-model.js +1 -1
- package/dist/src/sync/buffered-mailbox.d.ts +30 -0
- package/dist/src/sync/buffered-mailbox.d.ts.map +1 -0
- package/dist/src/sync/buffered-mailbox.js +136 -0
- package/dist/src/sync/buffered-mailbox.js.map +1 -0
- package/dist/src/sync/channels/composite-channel-factory.d.ts +9 -3
- package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -1
- package/dist/src/sync/channels/composite-channel-factory.js +16 -12
- package/dist/src/sync/channels/composite-channel-factory.js.map +1 -1
- package/dist/src/sync/channels/gql-channel-factory.d.ts +9 -3
- package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel-factory.js +14 -10
- package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
- package/dist/src/sync/channels/gql-channel.d.ts +22 -18
- package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
- package/dist/src/sync/channels/gql-channel.js +128 -64
- package/dist/src/sync/channels/gql-channel.js.map +1 -1
- package/dist/src/sync/channels/index.d.ts +2 -0
- package/dist/src/sync/channels/index.d.ts.map +1 -1
- package/dist/src/sync/channels/index.js +2 -0
- package/dist/src/sync/channels/index.js.map +1 -1
- package/dist/src/sync/channels/interval-poll-timer.d.ts +24 -0
- package/dist/src/sync/channels/interval-poll-timer.d.ts.map +1 -0
- package/dist/src/sync/channels/interval-poll-timer.js +69 -0
- package/dist/src/sync/channels/interval-poll-timer.js.map +1 -0
- package/dist/src/sync/channels/poll-timer.d.ts +14 -0
- package/dist/src/sync/channels/poll-timer.d.ts.map +1 -0
- package/dist/src/sync/channels/poll-timer.js +2 -0
- package/dist/src/sync/channels/poll-timer.js.map +1 -0
- package/dist/src/sync/channels/utils.d.ts.map +1 -1
- package/dist/src/sync/channels/utils.js +2 -2
- package/dist/src/sync/channels/utils.js.map +1 -1
- package/dist/src/sync/index.d.ts +6 -4
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +4 -3
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/interfaces.d.ts +18 -6
- package/dist/src/sync/interfaces.d.ts.map +1 -1
- package/dist/src/sync/mailbox.d.ts +21 -3
- package/dist/src/sync/mailbox.d.ts.map +1 -1
- package/dist/src/sync/mailbox.js +55 -2
- package/dist/src/sync/mailbox.js.map +1 -1
- package/dist/src/sync/sync-awaiter.d.ts +34 -0
- package/dist/src/sync/sync-awaiter.d.ts.map +1 -0
- package/dist/src/sync/sync-awaiter.js +124 -0
- package/dist/src/sync/sync-awaiter.js.map +1 -0
- package/dist/src/sync/sync-manager.d.ts +19 -5
- package/dist/src/sync/sync-manager.d.ts.map +1 -1
- package/dist/src/sync/sync-manager.js +318 -33
- package/dist/src/sync/sync-manager.js.map +1 -1
- package/dist/src/sync/sync-operation.d.ts +3 -1
- package/dist/src/sync/sync-operation.d.ts.map +1 -1
- package/dist/src/sync/sync-operation.js +5 -1
- package/dist/src/sync/sync-operation.js.map +1 -1
- package/dist/src/sync/types.d.ts +73 -1
- package/dist/src/sync/types.d.ts.map +1 -1
- package/dist/src/sync/types.js +10 -0
- package/dist/src/sync/types.js.map +1 -1
- package/dist/src/sync/utils.d.ts +11 -0
- package/dist/src/sync/utils.d.ts.map +1 -1
- package/dist/src/sync/utils.js +17 -0
- package/dist/src/sync/utils.js.map +1 -1
- package/dist/src/utils/reshuffle.d.ts +15 -5
- package/dist/src/utils/reshuffle.d.ts.map +1 -1
- package/dist/src/utils/reshuffle.js +29 -6
- package/dist/src/utils/reshuffle.js.map +1 -1
- package/package.json +11 -13
- package/dist/src/storage/consistency-aware-legacy-storage.d.ts +0 -33
- package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +0 -1
- package/dist/src/storage/consistency-aware-legacy-storage.js +0 -65
- package/dist/src/storage/consistency-aware-legacy-storage.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
|
|
1
|
+
import { isUndoRedo } from "document-model/core";
|
|
2
|
+
import { ReactorEventTypes } from "../events/types.js";
|
|
3
|
+
import { DocumentDeletedError } from "../shared/errors.js";
|
|
5
4
|
import { reshuffleByTimestamp } from "../utils/reshuffle.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { DocumentActionHandler } from "./document-action-handler.js";
|
|
6
|
+
import { SignatureVerifier } from "./signature-verifier.js";
|
|
7
|
+
import { buildErrorResult } from "./util.js";
|
|
8
|
+
const MAX_SKIP_THRESHOLD = 1000;
|
|
8
9
|
const documentScopeActions = [
|
|
9
10
|
"CREATE_DOCUMENT",
|
|
10
11
|
"DELETE_DOCUMENT",
|
|
@@ -14,42 +15,37 @@ const documentScopeActions = [
|
|
|
14
15
|
];
|
|
15
16
|
/**
|
|
16
17
|
* Simple job executor that processes a job by applying actions through document model reducers.
|
|
17
|
-
*
|
|
18
|
-
* @see docs/planning/Storage/IOperationStore.md for storage schema
|
|
19
|
-
* @see docs/planning/Operations/index.md for operation structure
|
|
20
|
-
* @see docs/planning/Jobs/reshuffle.md for skip mechanism details
|
|
21
18
|
*/
|
|
22
19
|
export class SimpleJobExecutor {
|
|
23
20
|
logger;
|
|
24
21
|
registry;
|
|
25
|
-
documentStorage;
|
|
26
|
-
operationStorage;
|
|
27
22
|
operationStore;
|
|
28
23
|
eventBus;
|
|
29
24
|
writeCache;
|
|
30
25
|
operationIndex;
|
|
31
26
|
documentMetaCache;
|
|
32
|
-
|
|
27
|
+
collectionMembershipCache;
|
|
33
28
|
config;
|
|
34
|
-
|
|
29
|
+
signatureVerifierModule;
|
|
30
|
+
documentActionHandler;
|
|
31
|
+
constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier) {
|
|
35
32
|
this.logger = logger;
|
|
36
33
|
this.registry = registry;
|
|
37
|
-
this.documentStorage = documentStorage;
|
|
38
|
-
this.operationStorage = operationStorage;
|
|
39
34
|
this.operationStore = operationStore;
|
|
40
35
|
this.eventBus = eventBus;
|
|
41
36
|
this.writeCache = writeCache;
|
|
42
37
|
this.operationIndex = operationIndex;
|
|
43
38
|
this.documentMetaCache = documentMetaCache;
|
|
44
|
-
this.
|
|
39
|
+
this.collectionMembershipCache = collectionMembershipCache;
|
|
45
40
|
this.config = {
|
|
46
41
|
maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
|
|
47
42
|
maxConcurrency: config.maxConcurrency ?? 1,
|
|
48
43
|
jobTimeoutMs: config.jobTimeoutMs ?? 30000,
|
|
49
44
|
retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
|
|
50
45
|
retryMaxDelayMs: config.retryMaxDelayMs ?? 5000,
|
|
51
|
-
legacyStorageEnabled: config.legacyStorageEnabled ?? true,
|
|
52
46
|
};
|
|
47
|
+
this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
|
|
48
|
+
this.documentActionHandler = new DocumentActionHandler(writeCache, operationStore, documentMetaCache, collectionMembershipCache, registry, logger);
|
|
53
49
|
}
|
|
54
50
|
/**
|
|
55
51
|
* Execute a single job by applying all its actions through the appropriate reducers.
|
|
@@ -66,13 +62,15 @@ export class SimpleJobExecutor {
|
|
|
66
62
|
result.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
67
63
|
}
|
|
68
64
|
if (result.operationsWithContext.length > 0) {
|
|
65
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(result.operationsWithContext);
|
|
69
66
|
const event = {
|
|
70
67
|
jobId: job.id,
|
|
71
68
|
operations: result.operationsWithContext,
|
|
72
69
|
jobMeta: job.meta,
|
|
70
|
+
collectionMemberships,
|
|
73
71
|
};
|
|
74
72
|
this.eventBus
|
|
75
|
-
.emit(
|
|
73
|
+
.emit(ReactorEventTypes.JOB_WRITE_READY, event)
|
|
76
74
|
.catch(() => {
|
|
77
75
|
// TODO: Log error
|
|
78
76
|
});
|
|
@@ -94,14 +92,14 @@ export class SimpleJobExecutor {
|
|
|
94
92
|
for (let i = 0; i < result.operationsWithContext.length; i++) {
|
|
95
93
|
result.operationsWithContext[i].context.ordinal = ordinals[i];
|
|
96
94
|
}
|
|
95
|
+
const collectionMemberships = await this.getCollectionMembershipsForOperations(result.operationsWithContext);
|
|
97
96
|
const event = {
|
|
98
97
|
jobId: job.id,
|
|
99
98
|
operations: result.operationsWithContext,
|
|
100
99
|
jobMeta: job.meta,
|
|
100
|
+
collectionMemberships,
|
|
101
101
|
};
|
|
102
|
-
this.eventBus
|
|
103
|
-
.emit(OperationEventTypes.OPERATION_WRITTEN, event)
|
|
104
|
-
.catch(() => {
|
|
102
|
+
this.eventBus.emit(ReactorEventTypes.JOB_WRITE_READY, event).catch(() => {
|
|
105
103
|
// TODO: Log error
|
|
106
104
|
});
|
|
107
105
|
}
|
|
@@ -113,11 +111,17 @@ export class SimpleJobExecutor {
|
|
|
113
111
|
duration: Date.now() - startTime,
|
|
114
112
|
};
|
|
115
113
|
}
|
|
114
|
+
async getCollectionMembershipsForOperations(operations) {
|
|
115
|
+
const documentIds = [
|
|
116
|
+
...new Set(operations.map((op) => op.context.documentId)),
|
|
117
|
+
];
|
|
118
|
+
return this.collectionMembershipCache.getCollectionsForDocuments(documentIds);
|
|
119
|
+
}
|
|
116
120
|
async processActions(job, actions, startTime, indexTxn, skipValues, sourceOperations) {
|
|
117
121
|
const generatedOperations = [];
|
|
118
122
|
const operationsWithContext = [];
|
|
119
123
|
try {
|
|
120
|
-
await this.
|
|
124
|
+
await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
|
|
121
125
|
}
|
|
122
126
|
catch (error) {
|
|
123
127
|
return {
|
|
@@ -133,7 +137,7 @@ export class SimpleJobExecutor {
|
|
|
133
137
|
const sourceOperation = sourceOperations?.[actionIndex];
|
|
134
138
|
const isDocumentAction = documentScopeActions.includes(action.type);
|
|
135
139
|
const result = isDocumentAction
|
|
136
|
-
? await this.
|
|
140
|
+
? await this.documentActionHandler.execute(job, action, startTime, indexTxn, skip)
|
|
137
141
|
: await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation);
|
|
138
142
|
const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
|
|
139
143
|
if (error !== null) {
|
|
@@ -151,414 +155,31 @@ export class SimpleJobExecutor {
|
|
|
151
155
|
operationsWithContext,
|
|
152
156
|
};
|
|
153
157
|
}
|
|
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
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Execute a CREATE_DOCUMENT system action.
|
|
176
|
-
* This creates a new document in storage along with its initial operation.
|
|
177
|
-
* For a new document, the operation index is always 0.
|
|
178
|
-
*/
|
|
179
|
-
async executeCreateDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
180
|
-
if (job.scope !== "document") {
|
|
181
|
-
return {
|
|
182
|
-
job,
|
|
183
|
-
success: false,
|
|
184
|
-
error: new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
|
|
185
|
-
duration: Date.now() - startTime,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
const document = createDocumentFromAction(action);
|
|
189
|
-
// Legacy: Store the document in storage
|
|
190
|
-
if (this.config.legacyStorageEnabled) {
|
|
191
|
-
try {
|
|
192
|
-
await this.documentStorage.create(document);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
return this.buildErrorResult(job, new Error(`Failed to create document in storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const operation = this.createOperation(action, 0, skip, {
|
|
199
|
-
documentId: document.header.id,
|
|
200
|
-
scope: job.scope,
|
|
201
|
-
branch: job.branch,
|
|
202
|
-
});
|
|
203
|
-
// Legacy: Write the CREATE_DOCUMENT operation to legacy storage
|
|
204
|
-
if (this.config.legacyStorageEnabled) {
|
|
205
|
-
try {
|
|
206
|
-
await this.operationStorage.addDocumentOperations(document.header.id, [operation], document);
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
return this.buildErrorResult(job, new Error(`Failed to write CREATE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// Compute resultingState for passing via context (not persisted)
|
|
213
|
-
// Include header and all scopes present in the document state (auth, document, etc.)
|
|
214
|
-
// but not global/local which aren't initialized by CREATE_DOCUMENT
|
|
215
|
-
const resultingStateObj = {
|
|
216
|
-
header: document.header,
|
|
217
|
-
...document.state,
|
|
218
|
-
};
|
|
219
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
220
|
-
const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
221
|
-
if (writeError !== null) {
|
|
222
|
-
return writeError;
|
|
223
|
-
}
|
|
224
|
-
this.updateDocumentRevision(document, job.scope, operation.index);
|
|
225
|
-
this.writeCacheState(document.header.id, job.scope, job.branch, operation.index, document);
|
|
226
|
-
indexTxn.write([
|
|
227
|
-
{
|
|
228
|
-
...operation,
|
|
229
|
-
documentId: document.header.id,
|
|
230
|
-
documentType: document.header.documentType,
|
|
231
|
-
branch: job.branch,
|
|
232
|
-
scope: job.scope,
|
|
233
|
-
},
|
|
234
|
-
]);
|
|
235
|
-
// collection membership has to be _after_ the write, as it requires the
|
|
236
|
-
// ordinal of the operation to be set
|
|
237
|
-
if (document.header.documentType === "powerhouse/document-drive") {
|
|
238
|
-
const collectionId = driveCollectionId(job.branch, document.header.id);
|
|
239
|
-
indexTxn.createCollection(collectionId);
|
|
240
|
-
indexTxn.addToCollection(collectionId, document.header.id);
|
|
241
|
-
}
|
|
242
|
-
this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
|
|
243
|
-
state: document.state.document,
|
|
244
|
-
documentType: document.header.documentType,
|
|
245
|
-
documentScopeRevision: 1,
|
|
246
|
-
});
|
|
247
|
-
return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Execute a DELETE_DOCUMENT system action.
|
|
251
|
-
* This deletes a document from legacy storage and writes the operation to IOperationStore.
|
|
252
|
-
* The operation index is determined from the document's current operation count.
|
|
253
|
-
*/
|
|
254
|
-
async executeDeleteDocumentAction(job, action, startTime, indexTxn) {
|
|
255
|
-
const input = action.input;
|
|
256
|
-
if (!input.documentId) {
|
|
257
|
-
return this.buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
|
|
258
|
-
}
|
|
259
|
-
const documentId = input.documentId;
|
|
260
|
-
let document;
|
|
261
|
-
try {
|
|
262
|
-
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
return this.buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
266
|
-
}
|
|
267
|
-
// Check if document is already deleted
|
|
268
|
-
const documentState = document.state.document;
|
|
269
|
-
if (documentState.isDeleted) {
|
|
270
|
-
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
271
|
-
}
|
|
272
|
-
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
273
|
-
const operation = this.createOperation(action, nextIndex, 0, {
|
|
274
|
-
documentId,
|
|
275
|
-
scope: job.scope,
|
|
276
|
-
branch: job.branch,
|
|
277
|
-
});
|
|
278
|
-
if (this.config.legacyStorageEnabled) {
|
|
279
|
-
try {
|
|
280
|
-
await this.documentStorage.delete(documentId);
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
return this.buildErrorResult(job, new Error(`Failed to delete document from legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// Mark the document as deleted in the state for read model indexing
|
|
287
|
-
applyDeleteDocumentAction(document, action);
|
|
288
|
-
// Compute resultingState for passing via context (not persisted)
|
|
289
|
-
// DELETE_DOCUMENT only affects header and document scopes
|
|
290
|
-
const resultingStateObj = {
|
|
291
|
-
header: document.header,
|
|
292
|
-
document: document.state.document,
|
|
293
|
-
};
|
|
294
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
295
|
-
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
296
|
-
if (writeError !== null) {
|
|
297
|
-
return writeError;
|
|
298
|
-
}
|
|
299
|
-
indexTxn.write([
|
|
300
|
-
{
|
|
301
|
-
...operation,
|
|
302
|
-
documentId: documentId,
|
|
303
|
-
documentType: document.header.documentType,
|
|
304
|
-
branch: job.branch,
|
|
305
|
-
scope: job.scope,
|
|
306
|
-
},
|
|
307
|
-
]);
|
|
308
|
-
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
309
|
-
state: document.state.document,
|
|
310
|
-
documentType: document.header.documentType,
|
|
311
|
-
documentScopeRevision: operation.index + 1,
|
|
312
|
-
});
|
|
313
|
-
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Execute an UPGRADE_DOCUMENT system action.
|
|
317
|
-
* Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
|
|
318
|
-
* The operation index is determined from the document's current operation count.
|
|
319
|
-
*/
|
|
320
|
-
async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
|
|
321
|
-
const input = action.input;
|
|
322
|
-
if (!input.documentId) {
|
|
323
|
-
return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
|
|
324
|
-
}
|
|
325
|
-
const documentId = input.documentId;
|
|
326
|
-
const fromVersion = input.fromVersion;
|
|
327
|
-
const toVersion = input.toVersion;
|
|
328
|
-
let document;
|
|
329
|
-
try {
|
|
330
|
-
document = await this.writeCache.getState(documentId, job.scope, job.branch);
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
return this.buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
334
|
-
}
|
|
335
|
-
const documentState = document.state.document;
|
|
336
|
-
if (documentState.isDeleted) {
|
|
337
|
-
return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
|
|
338
|
-
}
|
|
339
|
-
const nextIndex = getNextIndexForScope(document, job.scope);
|
|
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
|
-
});
|
|
369
|
-
// Write the updated document to legacy storage
|
|
370
|
-
if (this.config.legacyStorageEnabled) {
|
|
371
|
-
try {
|
|
372
|
-
await this.operationStorage.addDocumentOperations(documentId, [operation], document);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
return this.buildErrorResult(job, new Error(`Failed to write UPGRADE_DOCUMENT operation to legacy storage: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
// Compute resultingState for passing via context (not persisted)
|
|
379
|
-
const resultingStateObj = {
|
|
380
|
-
header: document.header,
|
|
381
|
-
...document.state,
|
|
382
|
-
};
|
|
383
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
384
|
-
const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
385
|
-
if (writeError !== null) {
|
|
386
|
-
return writeError;
|
|
387
|
-
}
|
|
388
|
-
this.updateDocumentRevision(document, job.scope, operation.index);
|
|
389
|
-
this.writeCacheState(documentId, job.scope, job.branch, operation.index, document);
|
|
390
|
-
indexTxn.write([
|
|
391
|
-
{
|
|
392
|
-
...operation,
|
|
393
|
-
documentId: documentId,
|
|
394
|
-
documentType: document.header.documentType,
|
|
395
|
-
branch: job.branch,
|
|
396
|
-
scope: job.scope,
|
|
397
|
-
},
|
|
398
|
-
]);
|
|
399
|
-
this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
|
|
400
|
-
state: document.state.document,
|
|
401
|
-
documentType: document.header.documentType,
|
|
402
|
-
documentScopeRevision: operation.index + 1,
|
|
403
|
-
});
|
|
404
|
-
return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
|
|
405
|
-
}
|
|
406
|
-
async executeAddRelationshipAction(job, action, startTime, indexTxn) {
|
|
407
|
-
if (job.scope !== "document") {
|
|
408
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
|
|
409
|
-
}
|
|
410
|
-
const input = action.input;
|
|
411
|
-
if (!input.sourceId || !input.targetId || !input.relationshipType) {
|
|
412
|
-
return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
|
|
413
|
-
}
|
|
414
|
-
if (input.sourceId === input.targetId) {
|
|
415
|
-
return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)"), startTime);
|
|
416
|
-
}
|
|
417
|
-
let sourceDoc;
|
|
418
|
-
try {
|
|
419
|
-
sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
423
|
-
}
|
|
424
|
-
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
425
|
-
const operation = this.createOperation(action, nextIndex, 0, {
|
|
426
|
-
documentId: input.sourceId,
|
|
427
|
-
scope: job.scope,
|
|
428
|
-
branch: job.branch,
|
|
429
|
-
});
|
|
430
|
-
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
431
|
-
if (writeError !== null) {
|
|
432
|
-
return writeError;
|
|
433
|
-
}
|
|
434
|
-
sourceDoc.header.lastModifiedAtUtcIso =
|
|
435
|
-
operation.timestampUtcMs || new Date().toISOString();
|
|
436
|
-
this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
|
|
437
|
-
sourceDoc.operations = {
|
|
438
|
-
...sourceDoc.operations,
|
|
439
|
-
[job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
|
|
440
|
-
};
|
|
441
|
-
const scopeState = sourceDoc.state[job.scope];
|
|
442
|
-
const resultingStateObj = {
|
|
443
|
-
header: structuredClone(sourceDoc.header),
|
|
444
|
-
[job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
|
|
445
|
-
};
|
|
446
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
447
|
-
this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
448
|
-
indexTxn.write([
|
|
449
|
-
{
|
|
450
|
-
...operation,
|
|
451
|
-
documentId: input.sourceId,
|
|
452
|
-
documentType: sourceDoc.header.documentType,
|
|
453
|
-
branch: job.branch,
|
|
454
|
-
scope: job.scope,
|
|
455
|
-
},
|
|
456
|
-
]);
|
|
457
|
-
// collection membership has to be _after_ the write, as it requires the
|
|
458
|
-
// ordinal of the operation to be set
|
|
459
|
-
if (sourceDoc.header.documentType === "powerhouse/document-drive") {
|
|
460
|
-
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
461
|
-
indexTxn.addToCollection(collectionId, input.targetId);
|
|
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
|
-
});
|
|
468
|
-
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
469
|
-
}
|
|
470
|
-
async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
|
|
471
|
-
if (job.scope !== "document") {
|
|
472
|
-
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
|
|
473
|
-
}
|
|
474
|
-
const input = action.input;
|
|
475
|
-
if (!input.sourceId || !input.targetId || !input.relationshipType) {
|
|
476
|
-
return this.buildErrorResult(job, new Error("REMOVE_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
|
|
477
|
-
}
|
|
478
|
-
let sourceDoc;
|
|
479
|
-
try {
|
|
480
|
-
sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
|
|
481
|
-
}
|
|
482
|
-
catch (error) {
|
|
483
|
-
return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
|
|
484
|
-
}
|
|
485
|
-
const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
|
|
486
|
-
const operation = this.createOperation(action, nextIndex, 0, {
|
|
487
|
-
documentId: input.sourceId,
|
|
488
|
-
scope: job.scope,
|
|
489
|
-
branch: job.branch,
|
|
490
|
-
});
|
|
491
|
-
const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
|
|
492
|
-
if (writeError !== null) {
|
|
493
|
-
return writeError;
|
|
494
|
-
}
|
|
495
|
-
sourceDoc.header.lastModifiedAtUtcIso =
|
|
496
|
-
operation.timestampUtcMs || new Date().toISOString();
|
|
497
|
-
this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
|
|
498
|
-
sourceDoc.operations = {
|
|
499
|
-
...sourceDoc.operations,
|
|
500
|
-
[job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
|
|
501
|
-
};
|
|
502
|
-
const scopeState = sourceDoc.state[job.scope];
|
|
503
|
-
const resultingStateObj = {
|
|
504
|
-
header: structuredClone(sourceDoc.header),
|
|
505
|
-
[job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
|
|
506
|
-
};
|
|
507
|
-
const resultingState = JSON.stringify(resultingStateObj);
|
|
508
|
-
this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
|
|
509
|
-
indexTxn.write([
|
|
510
|
-
{
|
|
511
|
-
...operation,
|
|
512
|
-
documentId: input.sourceId,
|
|
513
|
-
documentType: sourceDoc.header.documentType,
|
|
514
|
-
branch: job.branch,
|
|
515
|
-
scope: job.scope,
|
|
516
|
-
},
|
|
517
|
-
]);
|
|
518
|
-
// collection membership has to be _after_ the write, as it requires the
|
|
519
|
-
// ordinal of the operation to be set
|
|
520
|
-
if (sourceDoc.header.documentType === "powerhouse/document-drive") {
|
|
521
|
-
const collectionId = driveCollectionId(job.branch, input.sourceId);
|
|
522
|
-
indexTxn.removeFromCollection(collectionId, input.targetId);
|
|
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
|
-
});
|
|
529
|
-
return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
|
|
530
|
-
}
|
|
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
158
|
async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
|
|
536
159
|
let docMeta;
|
|
537
160
|
try {
|
|
538
161
|
docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
|
|
539
162
|
}
|
|
540
163
|
catch (error) {
|
|
541
|
-
return
|
|
164
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
542
165
|
}
|
|
543
166
|
if (docMeta.state.isDeleted) {
|
|
544
|
-
return
|
|
167
|
+
return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
|
|
545
168
|
}
|
|
546
169
|
let document;
|
|
547
170
|
try {
|
|
548
171
|
document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
|
|
549
172
|
}
|
|
550
173
|
catch (error) {
|
|
551
|
-
return
|
|
174
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
552
175
|
}
|
|
553
176
|
let module;
|
|
554
177
|
try {
|
|
555
|
-
// Use document version to get the correct module
|
|
556
|
-
// Version 0 means not yet upgraded - use latest version
|
|
557
178
|
const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
|
|
558
179
|
module = this.registry.getModule(document.header.documentType, moduleVersion);
|
|
559
180
|
}
|
|
560
181
|
catch (error) {
|
|
561
|
-
return
|
|
182
|
+
return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
562
183
|
}
|
|
563
184
|
let updatedDocument;
|
|
564
185
|
try {
|
|
@@ -579,32 +200,35 @@ export class SimpleJobExecutor {
|
|
|
579
200
|
if (error instanceof Error && error.stack) {
|
|
580
201
|
enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
|
|
581
202
|
}
|
|
582
|
-
return
|
|
203
|
+
return buildErrorResult(job, enhancedError, startTime);
|
|
583
204
|
}
|
|
584
205
|
const scope = job.scope;
|
|
585
206
|
const operations = updatedDocument.operations[scope];
|
|
586
207
|
if (operations.length === 0) {
|
|
587
|
-
return
|
|
208
|
+
return buildErrorResult(job, new Error("No operation generated from action"), startTime);
|
|
588
209
|
}
|
|
589
210
|
const newOperation = operations[operations.length - 1];
|
|
590
211
|
if (!isUndoRedo(action)) {
|
|
591
212
|
newOperation.skip = skip;
|
|
592
213
|
}
|
|
593
|
-
if (this.config.legacyStorageEnabled) {
|
|
594
|
-
try {
|
|
595
|
-
await this.operationStorage.addDocumentOperations(job.documentId, [newOperation], updatedDocument);
|
|
596
|
-
}
|
|
597
|
-
catch (error) {
|
|
598
|
-
return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
214
|
const resultingState = JSON.stringify({
|
|
602
215
|
...updatedDocument.state,
|
|
603
216
|
header: updatedDocument.header,
|
|
604
217
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
218
|
+
try {
|
|
219
|
+
await this.operationStore.apply(job.documentId, document.header.documentType, scope, job.branch, newOperation.index, (txn) => {
|
|
220
|
+
txn.addOperations(newOperation);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.logger.error("Error writing @Operation to IOperationStore: @Error", newOperation, error);
|
|
225
|
+
this.writeCache.invalidate(job.documentId, scope, job.branch);
|
|
226
|
+
return {
|
|
227
|
+
job,
|
|
228
|
+
success: false,
|
|
229
|
+
error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
230
|
+
duration: Date.now() - startTime,
|
|
231
|
+
};
|
|
608
232
|
}
|
|
609
233
|
updatedDocument.header.revision = {
|
|
610
234
|
...updatedDocument.header.revision,
|
|
@@ -640,20 +264,9 @@ export class SimpleJobExecutor {
|
|
|
640
264
|
duration: Date.now() - startTime,
|
|
641
265
|
};
|
|
642
266
|
}
|
|
643
|
-
createOperation(action, index, skip = 0, context) {
|
|
644
|
-
const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
|
|
645
|
-
return {
|
|
646
|
-
id,
|
|
647
|
-
index: index,
|
|
648
|
-
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
649
|
-
hash: "",
|
|
650
|
-
skip: skip,
|
|
651
|
-
action: action,
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
267
|
async executeLoadJob(job, startTime, indexTxn) {
|
|
655
268
|
if (job.operations.length === 0) {
|
|
656
|
-
return
|
|
269
|
+
return buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
|
|
657
270
|
}
|
|
658
271
|
const scope = job.scope;
|
|
659
272
|
let latestRevision = 0;
|
|
@@ -675,8 +288,8 @@ export class SimpleJobExecutor {
|
|
|
675
288
|
}
|
|
676
289
|
let conflictingOps = [];
|
|
677
290
|
try {
|
|
678
|
-
const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { limit: this.config.maxSkipThreshold + 1 });
|
|
679
|
-
if (conflictingResult.
|
|
291
|
+
const conflictingResult = await this.operationStore.getConflicting(job.documentId, scope, job.branch, minIncomingTimestamp, { cursor: "0", limit: this.config.maxSkipThreshold + 1 });
|
|
292
|
+
if (conflictingResult.nextCursor !== undefined) {
|
|
680
293
|
return {
|
|
681
294
|
job,
|
|
682
295
|
success: false,
|
|
@@ -685,16 +298,24 @@ export class SimpleJobExecutor {
|
|
|
685
298
|
duration: Date.now() - startTime,
|
|
686
299
|
};
|
|
687
300
|
}
|
|
688
|
-
conflictingOps = conflictingResult.
|
|
301
|
+
conflictingOps = conflictingResult.results;
|
|
689
302
|
}
|
|
690
303
|
catch {
|
|
691
304
|
conflictingOps = [];
|
|
692
305
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
306
|
+
let allOpsFromMinConflictingIndex = conflictingOps;
|
|
307
|
+
if (conflictingOps.length > 0) {
|
|
308
|
+
const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
|
|
309
|
+
try {
|
|
310
|
+
const allOpsResult = await this.operationStore.getSince(job.documentId, scope, job.branch, minConflictingIndex - 1, undefined, { cursor: "0", limit: this.config.maxSkipThreshold * 2 });
|
|
311
|
+
allOpsFromMinConflictingIndex = allOpsResult.results;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
allOpsFromMinConflictingIndex = conflictingOps;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
696
317
|
const nonSupersededOps = conflictingOps.filter((op) => {
|
|
697
|
-
for (const laterOp of
|
|
318
|
+
for (const laterOp of allOpsFromMinConflictingIndex) {
|
|
698
319
|
if (laterOp.index > op.index && laterOp.skip > 0) {
|
|
699
320
|
const logicalIndex = laterOp.index - laterOp.skip;
|
|
700
321
|
if (logicalIndex <= op.index) {
|
|
@@ -704,9 +325,7 @@ export class SimpleJobExecutor {
|
|
|
704
325
|
}
|
|
705
326
|
return true;
|
|
706
327
|
});
|
|
707
|
-
// All non-superseded conflicting operations need to be reshuffled
|
|
708
328
|
const existingOpsToReshuffle = nonSupersededOps;
|
|
709
|
-
// Skip count is the number of existing operations that need to be rewound
|
|
710
329
|
const skipCount = existingOpsToReshuffle.length;
|
|
711
330
|
if (skipCount > this.config.maxSkipThreshold) {
|
|
712
331
|
return {
|
|
@@ -717,8 +336,6 @@ export class SimpleJobExecutor {
|
|
|
717
336
|
duration: Date.now() - startTime,
|
|
718
337
|
};
|
|
719
338
|
}
|
|
720
|
-
// Filter out incoming operations that are duplicates (action already exists locally
|
|
721
|
-
// or appears multiple times in incoming)
|
|
722
339
|
const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
|
|
723
340
|
const seenIncomingActionIds = new Set();
|
|
724
341
|
const incomingOpsToApply = job.operations.filter((op) => {
|
|
@@ -736,7 +353,6 @@ export class SimpleJobExecutor {
|
|
|
736
353
|
...operation,
|
|
737
354
|
id: operation.id,
|
|
738
355
|
})));
|
|
739
|
-
// For v2, all NOOPs have skip=1 - consecutive NOOPs are handled during state rebuild
|
|
740
356
|
for (const operation of reshuffledOperations) {
|
|
741
357
|
if (operation.action.type === "NOOP") {
|
|
742
358
|
operation.skip = 1;
|
|
@@ -765,123 +381,6 @@ export class SimpleJobExecutor {
|
|
|
765
381
|
duration: Date.now() - startTime,
|
|
766
382
|
};
|
|
767
383
|
}
|
|
768
|
-
async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
|
|
769
|
-
try {
|
|
770
|
-
await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
|
|
771
|
-
txn.addOperations(operation);
|
|
772
|
-
});
|
|
773
|
-
return null;
|
|
774
|
-
}
|
|
775
|
-
catch (error) {
|
|
776
|
-
this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
|
|
777
|
-
this.writeCache.invalidate(documentId, scope, branch);
|
|
778
|
-
return {
|
|
779
|
-
job,
|
|
780
|
-
success: false,
|
|
781
|
-
error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
|
|
782
|
-
duration: Date.now() - startTime,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
updateDocumentRevision(document, scope, operationIndex) {
|
|
787
|
-
document.header.revision = {
|
|
788
|
-
...document.header.revision,
|
|
789
|
-
[scope]: operationIndex + 1,
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
writeCacheState(documentId, scope, branch, operationIndex, document) {
|
|
793
|
-
this.writeCache.putState(documentId, scope, branch, operationIndex, document);
|
|
794
|
-
}
|
|
795
|
-
buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
|
|
796
|
-
return {
|
|
797
|
-
job,
|
|
798
|
-
success: true,
|
|
799
|
-
operations: [operation],
|
|
800
|
-
operationsWithContext: [
|
|
801
|
-
{
|
|
802
|
-
operation,
|
|
803
|
-
context: {
|
|
804
|
-
documentId: documentId,
|
|
805
|
-
scope: job.scope,
|
|
806
|
-
branch: job.branch,
|
|
807
|
-
documentType: documentType,
|
|
808
|
-
resultingState,
|
|
809
|
-
ordinal: 0,
|
|
810
|
-
},
|
|
811
|
-
},
|
|
812
|
-
],
|
|
813
|
-
duration: Date.now() - startTime,
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
buildErrorResult(job, error, startTime) {
|
|
817
|
-
return {
|
|
818
|
-
job,
|
|
819
|
-
success: false,
|
|
820
|
-
error: error,
|
|
821
|
-
duration: Date.now() - startTime,
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
async verifyOperationSignatures(job, operations) {
|
|
825
|
-
if (!this.signatureVerifier) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
for (let i = 0; i < operations.length; i++) {
|
|
829
|
-
const operation = operations[i];
|
|
830
|
-
const signer = operation.action.context?.signer;
|
|
831
|
-
if (!signer) {
|
|
832
|
-
continue;
|
|
833
|
-
}
|
|
834
|
-
if (signer.signatures.length === 0) {
|
|
835
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
|
|
836
|
-
}
|
|
837
|
-
const publicKey = signer.app.key;
|
|
838
|
-
let isValid = false;
|
|
839
|
-
try {
|
|
840
|
-
isValid = await this.signatureVerifier(operation, publicKey);
|
|
841
|
-
}
|
|
842
|
-
catch (error) {
|
|
843
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
844
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
|
|
845
|
-
}
|
|
846
|
-
if (!isValid) {
|
|
847
|
-
throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
async verifyActionSignatures(job, actions) {
|
|
852
|
-
if (!this.signatureVerifier) {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
for (const action of actions) {
|
|
856
|
-
const signer = action.context?.signer;
|
|
857
|
-
if (!signer) {
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
if (signer.signatures.length === 0) {
|
|
861
|
-
throw new InvalidSignatureError(job.documentId, `Action ${action.id} has signer but no signatures`);
|
|
862
|
-
}
|
|
863
|
-
const publicKey = signer.app.key;
|
|
864
|
-
let isValid = false;
|
|
865
|
-
try {
|
|
866
|
-
const tempOperation = {
|
|
867
|
-
id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
|
|
868
|
-
index: 0,
|
|
869
|
-
timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
|
|
870
|
-
hash: "",
|
|
871
|
-
skip: 0,
|
|
872
|
-
action: action,
|
|
873
|
-
};
|
|
874
|
-
isValid = await this.signatureVerifier(tempOperation, publicKey);
|
|
875
|
-
}
|
|
876
|
-
catch (error) {
|
|
877
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
878
|
-
throw new InvalidSignatureError(job.documentId, `Action ${action.id} verification failed: ${errorMessage}`);
|
|
879
|
-
}
|
|
880
|
-
if (!isValid) {
|
|
881
|
-
throw new InvalidSignatureError(job.documentId, `Action ${action.id} signature verification returned false`);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
384
|
accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
|
|
886
385
|
if (!result.success) {
|
|
887
386
|
return result;
|