@powerhousedao/reactor 6.0.0-dev.35 → 6.0.0-dev.37

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.
Files changed (60) hide show
  1. package/dist/src/cache/kysely-operation-index.js +1 -1
  2. package/dist/src/cache/kysely-operation-index.js.map +1 -1
  3. package/dist/src/client/reactor-client.d.ts +2 -1
  4. package/dist/src/client/reactor-client.d.ts.map +1 -1
  5. package/dist/src/client/reactor-client.js +96 -19
  6. package/dist/src/client/reactor-client.js.map +1 -1
  7. package/dist/src/client/types.d.ts +10 -0
  8. package/dist/src/client/types.d.ts.map +1 -1
  9. package/dist/src/client/types.js.map +1 -1
  10. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  11. package/dist/src/core/reactor-builder.js +1 -3
  12. package/dist/src/core/reactor-builder.js.map +1 -1
  13. package/dist/src/core/reactor.d.ts +2 -1
  14. package/dist/src/core/reactor.d.ts.map +1 -1
  15. package/dist/src/core/reactor.js +98 -4
  16. package/dist/src/core/reactor.js.map +1 -1
  17. package/dist/src/core/types.d.ts +32 -0
  18. package/dist/src/core/types.d.ts.map +1 -1
  19. package/dist/src/core/utils.d.ts +29 -0
  20. package/dist/src/core/utils.d.ts.map +1 -1
  21. package/dist/src/core/utils.js +31 -2
  22. package/dist/src/core/utils.js.map +1 -1
  23. package/dist/src/executor/document-action-handler.d.ts +37 -0
  24. package/dist/src/executor/document-action-handler.d.ts.map +1 -0
  25. package/dist/src/executor/document-action-handler.js +349 -0
  26. package/dist/src/executor/document-action-handler.js.map +1 -0
  27. package/dist/src/executor/signature-verifier.d.ts +9 -0
  28. package/dist/src/executor/signature-verifier.d.ts.map +1 -0
  29. package/dist/src/executor/signature-verifier.js +70 -0
  30. package/dist/src/executor/signature-verifier.js.map +1 -0
  31. package/dist/src/executor/simple-job-executor.d.ts +3 -39
  32. package/dist/src/executor/simple-job-executor.d.ts.map +1 -1
  33. package/dist/src/executor/simple-job-executor.js +32 -510
  34. package/dist/src/executor/simple-job-executor.js.map +1 -1
  35. package/dist/src/executor/util.d.ts +11 -1
  36. package/dist/src/executor/util.d.ts.map +1 -1
  37. package/dist/src/executor/util.js +47 -1
  38. package/dist/src/executor/util.js.map +1 -1
  39. package/dist/src/index.d.ts +1 -1
  40. package/dist/src/index.d.ts.map +1 -1
  41. package/dist/src/index.js.map +1 -1
  42. package/dist/src/sync/channels/gql-channel.d.ts +1 -1
  43. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  44. package/dist/src/sync/channels/gql-channel.js +54 -19
  45. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  46. package/dist/src/sync/channels/utils.d.ts.map +1 -1
  47. package/dist/src/sync/channels/utils.js +2 -2
  48. package/dist/src/sync/channels/utils.js.map +1 -1
  49. package/dist/src/sync/sync-manager.d.ts +6 -2
  50. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  51. package/dist/src/sync/sync-manager.js +213 -129
  52. package/dist/src/sync/sync-manager.js.map +1 -1
  53. package/dist/src/sync/sync-operation.d.ts +2 -1
  54. package/dist/src/sync/sync-operation.d.ts.map +1 -1
  55. package/dist/src/sync/sync-operation.js +3 -1
  56. package/dist/src/sync/sync-operation.js.map +1 -1
  57. package/dist/src/sync/types.d.ts +2 -0
  58. package/dist/src/sync/types.d.ts.map +1 -1
  59. package/dist/src/sync/types.js.map +1 -1
  60. package/package.json +3 -3
@@ -1,9 +1,10 @@
1
- import { deriveOperationId, isUndoRedo } from "document-model/core";
2
- import { driveCollectionId } from "../cache/operation-index-types.js";
1
+ import { isUndoRedo } from "document-model/core";
3
2
  import { ReactorEventTypes } from "../events/types.js";
4
- import { DocumentDeletedError, InvalidSignatureError, } from "../shared/errors.js";
3
+ import { DocumentDeletedError } from "../shared/errors.js";
5
4
  import { reshuffleByTimestamp } from "../utils/reshuffle.js";
6
- import { applyDeleteDocumentAction, applyUpgradeDocumentAction, createDocumentFromAction, getNextIndexForScope, } from "./util.js";
5
+ import { DocumentActionHandler } from "./document-action-handler.js";
6
+ import { SignatureVerifier } from "./signature-verifier.js";
7
+ import { buildErrorResult } from "./util.js";
7
8
  const MAX_SKIP_THRESHOLD = 1000;
8
9
  const documentScopeActions = [
9
10
  "CREATE_DOCUMENT",
@@ -24,8 +25,9 @@ export class SimpleJobExecutor {
24
25
  operationIndex;
25
26
  documentMetaCache;
26
27
  collectionMembershipCache;
27
- signatureVerifier;
28
28
  config;
29
+ signatureVerifierModule;
30
+ documentActionHandler;
29
31
  constructor(logger, registry, operationStore, eventBus, writeCache, operationIndex, documentMetaCache, collectionMembershipCache, config, signatureVerifier) {
30
32
  this.logger = logger;
31
33
  this.registry = registry;
@@ -35,7 +37,6 @@ export class SimpleJobExecutor {
35
37
  this.operationIndex = operationIndex;
36
38
  this.documentMetaCache = documentMetaCache;
37
39
  this.collectionMembershipCache = collectionMembershipCache;
38
- this.signatureVerifier = signatureVerifier;
39
40
  this.config = {
40
41
  maxSkipThreshold: config.maxSkipThreshold ?? MAX_SKIP_THRESHOLD,
41
42
  maxConcurrency: config.maxConcurrency ?? 1,
@@ -43,6 +44,8 @@ export class SimpleJobExecutor {
43
44
  retryBaseDelayMs: config.retryBaseDelayMs ?? 100,
44
45
  retryMaxDelayMs: config.retryMaxDelayMs ?? 5000,
45
46
  };
47
+ this.signatureVerifierModule = new SignatureVerifier(signatureVerifier);
48
+ this.documentActionHandler = new DocumentActionHandler(writeCache, operationStore, documentMetaCache, collectionMembershipCache, registry, logger);
46
49
  }
47
50
  /**
48
51
  * Execute a single job by applying all its actions through the appropriate reducers.
@@ -118,7 +121,7 @@ export class SimpleJobExecutor {
118
121
  const generatedOperations = [];
119
122
  const operationsWithContext = [];
120
123
  try {
121
- await this.verifyActionSignatures(job, actions);
124
+ await this.signatureVerifierModule.verifyActions(job.documentId, job.branch, actions);
122
125
  }
123
126
  catch (error) {
124
127
  return {
@@ -134,7 +137,7 @@ export class SimpleJobExecutor {
134
137
  const sourceOperation = sourceOperations?.[actionIndex];
135
138
  const isDocumentAction = documentScopeActions.includes(action.type);
136
139
  const result = isDocumentAction
137
- ? await this.executeDocumentAction(job, action, startTime, indexTxn, skip)
140
+ ? await this.documentActionHandler.execute(job, action, startTime, indexTxn, skip)
138
141
  : await this.executeRegularAction(job, action, startTime, indexTxn, skip, sourceOperation);
139
142
  const error = this.accumulateResultOrReturnError(result, generatedOperations, operationsWithContext);
140
143
  if (error !== null) {
@@ -152,381 +155,31 @@ export class SimpleJobExecutor {
152
155
  operationsWithContext,
153
156
  };
154
157
  }
155
- /**
156
- * Execute a document scope action (CREATE_DOCUMENT, DELETE_DOCUMENT, UPGRADE_DOCUMENT,
157
- * ADD_RELATIONSHIP, REMOVE_RELATIONSHIP) by dispatching to the appropriate handler.
158
- */
159
- async executeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
160
- switch (action.type) {
161
- case "CREATE_DOCUMENT":
162
- return this.executeCreateDocumentAction(job, action, startTime, indexTxn, skip);
163
- case "DELETE_DOCUMENT":
164
- return this.executeDeleteDocumentAction(job, action, startTime, indexTxn);
165
- case "UPGRADE_DOCUMENT":
166
- return this.executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip);
167
- case "ADD_RELATIONSHIP":
168
- return this.executeAddRelationshipAction(job, action, startTime, indexTxn);
169
- case "REMOVE_RELATIONSHIP":
170
- return this.executeRemoveRelationshipAction(job, action, startTime, indexTxn);
171
- default:
172
- return this.buildErrorResult(job, new Error(`Unknown document action type: ${action.type}`), startTime);
173
- }
174
- }
175
- /**
176
- * Execute a CREATE_DOCUMENT system action.
177
- * This creates a new document in storage along with its initial operation.
178
- * For a new document, the operation index is always 0.
179
- */
180
- async executeCreateDocumentAction(job, action, startTime, indexTxn, skip = 0) {
181
- if (job.scope !== "document") {
182
- return {
183
- job,
184
- success: false,
185
- error: new Error(`CREATE_DOCUMENT must be in "document" scope, got "${job.scope}"`),
186
- duration: Date.now() - startTime,
187
- };
188
- }
189
- const document = createDocumentFromAction(action);
190
- const operation = this.createOperation(action, 0, skip, {
191
- documentId: document.header.id,
192
- scope: job.scope,
193
- branch: job.branch,
194
- });
195
- // Compute resultingState for passing via context (not persisted)
196
- // Include header and all scopes present in the document state (auth, document, etc.)
197
- // but not global/local which aren't initialized by CREATE_DOCUMENT
198
- const resultingStateObj = {
199
- header: document.header,
200
- ...document.state,
201
- };
202
- const resultingState = JSON.stringify(resultingStateObj);
203
- const writeError = await this.writeOperationToStore(document.header.id, document.header.documentType, job.scope, job.branch, operation, job, startTime);
204
- if (writeError !== null) {
205
- return writeError;
206
- }
207
- this.updateDocumentRevision(document, job.scope, operation.index);
208
- this.writeCacheState(document.header.id, job.scope, job.branch, operation.index, document);
209
- indexTxn.write([
210
- {
211
- ...operation,
212
- documentId: document.header.id,
213
- documentType: document.header.documentType,
214
- branch: job.branch,
215
- scope: job.scope,
216
- },
217
- ]);
218
- // collection membership has to be _after_ the write, as it requires the
219
- // ordinal of the operation to be set
220
- if (document.header.documentType === "powerhouse/document-drive") {
221
- const collectionId = driveCollectionId(job.branch, document.header.id);
222
- indexTxn.createCollection(collectionId);
223
- indexTxn.addToCollection(collectionId, document.header.id);
224
- }
225
- this.documentMetaCache.putDocumentMeta(document.header.id, job.branch, {
226
- state: document.state.document,
227
- documentType: document.header.documentType,
228
- documentScopeRevision: 1,
229
- });
230
- return this.buildSuccessResult(job, operation, document.header.id, document.header.documentType, resultingState, startTime);
231
- }
232
- /**
233
- * Execute a DELETE_DOCUMENT system action.
234
- * This deletes a document from legacy storage and writes the operation to IOperationStore.
235
- * The operation index is determined from the document's current operation count.
236
- */
237
- async executeDeleteDocumentAction(job, action, startTime, indexTxn) {
238
- const input = action.input;
239
- if (!input.documentId) {
240
- return this.buildErrorResult(job, new Error("DELETE_DOCUMENT action requires a documentId in input"), startTime);
241
- }
242
- const documentId = input.documentId;
243
- let document;
244
- try {
245
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
246
- }
247
- catch (error) {
248
- return this.buildErrorResult(job, new Error(`Failed to fetch document before deletion: ${error instanceof Error ? error.message : String(error)}`), startTime);
249
- }
250
- // Check if document is already deleted
251
- const documentState = document.state.document;
252
- if (documentState.isDeleted) {
253
- return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
254
- }
255
- const nextIndex = getNextIndexForScope(document, job.scope);
256
- const operation = this.createOperation(action, nextIndex, 0, {
257
- documentId,
258
- scope: job.scope,
259
- branch: job.branch,
260
- });
261
- // Mark the document as deleted in the state for read model indexing
262
- applyDeleteDocumentAction(document, action);
263
- // Compute resultingState for passing via context (not persisted)
264
- // DELETE_DOCUMENT only affects header and document scopes
265
- const resultingStateObj = {
266
- header: document.header,
267
- document: document.state.document,
268
- };
269
- const resultingState = JSON.stringify(resultingStateObj);
270
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
271
- if (writeError !== null) {
272
- return writeError;
273
- }
274
- indexTxn.write([
275
- {
276
- ...operation,
277
- documentId: documentId,
278
- documentType: document.header.documentType,
279
- branch: job.branch,
280
- scope: job.scope,
281
- },
282
- ]);
283
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
284
- state: document.state.document,
285
- documentType: document.header.documentType,
286
- documentScopeRevision: operation.index + 1,
287
- });
288
- return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
289
- }
290
- /**
291
- * Execute an UPGRADE_DOCUMENT system action.
292
- * Handles initial upgrades (version 0 to N), same-version no-ops, and multi-step upgrade chains.
293
- * The operation index is determined from the document's current operation count.
294
- */
295
- async executeUpgradeDocumentAction(job, action, startTime, indexTxn, skip = 0) {
296
- const input = action.input;
297
- if (!input.documentId) {
298
- return this.buildErrorResult(job, new Error("UPGRADE_DOCUMENT action requires a documentId in input"), startTime);
299
- }
300
- const documentId = input.documentId;
301
- const fromVersion = input.fromVersion;
302
- const toVersion = input.toVersion;
303
- let document;
304
- try {
305
- document = await this.writeCache.getState(documentId, job.scope, job.branch);
306
- }
307
- catch (error) {
308
- return this.buildErrorResult(job, new Error(`Failed to fetch document for upgrade: ${error instanceof Error ? error.message : String(error)}`), startTime);
309
- }
310
- const documentState = document.state.document;
311
- if (documentState.isDeleted) {
312
- return this.buildErrorResult(job, new DocumentDeletedError(documentId, documentState.deletedAtUtcIso), startTime);
313
- }
314
- const nextIndex = getNextIndexForScope(document, job.scope);
315
- let upgradePath;
316
- if (fromVersion > 0 && fromVersion < toVersion) {
317
- try {
318
- upgradePath = this.registry.computeUpgradePath(document.header.documentType, fromVersion, toVersion);
319
- }
320
- catch (error) {
321
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
322
- }
323
- }
324
- if (fromVersion === toVersion && fromVersion > 0) {
325
- return {
326
- job,
327
- success: true,
328
- operations: [],
329
- operationsWithContext: [],
330
- duration: Date.now() - startTime,
331
- };
332
- }
333
- try {
334
- document = applyUpgradeDocumentAction(document, action, upgradePath);
335
- }
336
- catch (error) {
337
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
338
- }
339
- const operation = this.createOperation(action, nextIndex, skip, {
340
- documentId,
341
- scope: job.scope,
342
- branch: job.branch,
343
- });
344
- // Compute resultingState for passing via context (not persisted)
345
- const resultingStateObj = {
346
- header: document.header,
347
- ...document.state,
348
- };
349
- const resultingState = JSON.stringify(resultingStateObj);
350
- const writeError = await this.writeOperationToStore(documentId, document.header.documentType, job.scope, job.branch, operation, job, startTime);
351
- if (writeError !== null) {
352
- return writeError;
353
- }
354
- this.updateDocumentRevision(document, job.scope, operation.index);
355
- this.writeCacheState(documentId, job.scope, job.branch, operation.index, document);
356
- indexTxn.write([
357
- {
358
- ...operation,
359
- documentId: documentId,
360
- documentType: document.header.documentType,
361
- branch: job.branch,
362
- scope: job.scope,
363
- },
364
- ]);
365
- this.documentMetaCache.putDocumentMeta(documentId, job.branch, {
366
- state: document.state.document,
367
- documentType: document.header.documentType,
368
- documentScopeRevision: operation.index + 1,
369
- });
370
- return this.buildSuccessResult(job, operation, documentId, document.header.documentType, resultingState, startTime);
371
- }
372
- async executeAddRelationshipAction(job, action, startTime, indexTxn) {
373
- if (job.scope !== "document") {
374
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
375
- }
376
- const input = action.input;
377
- if (!input.sourceId || !input.targetId || !input.relationshipType) {
378
- return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
379
- }
380
- if (input.sourceId === input.targetId) {
381
- return this.buildErrorResult(job, new Error("ADD_RELATIONSHIP: sourceId and targetId cannot be the same (self-relationships not allowed)"), startTime);
382
- }
383
- let sourceDoc;
384
- try {
385
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
386
- }
387
- catch (error) {
388
- return this.buildErrorResult(job, new Error(`ADD_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
389
- }
390
- const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
391
- const operation = this.createOperation(action, nextIndex, 0, {
392
- documentId: input.sourceId,
393
- scope: job.scope,
394
- branch: job.branch,
395
- });
396
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
397
- if (writeError !== null) {
398
- return writeError;
399
- }
400
- sourceDoc.header.lastModifiedAtUtcIso =
401
- operation.timestampUtcMs || new Date().toISOString();
402
- this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
403
- sourceDoc.operations = {
404
- ...sourceDoc.operations,
405
- [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
406
- };
407
- const scopeState = sourceDoc.state[job.scope];
408
- const resultingStateObj = {
409
- header: structuredClone(sourceDoc.header),
410
- [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
411
- };
412
- const resultingState = JSON.stringify(resultingStateObj);
413
- this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
414
- indexTxn.write([
415
- {
416
- ...operation,
417
- documentId: input.sourceId,
418
- documentType: sourceDoc.header.documentType,
419
- branch: job.branch,
420
- scope: job.scope,
421
- },
422
- ]);
423
- // collection membership has to be _after_ the write, as it requires the
424
- // ordinal of the operation to be set
425
- if (sourceDoc.header.documentType === "powerhouse/document-drive") {
426
- const collectionId = driveCollectionId(job.branch, input.sourceId);
427
- indexTxn.addToCollection(collectionId, input.targetId);
428
- this.collectionMembershipCache.invalidate(input.targetId);
429
- }
430
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
431
- state: sourceDoc.state.document,
432
- documentType: sourceDoc.header.documentType,
433
- documentScopeRevision: operation.index + 1,
434
- });
435
- return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
436
- }
437
- async executeRemoveRelationshipAction(job, action, startTime, indexTxn) {
438
- if (job.scope !== "document") {
439
- return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP must be in "document" scope, got "${job.scope}"`), startTime);
440
- }
441
- const input = action.input;
442
- if (!input.sourceId || !input.targetId || !input.relationshipType) {
443
- return this.buildErrorResult(job, new Error("REMOVE_RELATIONSHIP action requires sourceId, targetId, and relationshipType in input"), startTime);
444
- }
445
- let sourceDoc;
446
- try {
447
- sourceDoc = await this.writeCache.getState(input.sourceId, "document", job.branch);
448
- }
449
- catch (error) {
450
- return this.buildErrorResult(job, new Error(`REMOVE_RELATIONSHIP: source document ${input.sourceId} not found: ${error instanceof Error ? error.message : String(error)}`), startTime);
451
- }
452
- const nextIndex = getNextIndexForScope(sourceDoc, job.scope);
453
- const operation = this.createOperation(action, nextIndex, 0, {
454
- documentId: input.sourceId,
455
- scope: job.scope,
456
- branch: job.branch,
457
- });
458
- const writeError = await this.writeOperationToStore(input.sourceId, sourceDoc.header.documentType, job.scope, job.branch, operation, job, startTime);
459
- if (writeError !== null) {
460
- return writeError;
461
- }
462
- sourceDoc.header.lastModifiedAtUtcIso =
463
- operation.timestampUtcMs || new Date().toISOString();
464
- this.updateDocumentRevision(sourceDoc, job.scope, operation.index);
465
- sourceDoc.operations = {
466
- ...sourceDoc.operations,
467
- [job.scope]: [...(sourceDoc.operations[job.scope] ?? []), operation],
468
- };
469
- const scopeState = sourceDoc.state[job.scope];
470
- const resultingStateObj = {
471
- header: structuredClone(sourceDoc.header),
472
- [job.scope]: scopeState === undefined ? {} : structuredClone(scopeState),
473
- };
474
- const resultingState = JSON.stringify(resultingStateObj);
475
- this.writeCacheState(input.sourceId, job.scope, job.branch, operation.index, sourceDoc);
476
- indexTxn.write([
477
- {
478
- ...operation,
479
- documentId: input.sourceId,
480
- documentType: sourceDoc.header.documentType,
481
- branch: job.branch,
482
- scope: job.scope,
483
- },
484
- ]);
485
- // collection membership has to be _after_ the write, as it requires the
486
- // ordinal of the operation to be set
487
- if (sourceDoc.header.documentType === "powerhouse/document-drive") {
488
- const collectionId = driveCollectionId(job.branch, input.sourceId);
489
- indexTxn.removeFromCollection(collectionId, input.targetId);
490
- this.collectionMembershipCache.invalidate(input.targetId);
491
- }
492
- this.documentMetaCache.putDocumentMeta(input.sourceId, job.branch, {
493
- state: sourceDoc.state.document,
494
- documentType: sourceDoc.header.documentType,
495
- documentScopeRevision: operation.index + 1,
496
- });
497
- return this.buildSuccessResult(job, operation, input.sourceId, sourceDoc.header.documentType, resultingState, startTime);
498
- }
499
- /**
500
- * Execute a regular document action by applying it through the document model reducer.
501
- * If sourceOperation is provided (for load jobs), its id and timestamp are preserved.
502
- */
503
158
  async executeRegularAction(job, action, startTime, indexTxn, skip = 0, sourceOperation) {
504
159
  let docMeta;
505
160
  try {
506
161
  docMeta = await this.documentMetaCache.getDocumentMeta(job.documentId, job.branch);
507
162
  }
508
163
  catch (error) {
509
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
164
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
510
165
  }
511
166
  if (docMeta.state.isDeleted) {
512
- return this.buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
167
+ return buildErrorResult(job, new DocumentDeletedError(job.documentId, docMeta.state.deletedAtUtcIso), startTime);
513
168
  }
514
169
  let document;
515
170
  try {
516
171
  document = await this.writeCache.getState(job.documentId, job.scope, job.branch);
517
172
  }
518
173
  catch (error) {
519
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
174
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
520
175
  }
521
176
  let module;
522
177
  try {
523
- // Use document version to get the correct module
524
- // Version 0 means not yet upgraded - use latest version
525
178
  const moduleVersion = docMeta.state.version === 0 ? undefined : docMeta.state.version;
526
179
  module = this.registry.getModule(document.header.documentType, moduleVersion);
527
180
  }
528
181
  catch (error) {
529
- return this.buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
182
+ return buildErrorResult(job, error instanceof Error ? error : new Error(String(error)), startTime);
530
183
  }
531
184
  let updatedDocument;
532
185
  try {
@@ -547,12 +200,12 @@ export class SimpleJobExecutor {
547
200
  if (error instanceof Error && error.stack) {
548
201
  enhancedError.stack = `${contextMessage}\n\nOriginal stack trace:\n${error.stack}`;
549
202
  }
550
- return this.buildErrorResult(job, enhancedError, startTime);
203
+ return buildErrorResult(job, enhancedError, startTime);
551
204
  }
552
205
  const scope = job.scope;
553
206
  const operations = updatedDocument.operations[scope];
554
207
  if (operations.length === 0) {
555
- return this.buildErrorResult(job, new Error("No operation generated from action"), startTime);
208
+ return buildErrorResult(job, new Error("No operation generated from action"), startTime);
556
209
  }
557
210
  const newOperation = operations[operations.length - 1];
558
211
  if (!isUndoRedo(action)) {
@@ -562,9 +215,20 @@ export class SimpleJobExecutor {
562
215
  ...updatedDocument.state,
563
216
  header: updatedDocument.header,
564
217
  });
565
- const writeFailResult = await this.writeOperationToStore(job.documentId, document.header.documentType, scope, job.branch, newOperation, job, startTime);
566
- if (writeFailResult !== null) {
567
- return writeFailResult;
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
+ };
568
232
  }
569
233
  updatedDocument.header.revision = {
570
234
  ...updatedDocument.header.revision,
@@ -600,20 +264,9 @@ export class SimpleJobExecutor {
600
264
  duration: Date.now() - startTime,
601
265
  };
602
266
  }
603
- createOperation(action, index, skip = 0, context) {
604
- const id = deriveOperationId(context.documentId, context.scope, context.branch, action.id);
605
- return {
606
- id,
607
- index: index,
608
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
609
- hash: "",
610
- skip: skip,
611
- action: action,
612
- };
613
- }
614
267
  async executeLoadJob(job, startTime, indexTxn) {
615
268
  if (job.operations.length === 0) {
616
- return this.buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
269
+ return buildErrorResult(job, new Error("Load job must include at least one operation"), startTime);
617
270
  }
618
271
  const scope = job.scope;
619
272
  let latestRevision = 0;
@@ -650,10 +303,6 @@ export class SimpleJobExecutor {
650
303
  catch {
651
304
  conflictingOps = [];
652
305
  }
653
- // To properly detect superseded operations, we need to look at ALL operations
654
- // from the minimum conflicting index onwards, not just the conflicting ones.
655
- // An operation with an earlier timestamp (not in conflictingOps) might have
656
- // a skip value that supersedes operations in conflictingOps.
657
306
  let allOpsFromMinConflictingIndex = conflictingOps;
658
307
  if (conflictingOps.length > 0) {
659
308
  const minConflictingIndex = Math.min(...conflictingOps.map((op) => op.index));
@@ -665,11 +314,6 @@ export class SimpleJobExecutor {
665
314
  allOpsFromMinConflictingIndex = conflictingOps;
666
315
  }
667
316
  }
668
- // Filter out operations that have been superseded by later operations with skip values.
669
- // An operation at index N is superseded if there exists an operation at index M > N
670
- // where (M - skip_M) <= N, meaning the later operation's logical index covers N.
671
- // We check against ALL operations (not just conflicting) to catch superseding ops
672
- // that have earlier timestamps.
673
317
  const nonSupersededOps = conflictingOps.filter((op) => {
674
318
  for (const laterOp of allOpsFromMinConflictingIndex) {
675
319
  if (laterOp.index > op.index && laterOp.skip > 0) {
@@ -681,9 +325,7 @@ export class SimpleJobExecutor {
681
325
  }
682
326
  return true;
683
327
  });
684
- // All non-superseded conflicting operations need to be reshuffled
685
328
  const existingOpsToReshuffle = nonSupersededOps;
686
- // Skip count is the number of existing operations that need to be rewound
687
329
  const skipCount = existingOpsToReshuffle.length;
688
330
  if (skipCount > this.config.maxSkipThreshold) {
689
331
  return {
@@ -694,8 +336,6 @@ export class SimpleJobExecutor {
694
336
  duration: Date.now() - startTime,
695
337
  };
696
338
  }
697
- // Filter out incoming operations that are duplicates (action already exists locally
698
- // or appears multiple times in incoming)
699
339
  const existingActionIds = new Set(nonSupersededOps.map((op) => op.action.id));
700
340
  const seenIncomingActionIds = new Set();
701
341
  const incomingOpsToApply = job.operations.filter((op) => {
@@ -713,7 +353,6 @@ export class SimpleJobExecutor {
713
353
  ...operation,
714
354
  id: operation.id,
715
355
  })));
716
- // For v2, all NOOPs have skip=1 - consecutive NOOPs are handled during state rebuild
717
356
  for (const operation of reshuffledOperations) {
718
357
  if (operation.action.type === "NOOP") {
719
358
  operation.skip = 1;
@@ -742,123 +381,6 @@ export class SimpleJobExecutor {
742
381
  duration: Date.now() - startTime,
743
382
  };
744
383
  }
745
- async writeOperationToStore(documentId, documentType, scope, branch, operation, job, startTime) {
746
- try {
747
- await this.operationStore.apply(documentId, documentType, scope, branch, operation.index, (txn) => {
748
- txn.addOperations(operation);
749
- });
750
- return null;
751
- }
752
- catch (error) {
753
- this.logger.error("Error writing @Operation to IOperationStore: @Error", operation, error);
754
- this.writeCache.invalidate(documentId, scope, branch);
755
- return {
756
- job,
757
- success: false,
758
- error: new Error(`Failed to write operation to IOperationStore: ${error instanceof Error ? error.message : String(error)}`),
759
- duration: Date.now() - startTime,
760
- };
761
- }
762
- }
763
- updateDocumentRevision(document, scope, operationIndex) {
764
- document.header.revision = {
765
- ...document.header.revision,
766
- [scope]: operationIndex + 1,
767
- };
768
- }
769
- writeCacheState(documentId, scope, branch, operationIndex, document) {
770
- this.writeCache.putState(documentId, scope, branch, operationIndex, document);
771
- }
772
- buildSuccessResult(job, operation, documentId, documentType, resultingState, startTime) {
773
- return {
774
- job,
775
- success: true,
776
- operations: [operation],
777
- operationsWithContext: [
778
- {
779
- operation,
780
- context: {
781
- documentId: documentId,
782
- scope: job.scope,
783
- branch: job.branch,
784
- documentType: documentType,
785
- resultingState,
786
- ordinal: 0,
787
- },
788
- },
789
- ],
790
- duration: Date.now() - startTime,
791
- };
792
- }
793
- buildErrorResult(job, error, startTime) {
794
- return {
795
- job,
796
- success: false,
797
- error: error,
798
- duration: Date.now() - startTime,
799
- };
800
- }
801
- async verifyOperationSignatures(job, operations) {
802
- if (!this.signatureVerifier) {
803
- return;
804
- }
805
- for (let i = 0; i < operations.length; i++) {
806
- const operation = operations[i];
807
- const signer = operation.action.context?.signer;
808
- if (!signer) {
809
- continue;
810
- }
811
- if (signer.signatures.length === 0) {
812
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} has signer but no signatures`);
813
- }
814
- const publicKey = signer.app.key;
815
- let isValid = false;
816
- try {
817
- isValid = await this.signatureVerifier(operation, publicKey);
818
- }
819
- catch (error) {
820
- const errorMessage = error instanceof Error ? error.message : String(error);
821
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} verification failed: ${errorMessage}`);
822
- }
823
- if (!isValid) {
824
- throw new InvalidSignatureError(job.documentId, `Operation ${operation.id} at index ${operation.index} signature verification returned false`);
825
- }
826
- }
827
- }
828
- async verifyActionSignatures(job, actions) {
829
- if (!this.signatureVerifier) {
830
- return;
831
- }
832
- for (const action of actions) {
833
- const signer = action.context?.signer;
834
- if (!signer) {
835
- continue;
836
- }
837
- if (signer.signatures.length === 0) {
838
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} has signer but no signatures`);
839
- }
840
- const publicKey = signer.app.key;
841
- let isValid = false;
842
- try {
843
- const tempOperation = {
844
- id: deriveOperationId(job.documentId, action.scope, job.branch, action.id),
845
- index: 0,
846
- timestampUtcMs: action.timestampUtcMs || new Date().toISOString(),
847
- hash: "",
848
- skip: 0,
849
- action: action,
850
- };
851
- isValid = await this.signatureVerifier(tempOperation, publicKey);
852
- }
853
- catch (error) {
854
- const errorMessage = error instanceof Error ? error.message : String(error);
855
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} verification failed: ${errorMessage}`);
856
- }
857
- if (!isValid) {
858
- throw new InvalidSignatureError(job.documentId, `Action ${action.id} signature verification returned false`);
859
- }
860
- }
861
- }
862
384
  accumulateResultOrReturnError(result, generatedOperations, operationsWithContext) {
863
385
  if (!result.success) {
864
386
  return result;