@powerhousedao/reactor 6.0.0-dev.26 → 6.0.0-dev.28

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 (110) hide show
  1. package/dist/src/cache/document-meta-cache.js +3 -3
  2. package/dist/src/cache/document-meta-cache.js.map +1 -1
  3. package/dist/src/cache/kysely-operation-index.d.ts +2 -1
  4. package/dist/src/cache/kysely-operation-index.d.ts.map +1 -1
  5. package/dist/src/cache/kysely-operation-index.js +16 -4
  6. package/dist/src/cache/kysely-operation-index.js.map +1 -1
  7. package/dist/src/cache/kysely-write-cache.js +8 -8
  8. package/dist/src/cache/kysely-write-cache.js.map +1 -1
  9. package/dist/src/cache/operation-index-types.d.ts +2 -1
  10. package/dist/src/cache/operation-index-types.d.ts.map +1 -1
  11. package/dist/src/cache/operation-index-types.js.map +1 -1
  12. package/dist/src/client/reactor-client.d.ts +4 -6
  13. package/dist/src/client/reactor-client.d.ts.map +1 -1
  14. package/dist/src/client/reactor-client.js +20 -25
  15. package/dist/src/client/reactor-client.js.map +1 -1
  16. package/dist/src/client/types.d.ts +1 -4
  17. package/dist/src/client/types.d.ts.map +1 -1
  18. package/dist/src/core/reactor-builder.d.ts +3 -0
  19. package/dist/src/core/reactor-builder.d.ts.map +1 -1
  20. package/dist/src/core/reactor-builder.js +50 -6
  21. package/dist/src/core/reactor-builder.js.map +1 -1
  22. package/dist/src/core/reactor-client-builder.d.ts +3 -2
  23. package/dist/src/core/reactor-client-builder.d.ts.map +1 -1
  24. package/dist/src/core/reactor-client-builder.js +13 -4
  25. package/dist/src/core/reactor-client-builder.js.map +1 -1
  26. package/dist/src/core/reactor.d.ts +11 -75
  27. package/dist/src/core/reactor.d.ts.map +1 -1
  28. package/dist/src/core/reactor.js +87 -556
  29. package/dist/src/core/reactor.js.map +1 -1
  30. package/dist/src/core/types.d.ts +28 -19
  31. package/dist/src/core/types.d.ts.map +1 -1
  32. package/dist/src/core/utils.d.ts +6 -1
  33. package/dist/src/core/utils.d.ts.map +1 -1
  34. package/dist/src/core/utils.js +22 -6
  35. package/dist/src/core/utils.js.map +1 -1
  36. package/dist/src/executor/simple-job-executor.js +5 -5
  37. package/dist/src/executor/simple-job-executor.js.map +1 -1
  38. package/dist/src/index.d.ts +3 -4
  39. package/dist/src/index.d.ts.map +1 -1
  40. package/dist/src/index.js +2 -3
  41. package/dist/src/index.js.map +1 -1
  42. package/dist/src/read-models/base-read-model.js +4 -4
  43. package/dist/src/read-models/base-read-model.js.map +1 -1
  44. package/dist/src/read-models/document-view.d.ts +5 -2
  45. package/dist/src/read-models/document-view.d.ts.map +1 -1
  46. package/dist/src/read-models/document-view.js +129 -28
  47. package/dist/src/read-models/document-view.js.map +1 -1
  48. package/dist/src/shared/collect-all-pages.d.ts +7 -0
  49. package/dist/src/shared/collect-all-pages.d.ts.map +1 -0
  50. package/dist/src/shared/collect-all-pages.js +17 -0
  51. package/dist/src/shared/collect-all-pages.js.map +1 -0
  52. package/dist/src/shared/drive-url.d.ts +1 -1
  53. package/dist/src/shared/drive-url.js +2 -2
  54. package/dist/src/shared/drive-url.js.map +1 -1
  55. package/dist/src/shared/factories.d.ts +6 -2
  56. package/dist/src/shared/factories.d.ts.map +1 -1
  57. package/dist/src/shared/factories.js +10 -2
  58. package/dist/src/shared/factories.js.map +1 -1
  59. package/dist/src/shared/types.d.ts +8 -0
  60. package/dist/src/shared/types.d.ts.map +1 -1
  61. package/dist/src/shared/types.js.map +1 -1
  62. package/dist/src/signer/passthrough-signer.d.ts +1 -1
  63. package/dist/src/signer/passthrough-signer.d.ts.map +1 -1
  64. package/dist/src/signer/passthrough-signer.js +1 -3
  65. package/dist/src/signer/passthrough-signer.js.map +1 -1
  66. package/dist/src/storage/interfaces.d.ts +38 -94
  67. package/dist/src/storage/interfaces.d.ts.map +1 -1
  68. package/dist/src/storage/interfaces.js.map +1 -1
  69. package/dist/src/storage/kysely/document-indexer.d.ts +6 -6
  70. package/dist/src/storage/kysely/document-indexer.d.ts.map +1 -1
  71. package/dist/src/storage/kysely/document-indexer.js +123 -52
  72. package/dist/src/storage/kysely/document-indexer.js.map +1 -1
  73. package/dist/src/storage/kysely/store.d.ts +2 -1
  74. package/dist/src/storage/kysely/store.d.ts.map +1 -1
  75. package/dist/src/storage/kysely/store.js +33 -15
  76. package/dist/src/storage/kysely/store.js.map +1 -1
  77. package/dist/src/sync/channels/composite-channel-factory.d.ts.map +1 -1
  78. package/dist/src/sync/channels/composite-channel-factory.js +5 -2
  79. package/dist/src/sync/channels/composite-channel-factory.js.map +1 -1
  80. package/dist/src/sync/channels/gql-channel-factory.d.ts.map +1 -1
  81. package/dist/src/sync/channels/gql-channel-factory.js +5 -2
  82. package/dist/src/sync/channels/gql-channel-factory.js.map +1 -1
  83. package/dist/src/sync/channels/gql-channel.d.ts +4 -9
  84. package/dist/src/sync/channels/gql-channel.d.ts.map +1 -1
  85. package/dist/src/sync/channels/gql-channel.js +11 -31
  86. package/dist/src/sync/channels/gql-channel.js.map +1 -1
  87. package/dist/src/sync/channels/index.d.ts +2 -0
  88. package/dist/src/sync/channels/index.d.ts.map +1 -1
  89. package/dist/src/sync/channels/index.js +2 -0
  90. package/dist/src/sync/channels/index.js.map +1 -1
  91. package/dist/src/sync/channels/interval-poll-timer.d.ts +18 -0
  92. package/dist/src/sync/channels/interval-poll-timer.d.ts.map +1 -0
  93. package/dist/src/sync/channels/interval-poll-timer.js +43 -0
  94. package/dist/src/sync/channels/interval-poll-timer.js.map +1 -0
  95. package/dist/src/sync/channels/poll-timer.d.ts +14 -0
  96. package/dist/src/sync/channels/poll-timer.d.ts.map +1 -0
  97. package/dist/src/sync/channels/poll-timer.js +2 -0
  98. package/dist/src/sync/channels/poll-timer.js.map +1 -0
  99. package/dist/src/sync/index.d.ts +1 -1
  100. package/dist/src/sync/index.d.ts.map +1 -1
  101. package/dist/src/sync/index.js +1 -1
  102. package/dist/src/sync/index.js.map +1 -1
  103. package/dist/src/sync/sync-manager.d.ts.map +1 -1
  104. package/dist/src/sync/sync-manager.js +3 -2
  105. package/dist/src/sync/sync-manager.js.map +1 -1
  106. package/package.json +3 -3
  107. package/dist/src/storage/consistency-aware-legacy-storage.d.ts +0 -33
  108. package/dist/src/storage/consistency-aware-legacy-storage.d.ts.map +0 -1
  109. package/dist/src/storage/consistency-aware-legacy-storage.js +0 -65
  110. package/dist/src/storage/consistency-aware-legacy-storage.js.map +0 -1
@@ -5,7 +5,7 @@ import { ReactorEventTypes } from "../events/types.js";
5
5
  import { createMutableShutdownStatus } from "../shared/factories.js";
6
6
  import { JobStatus } from "../shared/types.js";
7
7
  import { matchesScope } from "../shared/utils.js";
8
- import { filterByType, getSharedScope, signAction, signActions, toErrorInfo, topologicalSort, validateActionScopes, validateBatchRequest, } from "./utils.js";
8
+ import { filterByType, getSharedActionScope, getSharedOperationScope, signAction, signActions, toErrorInfo, topologicalSort, validateActionScopes, validateBatchRequest, } from "./utils.js";
9
9
  /**
10
10
  * This class implements the IReactor interface and serves as the main entry point
11
11
  * for the new Reactor architecture.
@@ -13,67 +13,62 @@ import { filterByType, getSharedScope, signAction, signActions, toErrorInfo, top
13
13
  export class Reactor {
14
14
  logger;
15
15
  documentModelRegistry;
16
- documentStorage;
17
16
  shutdownStatus;
18
17
  setShutdown;
18
+ setCompleted;
19
19
  queue;
20
20
  jobTracker;
21
21
  readModelCoordinator;
22
22
  features;
23
23
  documentView;
24
- _documentIndexer;
24
+ documentIndexer;
25
25
  operationStore;
26
26
  eventBus;
27
- constructor(logger, documentModelRegistry, documentStorage, queue, jobTracker, readModelCoordinator, features, documentView, documentIndexer, operationStore, eventBus) {
27
+ executorManager;
28
+ constructor(logger, documentModelRegistry, queue, jobTracker, readModelCoordinator, features, documentView, documentIndexer, operationStore, eventBus, executorManager) {
28
29
  this.logger = logger;
29
30
  this.documentModelRegistry = documentModelRegistry;
30
- this.documentStorage = documentStorage;
31
31
  this.queue = queue;
32
32
  this.jobTracker = jobTracker;
33
33
  this.readModelCoordinator = readModelCoordinator;
34
34
  this.features = features;
35
35
  this.documentView = documentView;
36
- this._documentIndexer = documentIndexer;
36
+ this.documentIndexer = documentIndexer;
37
37
  this.operationStore = operationStore;
38
38
  this.eventBus = eventBus;
39
- const [status, setter] = createMutableShutdownStatus(false);
39
+ this.executorManager = executorManager;
40
+ const [status, setShutdown, setCompleted] = createMutableShutdownStatus(false);
40
41
  this.shutdownStatus = status;
41
- this.setShutdown = setter;
42
- this.logger.verbose("Reactor({ legacyStorage: @legacy })", features.legacyStorageEnabled);
42
+ this.setShutdown = setShutdown;
43
+ this.setCompleted = setCompleted;
43
44
  this.readModelCoordinator.start();
44
45
  }
45
- /**
46
- * Signals that the reactor should shutdown.
47
- */
48
46
  kill() {
49
47
  this.logger.verbose("kill()");
50
- // Mark the reactor as shutdown
48
+ if (this.shutdownStatus.isShutdown) {
49
+ return this.shutdownStatus;
50
+ }
51
51
  this.setShutdown(true);
52
- // Stop the read model coordinator
53
- this.readModelCoordinator.stop();
54
- // Stop the job tracker
55
- this.jobTracker.shutdown();
52
+ const shutdownAsync = async () => {
53
+ await this.executorManager.stop(true);
54
+ this.readModelCoordinator.stop();
55
+ this.jobTracker.shutdown();
56
+ };
57
+ this.setCompleted(shutdownAsync());
56
58
  return this.shutdownStatus;
57
59
  }
58
- /**
59
- * Retrieves a list of document model specifications
60
- */
61
60
  getDocumentModels(namespace, paging, signal) {
62
61
  this.logger.verbose("getDocumentModels(@namespace, @paging)", namespace, paging);
63
- // Get document model modules from the registry + filter
62
+ if (signal?.aborted) {
63
+ throw new AbortError();
64
+ }
64
65
  const modules = this.documentModelRegistry.getAllModules();
65
66
  const filteredModels = modules.filter((module) => !namespace || module.documentModel.global.id.startsWith(namespace));
66
- // Apply paging
67
67
  const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
68
68
  const limit = paging?.limit || filteredModels.length;
69
69
  const pagedModels = filteredModels.slice(startIndex, startIndex + limit);
70
- // Create paged results
71
70
  const hasMore = startIndex + limit < filteredModels.length;
72
71
  const nextCursor = hasMore ? String(startIndex + limit) : undefined;
73
- // even thought this is currently synchronous, they could have passed in an already-aborted signal
74
- if (signal?.aborted) {
75
- throw new AbortError();
76
- }
77
72
  return Promise.resolve({
78
73
  results: pagedModels,
79
74
  options: paging || { cursor: "0", limit: filteredModels.length },
@@ -83,197 +78,70 @@ export class Reactor {
83
78
  : undefined,
84
79
  });
85
80
  }
86
- /**
87
- * Retrieves a specific PHDocument by id
88
- */
89
81
  async get(id, view, consistencyToken, signal) {
90
82
  this.logger.verbose("get(@id, @view)", id, view);
91
- if (this.features.legacyStorageEnabled) {
92
- const document = await this.documentStorage.get(id, consistencyToken, signal);
93
- if (signal?.aborted) {
94
- throw new AbortError();
95
- }
96
- const childIds = await this.documentStorage.getChildren(id, consistencyToken, signal);
97
- if (signal?.aborted) {
98
- throw new AbortError();
99
- }
100
- for (const scope in document.state) {
101
- if (!matchesScope(view, scope)) {
102
- delete document.state[scope];
103
- }
104
- }
105
- return {
106
- document,
107
- childIds,
108
- };
109
- }
110
- else {
111
- const document = await this.documentView.get(id, view, consistencyToken, signal);
112
- if (signal?.aborted) {
113
- throw new AbortError();
114
- }
115
- const relationships = await this._documentIndexer.getOutgoing(id, ["child"], consistencyToken, signal);
116
- if (signal?.aborted) {
117
- throw new AbortError();
118
- }
119
- const childIds = relationships.map((rel) => rel.targetId);
120
- return {
121
- document,
122
- childIds,
123
- };
124
- }
83
+ return await this.documentView.get(id, view, consistencyToken, signal);
125
84
  }
126
- /**
127
- * Retrieves a specific PHDocument by slug
128
- */
129
85
  async getBySlug(slug, view, consistencyToken, signal) {
130
86
  this.logger.verbose("getBySlug(@slug, @view)", slug, view);
131
- if (this.features.legacyStorageEnabled) {
132
- let ids;
133
- try {
134
- ids = await this.documentStorage.resolveIds([slug], consistencyToken, signal);
135
- }
136
- catch (error) {
137
- if (error instanceof Error && error.message.includes("not found")) {
138
- throw new Error(`Document not found with slug: ${slug}`);
139
- }
140
- throw error;
141
- }
142
- if (ids.length === 0 || !ids[0]) {
143
- throw new Error(`Document not found with slug: ${slug}`);
144
- }
145
- return await this.get(ids[0], view, consistencyToken, signal);
146
- }
147
- else {
148
- const documentId = await this.documentView.resolveSlug(slug, view, consistencyToken, signal);
149
- if (!documentId) {
150
- throw new Error(`Document not found with slug: ${slug}`);
151
- }
152
- return await this.get(documentId, view, consistencyToken, signal);
87
+ const documentId = await this.documentView.resolveSlug(slug, view, consistencyToken, signal);
88
+ if (!documentId) {
89
+ throw new Error(`Document not found with slug: ${slug}`);
153
90
  }
91
+ return await this.get(documentId, view, consistencyToken, signal);
154
92
  }
155
- /**
156
- * Retrieves a specific PHDocument by identifier (either id or slug)
157
- */
158
93
  async getByIdOrSlug(identifier, view, consistencyToken, signal) {
159
94
  this.logger.verbose("getByIdOrSlug(@identifier, @view)", identifier, view);
160
- if (this.features.legacyStorageEnabled) {
161
- try {
162
- return await this.get(identifier, view, consistencyToken, signal);
163
- }
164
- catch {
165
- try {
166
- const ids = await this.documentStorage.resolveIds([identifier], consistencyToken, signal);
167
- if (ids.length === 0 || !ids[0]) {
168
- throw new Error(`Document not found: ${identifier}`);
169
- }
170
- return await this.get(ids[0], view, consistencyToken, signal);
171
- }
172
- catch {
173
- throw new Error(`Document not found: ${identifier}`);
174
- }
175
- }
95
+ return await this.documentView.getByIdOrSlug(identifier, view, consistencyToken, signal);
96
+ }
97
+ async getChildren(documentId, consistencyToken, signal) {
98
+ const relationships = await this.documentIndexer.getOutgoing(documentId, ["child"], undefined, consistencyToken, signal);
99
+ if (signal?.aborted) {
100
+ throw new AbortError();
176
101
  }
177
- else {
178
- const document = await this.documentView.getByIdOrSlug(identifier, view, consistencyToken, signal);
179
- if (signal?.aborted) {
180
- throw new AbortError();
181
- }
182
- const relationships = await this._documentIndexer.getOutgoing(document.header.id, ["child"], consistencyToken, signal);
183
- if (signal?.aborted) {
184
- throw new AbortError();
185
- }
186
- const childIds = relationships.map((rel) => rel.targetId);
187
- return {
188
- document,
189
- childIds,
190
- };
102
+ return relationships.results.map((rel) => rel.targetId);
103
+ }
104
+ async getParents(childId, consistencyToken, signal) {
105
+ const relationships = await this.documentIndexer.getIncoming(childId, ["parent"], undefined, consistencyToken, signal);
106
+ if (signal?.aborted) {
107
+ throw new AbortError();
191
108
  }
109
+ return relationships.results.map((rel) => rel.sourceId);
192
110
  }
193
- /**
194
- * Retrieves the operations for a document
195
- */
196
111
  async getOperations(documentId, view, filter, paging, consistencyToken, signal) {
197
112
  this.logger.verbose("getOperations(@documentId, @view, @filter, @paging)", documentId, view, filter, paging);
198
- if (this.features.legacyStorageEnabled) {
199
- const document = await this.documentStorage.get(documentId, consistencyToken, signal);
200
- if (signal?.aborted) {
201
- throw new AbortError();
202
- }
203
- const operations = document.operations;
204
- const result = {};
205
- for (const scope in operations) {
206
- if (matchesScope(view, scope)) {
207
- let scopeOperations = operations[scope];
208
- if (filter) {
209
- if (filter.actionTypes && filter.actionTypes.length > 0) {
210
- scopeOperations = scopeOperations.filter((op) => filter.actionTypes.includes(op.action.type));
211
- }
212
- if (filter.sinceRevision !== undefined) {
213
- scopeOperations = scopeOperations.filter((op) => op.index >= filter.sinceRevision);
214
- }
215
- if (filter.timestampFrom) {
216
- const fromMs = new Date(filter.timestampFrom).getTime();
217
- scopeOperations = scopeOperations.filter((op) => new Date(op.timestampUtcMs).getTime() >= fromMs);
218
- }
219
- if (filter.timestampTo) {
220
- const toMs = new Date(filter.timestampTo).getTime();
221
- scopeOperations = scopeOperations.filter((op) => new Date(op.timestampUtcMs).getTime() <= toMs);
222
- }
223
- }
224
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
225
- const limit = paging?.limit || scopeOperations.length;
226
- const pagedOperations = scopeOperations.slice(startIndex, startIndex + limit);
227
- result[scope] = {
228
- results: pagedOperations,
229
- options: { cursor: String(startIndex + limit), limit },
230
- };
231
- }
232
- }
233
- return Promise.resolve(result);
113
+ const branch = view?.branch || "main";
114
+ const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
115
+ if (signal?.aborted) {
116
+ throw new AbortError();
234
117
  }
235
- else {
236
- const branch = view?.branch || "main";
237
- const revisions = await this.operationStore.getRevisions(documentId, branch, signal);
118
+ const allScopes = Object.keys(revisions.revision);
119
+ const result = {};
120
+ for (const scope of allScopes) {
121
+ if (!matchesScope(view, scope)) {
122
+ continue;
123
+ }
238
124
  if (signal?.aborted) {
239
125
  throw new AbortError();
240
126
  }
241
- const allScopes = Object.keys(revisions.revision);
242
- const result = {};
243
- for (const scope of allScopes) {
244
- if (!matchesScope(view, scope)) {
245
- continue;
246
- }
247
- if (signal?.aborted) {
248
- throw new AbortError();
249
- }
250
- const scopeResult = await this.operationStore.getSince(documentId, scope, branch, -1, filter, paging, signal);
251
- const currentCursor = paging?.cursor ? parseInt(paging.cursor) : 0;
252
- const currentLimit = paging?.limit || 100;
253
- result[scope] = {
254
- results: scopeResult.items,
255
- options: {
256
- cursor: scopeResult.nextCursor || String(currentCursor),
257
- limit: currentLimit,
258
- },
259
- nextCursor: scopeResult.nextCursor,
260
- next: scopeResult.hasMore
261
- ? async () => {
262
- const nextPage = await this.getOperations(documentId, view, filter, {
263
- cursor: scopeResult.nextCursor,
264
- limit: currentLimit,
265
- }, consistencyToken, signal);
266
- return nextPage[scope];
267
- }
268
- : undefined,
269
- };
270
- }
271
- return result;
127
+ const scopeResult = await this.operationStore.getSince(documentId, scope, branch, -1, filter, paging, signal);
128
+ result[scope] = {
129
+ results: scopeResult.results,
130
+ options: scopeResult.options,
131
+ nextCursor: scopeResult.nextCursor,
132
+ next: scopeResult.next
133
+ ? async () => {
134
+ const nextPage = await this.getOperations(documentId, view, filter, {
135
+ cursor: scopeResult.nextCursor,
136
+ limit: scopeResult.options.limit,
137
+ }, consistencyToken, signal);
138
+ return nextPage[scope];
139
+ }
140
+ : undefined,
141
+ };
272
142
  }
143
+ return result;
273
144
  }
274
- /**
275
- * Filters documents by criteria and returns a list of them
276
- */
277
145
  async find(search, view, paging, consistencyToken, signal) {
278
146
  this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
279
147
  let results;
@@ -293,7 +161,7 @@ export class Reactor {
293
161
  }
294
162
  }
295
163
  else if (search.parentId) {
296
- results = await this.findByParentId(search.parentId, view, paging, signal);
164
+ results = await this.findByParentId(search.parentId, view, paging, consistencyToken, signal);
297
165
  if (search.type) {
298
166
  results = filterByType(results, search.type);
299
167
  }
@@ -309,9 +177,6 @@ export class Reactor {
309
177
  }
310
178
  return results;
311
179
  }
312
- /**
313
- * Creates a document
314
- */
315
180
  async create(document, signer, signal, meta) {
316
181
  this.logger.verbose("create(@id, @type, @slug)", document.header.id, document.header.documentType, document.header.slug);
317
182
  const createdAtUtcIso = new Date().toISOString();
@@ -345,12 +210,10 @@ export class Reactor {
345
210
  toVersion: document.state.document.version,
346
211
  initialState: document.state,
347
212
  });
348
- // Sign actions if signer is provided
349
213
  let actions = [createAction, upgradeAction];
350
214
  if (signer) {
351
215
  actions = await signActions(actions, signer, signal);
352
216
  }
353
- // Create a single job with both CREATE_DOCUMENT and UPGRADE_DOCUMENT actions
354
217
  const job = {
355
218
  id: uuidv4(),
356
219
  kind: "mutation",
@@ -365,7 +228,6 @@ export class Reactor {
365
228
  errorHistory: [],
366
229
  meta,
367
230
  };
368
- // Create job info and register with tracker
369
231
  const jobInfo = {
370
232
  id: job.id,
371
233
  status: JobStatus.PENDING,
@@ -379,13 +241,9 @@ export class Reactor {
379
241
  };
380
242
  this.jobTracker.registerJob(jobInfo);
381
243
  this.emitJobPending(jobInfo.id, meta);
382
- // Enqueue the job
383
244
  await this.queue.enqueue(job);
384
245
  return jobInfo;
385
246
  }
386
- /**
387
- * Deletes a document
388
- */
389
247
  async deleteDocument(id, signer, signal, meta) {
390
248
  this.logger.verbose("deleteDocument(@id)", id);
391
249
  const createdAtUtcIso = new Date().toISOString();
@@ -393,7 +251,6 @@ export class Reactor {
393
251
  throw new AbortError();
394
252
  }
395
253
  let action = deleteDocumentAction(id);
396
- // Sign action if signer is provided
397
254
  if (signer) {
398
255
  action = await signAction(action, signer, signal);
399
256
  }
@@ -427,18 +284,13 @@ export class Reactor {
427
284
  await this.queue.enqueue(job);
428
285
  return jobInfo;
429
286
  }
430
- /**
431
- * Applies a list of actions to a document
432
- */
433
287
  async execute(docId, branch, actions, signal, meta) {
434
288
  this.logger.verbose("execute(@docId, @branch, @actions)", docId, branch, actions);
435
289
  if (signal?.aborted) {
436
290
  throw new AbortError();
437
291
  }
438
292
  const createdAtUtcIso = new Date().toISOString();
439
- // Determine scope from first action (all actions should have the same scope)
440
- const scope = actions.length > 0 ? actions[0].scope || "global" : "global";
441
- // Create a single job with all actions
293
+ const scope = getSharedActionScope(actions);
442
294
  const job = {
443
295
  id: uuidv4(),
444
296
  kind: "mutation",
@@ -453,7 +305,6 @@ export class Reactor {
453
305
  errorHistory: [],
454
306
  meta,
455
307
  };
456
- // Create job info and register with tracker
457
308
  const jobInfo = {
458
309
  id: job.id,
459
310
  status: JobStatus.PENDING,
@@ -467,18 +318,12 @@ export class Reactor {
467
318
  };
468
319
  this.jobTracker.registerJob(jobInfo);
469
320
  this.emitJobPending(jobInfo.id, meta);
470
- // Enqueue the job
471
321
  await this.queue.enqueue(job);
472
322
  if (signal?.aborted) {
473
323
  throw new AbortError();
474
324
  }
475
325
  return jobInfo;
476
326
  }
477
- /**
478
- * Imports pre-existing operations that were produced by another reactor.
479
- * This function may cause a reshuffle, which will generate additional
480
- * operations.
481
- */
482
327
  async load(docId, branch, operations, signal, meta) {
483
328
  this.logger.verbose("load(@docId, @branch, @count, @operations)", docId, branch, operations.length, operations);
484
329
  if (signal?.aborted) {
@@ -487,13 +332,7 @@ export class Reactor {
487
332
  if (operations.length === 0) {
488
333
  throw new Error("load requires at least one operation");
489
334
  }
490
- // validate the operations
491
- const scope = getSharedScope(operations);
492
- operations.forEach((operation, index) => {
493
- if (!operation.timestampUtcMs) {
494
- throw new Error(`Operation at position ${index} is missing timestampUtcMs`);
495
- }
496
- });
335
+ const scope = getSharedOperationScope(operations);
497
336
  const createdAtUtcIso = new Date().toISOString();
498
337
  const job = {
499
338
  id: uuidv4(),
@@ -528,9 +367,6 @@ export class Reactor {
528
367
  }
529
368
  return jobInfo;
530
369
  }
531
- /**
532
- * Applies multiple mutations across documents with dependency management
533
- */
534
370
  async executeBatch(request, signal, meta) {
535
371
  this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
536
372
  if (signal?.aborted) {
@@ -611,39 +447,28 @@ export class Reactor {
611
447
  };
612
448
  return result;
613
449
  }
614
- /**
615
- * Adds multiple documents as children to another
616
- */
617
450
  async addChildren(parentId, documentIds, branch = "main", signer, signal) {
618
451
  this.logger.verbose("addChildren(@parentId, @count children, @branch)", parentId, documentIds.length, branch);
619
452
  if (signal?.aborted) {
620
453
  throw new AbortError();
621
454
  }
622
455
  let actions = documentIds.map((childId) => addRelationshipAction(parentId, childId, "child"));
623
- // Sign actions if signer is provided
624
456
  if (signer) {
625
457
  actions = await signActions(actions, signer, signal);
626
458
  }
627
459
  return await this.execute(parentId, branch, actions, signal);
628
460
  }
629
- /**
630
- * Removes multiple documents as children from another
631
- */
632
461
  async removeChildren(parentId, documentIds, branch = "main", signer, signal) {
633
462
  this.logger.verbose("removeChildren(@parentId, @count children, @branch)", parentId, documentIds.length, branch);
634
463
  if (signal?.aborted) {
635
464
  throw new AbortError();
636
465
  }
637
466
  let actions = documentIds.map((childId) => removeRelationshipAction(parentId, childId, "child"));
638
- // Sign actions if signer is provided
639
467
  if (signer) {
640
468
  actions = await signActions(actions, signer, signal);
641
469
  }
642
470
  return await this.execute(parentId, branch, actions, signal);
643
471
  }
644
- /**
645
- * Retrieves the status of a job
646
- */
647
472
  getJobStatus(jobId, signal) {
648
473
  this.logger.verbose("getJobStatus(@jobId)", jobId);
649
474
  if (signal?.aborted) {
@@ -651,7 +476,6 @@ export class Reactor {
651
476
  }
652
477
  const jobInfo = this.jobTracker.getJobStatus(jobId);
653
478
  if (!jobInfo) {
654
- // Job not found - return FAILED status with appropriate error
655
479
  const now = new Date().toISOString();
656
480
  return Promise.resolve({
657
481
  id: jobId,
@@ -668,327 +492,34 @@ export class Reactor {
668
492
  }
669
493
  return Promise.resolve(jobInfo);
670
494
  }
671
- /**
672
- * Finds documents by their IDs
673
- */
674
495
  async findByIds(ids, view, paging, consistencyToken, signal) {
675
496
  this.logger.verbose("findByIds(@count ids)", ids.length);
676
- if (consistencyToken) {
677
- await this.documentView.waitForConsistency(consistencyToken, undefined, signal);
678
- }
679
- if (this.features.legacyStorageEnabled) {
680
- const documents = [];
681
- // Fetch each document by ID using storage directly
682
- for (const id of ids) {
683
- if (signal?.aborted) {
684
- throw new AbortError();
685
- }
686
- let document;
687
- try {
688
- document = await this.documentStorage.get(id, consistencyToken, signal);
689
- }
690
- catch {
691
- // Skip documents that don't exist or can't be accessed
692
- // This matches the behavior expected from a search operation
693
- continue;
694
- }
695
- // Apply view filter - This will be removed when we pass the viewfilter along
696
- // to the underlying store, but is here now for the interface.
697
- for (const scope in document.state) {
698
- if (!matchesScope(view, scope)) {
699
- delete document.state[scope];
700
- }
701
- }
702
- documents.push(document);
703
- }
704
- if (signal?.aborted) {
705
- throw new AbortError();
706
- }
707
- // Apply paging
708
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
709
- const limit = paging?.limit || documents.length;
710
- const pagedDocuments = documents.slice(startIndex, startIndex + limit);
711
- // Create paged results
712
- const hasMore = startIndex + limit < documents.length;
713
- const nextCursor = hasMore ? String(startIndex + limit) : undefined;
714
- return {
715
- results: pagedDocuments,
716
- options: paging || { cursor: "0", limit: documents.length },
717
- nextCursor,
718
- next: hasMore
719
- ? () => this.findByIds(ids, view, { cursor: nextCursor, limit }, consistencyToken, signal)
720
- : undefined,
721
- };
722
- }
723
- else {
724
- const documents = [];
725
- // Fetch each document by ID using documentView
726
- for (const id of ids) {
727
- if (signal?.aborted) {
728
- throw new AbortError();
729
- }
730
- try {
731
- const document = await this.documentView.get(id, view, undefined, signal);
732
- documents.push(document);
733
- }
734
- catch {
735
- // Skip documents that don't exist or can't be accessed
736
- continue;
737
- }
738
- }
739
- if (signal?.aborted) {
740
- throw new AbortError();
741
- }
742
- // Apply paging
743
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
744
- const limit = paging?.limit || documents.length;
745
- const pagedDocuments = documents.slice(startIndex, startIndex + limit);
746
- // Create paged results
747
- const hasMore = startIndex + limit < documents.length;
748
- const nextCursor = hasMore ? String(startIndex + limit) : undefined;
749
- return {
750
- results: pagedDocuments,
751
- options: paging || { cursor: "0", limit: documents.length },
752
- nextCursor,
753
- next: hasMore
754
- ? () => this.findByIds(ids, view, { cursor: nextCursor, limit }, consistencyToken, signal)
755
- : undefined,
756
- };
757
- }
497
+ const startIndex = paging?.cursor ? parseInt(paging.cursor) || 0 : 0;
498
+ const limit = paging?.limit || ids.length;
499
+ const pagedIds = ids.slice(startIndex, startIndex + limit);
500
+ const results = await this.documentView.getMany(pagedIds, view, consistencyToken, signal);
501
+ const hasMore = startIndex + limit < ids.length;
502
+ const nextCursor = hasMore ? String(startIndex + limit) : undefined;
503
+ return {
504
+ results,
505
+ options: paging || { cursor: "0", limit: ids.length },
506
+ nextCursor,
507
+ };
758
508
  }
759
- /**
760
- * Finds documents by their slugs
761
- */
762
509
  async findBySlugs(slugs, view, paging, consistencyToken, signal) {
763
510
  this.logger.verbose("findBySlugs(@count slugs)", slugs.length);
764
- if (consistencyToken) {
765
- await this.documentView.waitForConsistency(consistencyToken, undefined, signal);
766
- }
767
- if (this.features.legacyStorageEnabled) {
768
- const documents = [];
769
- // Use storage to resolve slugs to IDs
770
- let ids;
771
- try {
772
- ids = await this.documentStorage.resolveIds(slugs, consistencyToken, signal);
773
- }
774
- catch {
775
- // If slug resolution fails, return empty results
776
- // This matches the behavior expected from a search operation
777
- ids = [];
778
- }
779
- // Fetch each document by resolved ID
780
- for (const id of ids) {
781
- if (signal?.aborted) {
782
- throw new AbortError();
783
- }
784
- let document;
785
- try {
786
- document = await this.documentStorage.get(id, consistencyToken, signal);
787
- }
788
- catch {
789
- // Skip documents that don't exist or can't be accessed
790
- continue;
791
- }
792
- // Apply view filter - This will be removed when we pass the viewfilter along
793
- // to the underlying store, but is here now for the interface.
794
- for (const scope in document.state) {
795
- if (!matchesScope(view, scope)) {
796
- delete document.state[scope];
797
- }
798
- }
799
- documents.push(document);
800
- }
801
- if (signal?.aborted) {
802
- throw new AbortError();
803
- }
804
- // Apply paging - this will be removed when we pass the paging along
805
- // to the underlying store, but is here now for the interface.
806
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
807
- const limit = paging?.limit || documents.length;
808
- const pagedDocuments = documents.slice(startIndex, startIndex + limit);
809
- // Create paged results
810
- const hasMore = startIndex + limit < documents.length;
811
- const nextCursor = hasMore ? String(startIndex + limit) : undefined;
812
- return {
813
- results: pagedDocuments,
814
- options: paging || { cursor: "0", limit: documents.length },
815
- nextCursor,
816
- next: hasMore
817
- ? () => this.findBySlugs(slugs, view, { cursor: nextCursor, limit }, consistencyToken, signal)
818
- : undefined,
819
- };
820
- }
821
- else {
822
- const documents = [];
823
- // Resolve each slug to a document ID
824
- const documentIds = [];
825
- for (const slug of slugs) {
826
- if (signal?.aborted) {
827
- throw new AbortError();
828
- }
829
- const documentId = await this.documentView.resolveSlug(slug, view, undefined, signal);
830
- if (documentId) {
831
- documentIds.push(documentId);
832
- }
833
- }
834
- // Fetch each document
835
- for (const documentId of documentIds) {
836
- if (signal?.aborted) {
837
- throw new AbortError();
838
- }
839
- try {
840
- const document = await this.documentView.get(documentId, view, undefined, signal);
841
- documents.push(document);
842
- }
843
- catch {
844
- // Skip documents that don't exist or can't be accessed
845
- continue;
846
- }
847
- }
848
- if (signal?.aborted) {
849
- throw new AbortError();
850
- }
851
- // Apply paging
852
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
853
- const limit = paging?.limit || documents.length;
854
- const pagedDocuments = documents.slice(startIndex, startIndex + limit);
855
- // Create paged results
856
- const hasMore = startIndex + limit < documents.length;
857
- const nextCursor = hasMore ? String(startIndex + limit) : undefined;
858
- return {
859
- results: pagedDocuments,
860
- options: paging || { cursor: "0", limit: documents.length },
861
- nextCursor,
862
- next: hasMore
863
- ? () => this.findBySlugs(slugs, view, { cursor: nextCursor, limit }, consistencyToken, signal)
864
- : undefined,
865
- };
866
- }
511
+ const ids = await this.documentView.resolveSlugs(slugs, view, consistencyToken, signal);
512
+ return await this.findByIds(ids, view, paging, consistencyToken, signal);
867
513
  }
868
- /**
869
- * Finds documents by parent ID
870
- */
871
- async findByParentId(parentId, view, paging, signal) {
514
+ async findByParentId(parentId, view, paging, consistencyToken, signal) {
872
515
  this.logger.verbose("findByParentId(@parentId)", parentId);
873
- // Get child relationships from indexer
874
- const relationships = await this._documentIndexer.getOutgoing(parentId, ["child"], undefined, signal);
875
- if (signal?.aborted) {
876
- throw new AbortError();
877
- }
878
- const documents = [];
879
- // Fetch each child document using the appropriate storage method
880
- for (const relationship of relationships) {
881
- if (signal?.aborted) {
882
- throw new AbortError();
883
- }
884
- try {
885
- let document;
886
- if (this.features.legacyStorageEnabled) {
887
- document = await this.documentStorage.get(relationship.targetId);
888
- // Apply view filter for legacy storage
889
- for (const scope in document.state) {
890
- if (!matchesScope(view, scope)) {
891
- delete document.state[scope];
892
- }
893
- }
894
- }
895
- else {
896
- document = await this.documentView.get(relationship.targetId, view, undefined, signal);
897
- }
898
- documents.push(document);
899
- }
900
- catch {
901
- // Skip documents that don't exist or can't be accessed
902
- continue;
903
- }
904
- }
905
- if (signal?.aborted) {
906
- throw new AbortError();
907
- }
908
- // Apply paging
909
- const startIndex = paging ? parseInt(paging.cursor) || 0 : 0;
910
- const limit = paging?.limit || documents.length;
911
- const pagedDocuments = documents.slice(startIndex, startIndex + limit);
912
- // Create paged results
913
- const hasMore = startIndex + limit < documents.length;
914
- const nextCursor = hasMore ? String(startIndex + limit) : undefined;
915
- return {
916
- results: pagedDocuments,
917
- options: paging || { cursor: "0", limit: documents.length },
918
- nextCursor,
919
- next: hasMore
920
- ? () => this.findByParentId(parentId, view, { cursor: nextCursor, limit }, signal)
921
- : undefined,
922
- };
516
+ const relationships = await this.documentIndexer.getOutgoing(parentId, ["child"], paging, consistencyToken, signal);
517
+ const ids = relationships.results.map((rel) => rel.targetId);
518
+ return await this.findByIds(ids, view, paging, undefined, signal);
923
519
  }
924
- /**
925
- * Finds documents by type
926
- */
927
520
  async findByType(type, view, paging, consistencyToken, signal) {
928
521
  this.logger.verbose("findByType(@type)", type);
929
- if (consistencyToken) {
930
- await this.documentView.waitForConsistency(consistencyToken, undefined, signal);
931
- }
932
- if (this.features.legacyStorageEnabled) {
933
- const documents = [];
934
- // Use storage's findByType method directly
935
- const cursor = paging?.cursor;
936
- const limit = paging?.limit || 100;
937
- // Get document IDs of the specified type
938
- const { documents: documentIds, nextCursor } = await this.documentStorage.findByType(type, limit, cursor, consistencyToken, signal);
939
- if (signal?.aborted) {
940
- throw new AbortError();
941
- }
942
- // Fetch each document by its ID
943
- for (const documentId of documentIds) {
944
- if (signal?.aborted) {
945
- throw new AbortError();
946
- }
947
- let document;
948
- try {
949
- document = await this.documentStorage.get(documentId, consistencyToken, signal);
950
- }
951
- catch {
952
- // Skip documents that can't be retrieved
953
- continue;
954
- }
955
- // Apply view filter
956
- for (const scope in document.state) {
957
- if (!matchesScope(view, scope)) {
958
- delete document.state[scope];
959
- }
960
- }
961
- documents.push(document);
962
- }
963
- if (signal?.aborted) {
964
- throw new AbortError();
965
- }
966
- // Results are already paged from the storage layer
967
- return {
968
- results: documents,
969
- options: paging || { cursor: cursor || "0", limit },
970
- nextCursor,
971
- next: nextCursor
972
- ? async () => this.findByType(type, view, { cursor: nextCursor, limit }, consistencyToken, signal)
973
- : undefined,
974
- };
975
- }
976
- else {
977
- const result = await this.documentView.findByType(type, view, paging, consistencyToken, signal);
978
- if (signal?.aborted) {
979
- throw new AbortError();
980
- }
981
- const cursor = paging?.cursor;
982
- const limit = paging?.limit || 100;
983
- return {
984
- results: result.items,
985
- options: paging || { cursor: cursor || "0", limit },
986
- nextCursor: result.nextCursor,
987
- next: result.nextCursor
988
- ? async () => this.findByType(type, view, { cursor: result.nextCursor, limit }, consistencyToken, signal)
989
- : undefined,
990
- };
991
- }
522
+ return await this.documentView.findByType(type, view, paging, consistencyToken, signal);
992
523
  }
993
524
  emitJobPending(jobId, meta) {
994
525
  const event = {