@powerhousedao/reactor 6.2.3-dev.22 → 6.2.3-dev.23

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { $ as parsePagingOptions, A as targetDocumentId, B as DuplicateOperationError, C as InvalidModuleError, D as createEmptyConsistencyToken, E as createConsistencyToken, F as buildDecisionModel, G as AuthorizationDeniedError, H as RevisionMismatchError, I as APPEND_CONDITION_FAILED_PREFIX, J as ExcessiveReshuffleError, K as DocumentDeletedError, L as AppendConditionFailedError, M as selectDecisionModel, N as documentDecisionModel, O as submittedActionIds, P as authDecisionModel, Q as matchesScope, R as DocumentAlreadyExistsError, S as DuplicateModuleError, T as GATED_DOCUMENT_ACTIONS, U as AuthEnforcementDisabledError, V as OptimisticLockError, W as AuthTimestampNotMonotonicError, X as RelationshipNotFoundError, Y as InvalidOperationTimestampError, Z as UpgradePreconditionFailedError, _ as DocumentMetaCache, a as createForwardingPoolInstrumentation, b as JobExecutorEventTypes, c as KyselyKeyframeStore, d as DriveCollectionId, et as throwIfAborted, f as KyselyExecutionScope, g as KyselyOperationIndex, h as KyselyWriteCache, i as runMigrations, j as decideAtHead, k as summarizeSubmittedActions, l as DocumentModelRegistry, m as EventBus, n as REACTOR_SCHEMA, o as instrumentPgPool, p as resolveFeatureFlags, q as DocumentNotFoundError, r as getMigrationStatus, s as KyselyOperationStore, t as DEFAULT_DRIVE_CONTAINER_TYPES, u as SimpleJobExecutor, v as CollectionMembershipCache, w as ModuleNotFoundError, x as DuplicateManifestError, y as DEFAULT_DEFERRED_JOB_TTL_MS, z as DocumentExistence } from "./drive-container-types-CS5IxDiA.js";
2
2
  import { n as ReactorEventTypes, t as EventBusAggregateError } from "./types-DMKLa0Ok.js";
3
3
  import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-D3S6Eysd.js";
4
- import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-C1oY6CaR.js";
4
+ import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-CQzTEE1o.js";
5
5
  import { n as errorToInfo, r as sanitizeArg, t as createForwardingLogger } from "./forwarding-logger-BBkMSxuJ.js";
6
6
  import { t as workerEntryPath } from "./worker-g03lNM42.js";
7
7
  import { DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actions, actions as documentActions, createPresignedHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, sortOperations, toTransportAction } from "@powerhousedao/shared/document-model";
@@ -2727,24 +2727,267 @@ function resolveProcessorSlots(records, legacy) {
2727
2727
  });
2728
2728
  }
2729
2729
  //#endregion
2730
+ //#region src/processors/processor-queue.ts
2731
+ function isDelivery(task) {
2732
+ return task?.kind === "live" || task?.kind === "advance";
2733
+ }
2734
+ function errorMessage(error) {
2735
+ return error instanceof Error ? error.message : String(error);
2736
+ }
2737
+ function lowestOf(ops) {
2738
+ let lowest = ops[0].context.ordinal;
2739
+ for (const op of ops) lowest = Math.min(lowest, op.context.ordinal);
2740
+ return lowest;
2741
+ }
2742
+ function highestOf(ops) {
2743
+ let highest = 0;
2744
+ for (const op of ops) highest = Math.max(highest, op.context.ordinal);
2745
+ return highest;
2746
+ }
2747
+ var ProcessorQueue = class {
2748
+ tasks = [];
2749
+ running = false;
2750
+ closed = false;
2751
+ replaysAhead = 0;
2752
+ unrouted = /* @__PURE__ */ new Set();
2753
+ constructor(options) {
2754
+ this.options = options;
2755
+ }
2756
+ get isClosed() {
2757
+ return this.closed;
2758
+ }
2759
+ /** Resolves at once behind a replay, so a pass never waits out a backfill. */
2760
+ live(ops) {
2761
+ const delivered = this.push((done) => ({
2762
+ kind: "live",
2763
+ ops,
2764
+ done
2765
+ }));
2766
+ return this.replaysAhead > 0 ? Promise.resolve() : delivered;
2767
+ }
2768
+ /** Raises the cursor past a batch with nothing for this processor. */
2769
+ advance(through) {
2770
+ const last = this.tasks.at(-1);
2771
+ if (last?.kind === "advance") {
2772
+ last.through = Math.max(last.through, through);
2773
+ return Promise.resolve();
2774
+ }
2775
+ return this.push((done) => ({
2776
+ kind: "advance",
2777
+ through,
2778
+ done
2779
+ }));
2780
+ }
2781
+ /** Replays from the cursor as it stands when the task runs. */
2782
+ backfill() {
2783
+ return this.replay("backfill", () => this.runBackfill());
2784
+ }
2785
+ /** Clears an error and replays from the cursor. No-op when active. */
2786
+ retry() {
2787
+ return this.replay("retry", async () => {
2788
+ const { cursor } = this.options;
2789
+ if (cursor.status !== "errored") return;
2790
+ cursor.status = "active";
2791
+ cursor.lastError = void 0;
2792
+ cursor.lastErrorTimestamp = void 0;
2793
+ await this.persist();
2794
+ await this.runBackfill();
2795
+ });
2796
+ }
2797
+ /** Stops cursor writes now; queued tasks still run before the disconnect. */
2798
+ close() {
2799
+ if (this.closed) return Promise.resolve();
2800
+ const disconnect = this.enqueue("disconnect", async () => {
2801
+ try {
2802
+ await this.options.processor.onDisconnect();
2803
+ } catch (error) {
2804
+ this.options.logger.error("Error disconnecting processor '@ProcessorId': @Error", this.options.processorId, error);
2805
+ }
2806
+ });
2807
+ this.closed = true;
2808
+ return disconnect;
2809
+ }
2810
+ replay(kind, run) {
2811
+ if (this.closed) return Promise.resolve();
2812
+ this.replaysAhead++;
2813
+ return this.enqueue(kind, async () => {
2814
+ try {
2815
+ await run();
2816
+ } finally {
2817
+ this.replaysAhead--;
2818
+ }
2819
+ });
2820
+ }
2821
+ enqueue(kind, run) {
2822
+ return this.push((done) => ({
2823
+ kind,
2824
+ run,
2825
+ done
2826
+ }));
2827
+ }
2828
+ push(task) {
2829
+ if (this.closed) return Promise.resolve();
2830
+ return new Promise((resolve) => {
2831
+ this.tasks.push(task(resolve));
2832
+ if (this.running) return;
2833
+ this.running = true;
2834
+ queueMicrotask(() => void this.drain());
2835
+ });
2836
+ }
2837
+ async drain() {
2838
+ while (this.tasks.length > 0) {
2839
+ const task = this.tasks.shift();
2840
+ const batch = [];
2841
+ if (isDelivery(task)) {
2842
+ batch.push(task);
2843
+ let size = task.kind === "live" ? task.ops.length : 0;
2844
+ let next = this.tasks[0];
2845
+ while (isDelivery(next) && size < 500) {
2846
+ if (next.kind === "live") size += next.ops.length;
2847
+ batch.push(next);
2848
+ this.tasks.shift();
2849
+ next = this.tasks[0];
2850
+ }
2851
+ }
2852
+ try {
2853
+ await (isDelivery(task) ? this.deliverLive(batch) : task.run());
2854
+ } catch (error) {
2855
+ this.options.logger.error("Processor '@ProcessorId' task '@Kind' failed: @Error", this.options.processorId, task.kind, error);
2856
+ } finally {
2857
+ task.done();
2858
+ for (const merged of batch.slice(1)) merged.done();
2859
+ }
2860
+ }
2861
+ this.running = false;
2862
+ }
2863
+ async deliverLive(batch) {
2864
+ const { cursor, floor } = this.options;
2865
+ const ops = [];
2866
+ let through = 0;
2867
+ for (const task of batch) if (task.kind === "live") ops.push(...task.ops);
2868
+ else through = Math.max(through, task.through);
2869
+ const fresh = ops.filter((op) => {
2870
+ const ordinal = op.context.ordinal;
2871
+ if (ordinal <= floor) return false;
2872
+ return !this.unrouted.delete(ordinal);
2873
+ });
2874
+ if (fresh.length === 0) {
2875
+ await this.raiseCursor(through);
2876
+ return;
2877
+ }
2878
+ if (cursor.status !== "active") {
2879
+ await this.parkBelow(fresh);
2880
+ return;
2881
+ }
2882
+ if (!await this.deliver(fresh)) {
2883
+ await this.parkBelow(fresh);
2884
+ return;
2885
+ }
2886
+ await this.raiseCursor(Math.max(highestOf(fresh), through));
2887
+ }
2888
+ async runBackfill() {
2889
+ const { cursor, filter, floor } = this.options;
2890
+ if (cursor.status !== "active") return;
2891
+ let page;
2892
+ try {
2893
+ page = await this.options.readSince(cursor.lastOrdinal);
2894
+ } catch (error) {
2895
+ await this.fail(error, "reading backfill");
2896
+ return;
2897
+ }
2898
+ while (page.results.length > 0) {
2899
+ if (this.closed) return;
2900
+ const matching = page.results.filter((op) => op.context.ordinal > floor && matchesFilter(op, filter));
2901
+ if (matching.length > 0) {
2902
+ if (!await this.deliver(matching)) {
2903
+ await this.persist();
2904
+ return;
2905
+ }
2906
+ this.dedupeQueued(matching);
2907
+ }
2908
+ await this.raiseCursor(highestOf(page.results));
2909
+ if (!page.next) break;
2910
+ try {
2911
+ page = await page.next();
2912
+ } catch (error) {
2913
+ await this.fail(error, "reading backfill");
2914
+ return;
2915
+ }
2916
+ }
2917
+ }
2918
+ dedupeQueued(delivered) {
2919
+ const routed = this.options.routedThrough();
2920
+ const queued = /* @__PURE__ */ new Set();
2921
+ for (const op of delivered) {
2922
+ const ordinal = op.context.ordinal;
2923
+ if (ordinal > routed) this.unrouted.add(ordinal);
2924
+ else queued.add(ordinal);
2925
+ }
2926
+ if (queued.size === 0) return;
2927
+ for (const task of this.tasks) {
2928
+ if (task.kind !== "live") continue;
2929
+ task.ops = task.ops.filter((op) => !queued.has(op.context.ordinal));
2930
+ }
2931
+ }
2932
+ async deliver(ops) {
2933
+ try {
2934
+ await this.options.processor.onOperations(ops);
2935
+ return true;
2936
+ } catch (error) {
2937
+ this.markErrored(error);
2938
+ this.options.logger.error("Processor '@ProcessorId' failed at ordinal @Ordinal: @Error", this.options.processorId, lowestOf(ops), error);
2939
+ return false;
2940
+ }
2941
+ }
2942
+ async fail(error, during) {
2943
+ this.markErrored(error);
2944
+ await this.persist();
2945
+ this.options.logger.error("Processor '@ProcessorId' failed @During: @Error", this.options.processorId, during, error);
2946
+ }
2947
+ markErrored(error) {
2948
+ const { cursor } = this.options;
2949
+ cursor.status = "errored";
2950
+ cursor.lastError = errorMessage(error);
2951
+ cursor.lastErrorTimestamp = /* @__PURE__ */ new Date();
2952
+ }
2953
+ async raiseCursor(through) {
2954
+ const { cursor } = this.options;
2955
+ if (cursor.status !== "active") return;
2956
+ if (through <= cursor.lastOrdinal) return;
2957
+ cursor.lastOrdinal = through;
2958
+ await this.persist();
2959
+ }
2960
+ async parkBelow(missed) {
2961
+ const { cursor } = this.options;
2962
+ cursor.lastOrdinal = Math.min(cursor.lastOrdinal, lowestOf(missed) - 1);
2963
+ await this.persist();
2964
+ }
2965
+ async persist() {
2966
+ if (this.closed) return;
2967
+ const { cursor } = this.options;
2968
+ try {
2969
+ await this.options.persist({
2970
+ lastOrdinal: cursor.lastOrdinal,
2971
+ status: cursor.status,
2972
+ lastError: cursor.lastError,
2973
+ lastErrorTimestamp: cursor.lastErrorTimestamp
2974
+ });
2975
+ } catch (error) {
2976
+ this.options.logger.error("Failed to persist cursor for '@ProcessorId': @Error", this.options.processorId, error);
2977
+ }
2978
+ }
2979
+ };
2980
+ //#endregion
2730
2981
  //#region src/processors/processor-manager.ts
2731
- /**
2732
- * Manages processor lifecycle based on operations.
2733
- * Extends BaseReadModel to receive operations from ReadModelCoordinator.
2734
- *
2735
- * Responsibilities:
2736
- * 1. Detect drive creation from CREATE_DOCUMENT operations
2737
- * 2. Create processors for each drive using registered factories
2738
- * 3. Route operations to matching processors based on filters
2739
- * 4. Clean up processors when drives are deleted or factories are unregistered
2740
- * 5. Track per-processor cursors for failure recovery and backfill
2741
- */
2742
2982
  var ProcessorManager = class extends BaseReadModel {
2743
2983
  factoryRegistry = /* @__PURE__ */ new Map();
2744
2984
  processorsByDrive = /* @__PURE__ */ new Map();
2745
- factoryToProcessors = /* @__PURE__ */ new Map();
2985
+ pendingSlots = /* @__PURE__ */ new Set();
2746
2986
  knownDrives = /* @__PURE__ */ new Map();
2987
+ highestRoutedOrdinal = 0;
2747
2988
  cursorCache = /* @__PURE__ */ new Map();
2989
+ cursorWrites = /* @__PURE__ */ new Map();
2990
+ draining = /* @__PURE__ */ new Map();
2748
2991
  logger;
2749
2992
  driveContainerTypes;
2750
2993
  legacyProcessorIds;
@@ -2759,39 +3002,31 @@ var ProcessorManager = class extends BaseReadModel {
2759
3002
  this.legacyProcessorIds = options.legacyProcessorIds ?? true;
2760
3003
  }
2761
3004
  async init() {
2762
- await super.init();
2763
3005
  await this.loadAllCursors();
3006
+ await super.init();
2764
3007
  await this.discoverExistingDrives();
2765
3008
  }
2766
3009
  async commitOperations(items) {
2767
- await this.detectAndRegisterNewDrives(items);
2768
- await this.detectAndCleanupDeletedDrives(items);
2769
- await this.routeOperationsToProcessors(items);
3010
+ const { runs, reserved } = this.detectNewDrives(items);
3011
+ const disconnects = this.detectDeletedDrives(items);
3012
+ const deliveries = this.enqueueRouted(items, reserved);
3013
+ await Promise.all([
3014
+ ...runs.map((run) => run()),
3015
+ ...disconnects,
3016
+ ...deliveries
3017
+ ]);
2770
3018
  }
2771
3019
  async registerFactory(identifier, factory) {
2772
- if (this.factoryRegistry.has(identifier)) await this.unregisterFactory(identifier);
3020
+ const removals = this.removeFactory(identifier);
2773
3021
  this.factoryRegistry.set(identifier, factory);
2774
- this.factoryToProcessors.set(identifier, /* @__PURE__ */ new Map());
2775
- for (const [driveId, documentType] of this.knownDrives) {
2776
- const driveHeader = createMinimalDriveHeader(driveId, documentType);
2777
- await this.createProcessorsForDrive(driveId, identifier, factory, driveHeader);
2778
- }
3022
+ const previous = this.draining.get(identifier);
3023
+ const creationOrdinal = this.highWater() + 1;
3024
+ const runs = [];
3025
+ for (const [driveId, documentType] of this.knownDrives) runs.push(this.reserveSlot(identifier, factory, driveId, createMinimalDriveHeader(driveId, documentType), creationOrdinal, void 0, false, previous).run);
3026
+ await Promise.all([...removals, ...runs.map((run) => run())]);
2779
3027
  }
2780
3028
  async unregisterFactory(identifier) {
2781
- const factoryProcessors = this.factoryToProcessors.get(identifier);
2782
- if (!factoryProcessors) return;
2783
- for (const [driveId, tracked] of factoryProcessors) {
2784
- for (const t of tracked) await this.safeDisconnect(t.record.processor);
2785
- const driveProcessors = this.processorsByDrive.get(driveId);
2786
- if (driveProcessors) {
2787
- const remaining = driveProcessors.filter((p) => !tracked.includes(p));
2788
- if (remaining.length > 0) this.processorsByDrive.set(driveId, remaining);
2789
- else this.processorsByDrive.delete(driveId);
2790
- }
2791
- }
2792
- await this.deleteProcessorCursors({ factoryId: identifier });
2793
- this.factoryToProcessors.delete(identifier);
2794
- this.factoryRegistry.delete(identifier);
3029
+ await Promise.all(this.removeFactory(identifier));
2795
3030
  }
2796
3031
  get(processorId) {
2797
3032
  for (const tracked of this.allTrackedProcessors()) if (tracked.processorId === processorId) return tracked;
@@ -2799,208 +3034,283 @@ var ProcessorManager = class extends BaseReadModel {
2799
3034
  getAll() {
2800
3035
  return Array.from(this.allTrackedProcessors());
2801
3036
  }
2802
- *allTrackedProcessors() {
2803
- for (const tracked of this.processorsByDrive.values()) yield* tracked;
2804
- }
2805
- async detectAndRegisterNewDrives(operations) {
2806
- for (const op of operations) {
3037
+ /** Synchronous: reserves a slot per registered factory for each new drive. */
3038
+ detectNewDrives(items) {
3039
+ const runs = [];
3040
+ const reserved = /* @__PURE__ */ new Set();
3041
+ for (const op of items) {
2807
3042
  if (!this.isDriveCreation(op)) continue;
2808
3043
  const driveId = op.context.documentId;
2809
3044
  if (this.knownDrives.has(driveId)) continue;
2810
3045
  this.knownDrives.set(driveId, op.context.documentType);
2811
3046
  const driveHeader = extractDriveHeader(op);
2812
3047
  if (!driveHeader) continue;
2813
- for (const [identifier, factory] of this.factoryRegistry) await this.createProcessorsForDrive(driveId, identifier, factory, driveHeader);
3048
+ for (const [identifier, factory] of this.factoryRegistry) {
3049
+ const { slot, run } = this.reserveSlot(identifier, factory, driveId, driveHeader, op.context.ordinal, items, true);
3050
+ reserved.add(slot);
3051
+ runs.push(run);
3052
+ }
2814
3053
  }
3054
+ return {
3055
+ runs,
3056
+ reserved
3057
+ };
2815
3058
  }
2816
- isDriveCreation(op) {
2817
- return op.operation.action.type === "CREATE_DOCUMENT" && this.driveContainerTypes.has(op.context.documentType);
2818
- }
2819
- async detectAndCleanupDeletedDrives(operations) {
2820
- for (const op of operations) {
3059
+ /** Synchronous: drops a deleted drive's processors from the tables. */
3060
+ detectDeletedDrives(items) {
3061
+ const pending = [];
3062
+ for (const op of items) {
2821
3063
  if (!isDriveDeletion(op)) continue;
2822
3064
  const driveId = extractDeletedDocumentId(op);
2823
3065
  if (!driveId || !this.knownDrives.has(driveId)) continue;
2824
- if (!this.isDeletedDocumentADrive(driveId)) continue;
2825
- await this.cleanupDriveProcessors(driveId);
2826
3066
  this.knownDrives.delete(driveId);
3067
+ for (const slot of this.pendingSlots) if (slot.driveId === driveId) this.pendingSlots.delete(slot);
3068
+ for (const { queue } of this.processorsByDrive.get(driveId) ?? []) queue.close();
3069
+ this.processorsByDrive.delete(driveId);
3070
+ pending.push(...this.deleteCursors((row) => row.driveId === driveId));
3071
+ }
3072
+ return pending;
3073
+ }
3074
+ /** Synchronous: puts each processor's share of the batch on its queue. */
3075
+ enqueueRouted(items, reserved) {
3076
+ if (items.length === 0) return [];
3077
+ let lowest = items[0].context.ordinal;
3078
+ let highest = 0;
3079
+ for (const item of items) {
3080
+ lowest = Math.min(lowest, item.context.ordinal);
3081
+ highest = Math.max(highest, item.context.ordinal);
3082
+ }
3083
+ this.highestRoutedOrdinal = Math.max(this.highestRoutedOrdinal, highest);
3084
+ for (const slot of this.pendingSlots) {
3085
+ if (reserved.has(slot)) continue;
3086
+ slot.lowestRoutedOrdinal = Math.min(slot.lowestRoutedOrdinal ?? lowest, lowest);
3087
+ }
3088
+ const deliveries = [];
3089
+ for (const { tracked, queue } of this.allBound()) {
3090
+ const matching = items.filter((op) => matchesFilter(op, tracked.record.filter));
3091
+ if (matching.length > 0) deliveries.push(queue.live(matching));
3092
+ else queue.advance(highest);
3093
+ }
3094
+ return deliveries;
3095
+ }
3096
+ /** Synchronous: binds records, or discards them if the slot was cancelled. */
3097
+ bind(slot, records, creationOrdinal, creationItems) {
3098
+ const released = this.pendingSlots.delete(slot);
3099
+ if (!records || records.length === 0) {
3100
+ const settled = Promise.resolve();
3101
+ return {
3102
+ persisted: settled,
3103
+ delivered: settled
3104
+ };
3105
+ }
3106
+ if (!released) {
3107
+ const discarded = records.map((record) => this.discard(slot, record.processor.onDisconnect.bind(record.processor)));
3108
+ const settled = Promise.all(discarded).then(() => void 0);
3109
+ return {
3110
+ persisted: settled,
3111
+ delivered: settled
3112
+ };
2827
3113
  }
3114
+ const { factoryId, driveId, lowestRoutedOrdinal } = slot;
3115
+ const ids = resolveProcessorSlots(records, this.legacyProcessorIds);
3116
+ const bound = records.map((record, i) => this.track(slot, record, i, `${factoryId}:${driveId}:${ids[i]}`, creationOrdinal, lowestRoutedOrdinal));
3117
+ const drive = this.processorsByDrive.get(driveId) ?? [];
3118
+ this.processorsByDrive.set(driveId, [...drive, ...bound]);
3119
+ const liveIds = new Set(bound.map((b) => b.tracked.processorId));
3120
+ const writes = this.deleteCursors((row) => row.factoryId === factoryId && row.driveId === driveId && !liveIds.has(row.processorId));
3121
+ for (const { tracked } of bound) writes.push(this.writeCursor(tracked, tracked));
3122
+ const deliveries = [];
3123
+ for (const { tracked, queue } of bound) {
3124
+ const missed = tracked.lastOrdinal < slot.reservedAt || lowestRoutedOrdinal !== void 0;
3125
+ if (tracked.status === "active" && missed) deliveries.push(queue.backfill());
3126
+ const matching = creationItems?.filter((op) => matchesFilter(op, tracked.record.filter));
3127
+ if (matching && matching.length > 0) deliveries.push(queue.live(matching));
3128
+ }
3129
+ return {
3130
+ persisted: Promise.all(writes).then(() => void 0),
3131
+ delivered: Promise.all(deliveries).then(() => void 0)
3132
+ };
2828
3133
  }
2829
- async discoverExistingDrives() {
2830
- const drives = await this.db.selectFrom("DocumentSnapshot").select(["documentId", "documentType"]).where("documentType", "in", [...this.driveContainerTypes]).where("isDeleted", "=", false).execute();
2831
- for (const drive of drives) this.knownDrives.set(drive.documentId, drive.documentType);
3134
+ /** Synchronous: removes a factory's slots, processors and cursor rows. */
3135
+ removeFactory(identifier) {
3136
+ if (!this.factoryRegistry.delete(identifier)) return [];
3137
+ const closing = [];
3138
+ for (const slot of this.pendingSlots) {
3139
+ if (slot.factoryId !== identifier) continue;
3140
+ this.pendingSlots.delete(slot);
3141
+ if (slot.settled) closing.push(slot.settled);
3142
+ }
3143
+ for (const [driveId, drive] of this.processorsByDrive) {
3144
+ const remaining = [];
3145
+ for (const b of drive) if (b.tracked.factoryId === identifier) closing.push(b.queue.close());
3146
+ else remaining.push(b);
3147
+ if (remaining.length > 0) this.processorsByDrive.set(driveId, remaining);
3148
+ else this.processorsByDrive.delete(driveId);
3149
+ }
3150
+ if (closing.length > 0) {
3151
+ const drained = Promise.all([this.draining.get(identifier), ...closing]).then(() => void 0);
3152
+ this.draining.set(identifier, drained);
3153
+ drained.then(() => {
3154
+ if (this.draining.get(identifier) === drained) this.draining.delete(identifier);
3155
+ });
3156
+ }
3157
+ return this.deleteCursors((row) => row.factoryId === identifier);
2832
3158
  }
2833
- isDeletedDocumentADrive(documentId) {
2834
- return this.knownDrives.has(documentId);
3159
+ reserveSlot(factoryId, factory, driveId, driveHeader, creationOrdinal, creationItems, awaitDelivery, previous) {
3160
+ const slot = {
3161
+ factoryId,
3162
+ driveId,
3163
+ reservedAt: this.highWater(),
3164
+ lowestRoutedOrdinal: void 0,
3165
+ settled: void 0
3166
+ };
3167
+ this.pendingSlots.add(slot);
3168
+ const run = async () => {
3169
+ const bound = (async () => {
3170
+ await previous;
3171
+ const records = await this.runFactory(slot, factory, driveHeader);
3172
+ return this.bind(slot, records, creationOrdinal, creationItems);
3173
+ })();
3174
+ slot.settled = bound.then(({ persisted }) => persisted);
3175
+ const { persisted, delivered } = await bound;
3176
+ await (awaitDelivery ? Promise.all([persisted, delivered]) : persisted);
3177
+ };
3178
+ return {
3179
+ slot,
3180
+ run
3181
+ };
2835
3182
  }
2836
- async createProcessorsForDrive(driveId, identifier, factory, driveHeader) {
2837
- let records;
3183
+ async runFactory(slot, factory, driveHeader) {
2838
3184
  try {
2839
- records = await factory(driveHeader);
3185
+ return await factory(driveHeader);
2840
3186
  } catch (error) {
2841
- this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error", identifier, driveId, error);
3187
+ this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error", slot.factoryId, slot.driveId, error);
2842
3188
  return;
2843
3189
  }
2844
- if (records.length === 0) return;
2845
- const trackedList = [];
2846
- const slots = resolveProcessorSlots(records, this.legacyProcessorIds);
2847
- for (let i = 0; i < records.length; i++) {
2848
- const record = records[i];
2849
- const processorId = `${identifier}:${driveId}:${slots[i]}`;
2850
- const cached = this.cursorCache.get(processorId);
2851
- let lastOrdinal;
2852
- let status;
2853
- let lastError;
2854
- let lastErrorTimestamp;
2855
- if (cached) {
2856
- lastOrdinal = cached.lastOrdinal;
2857
- status = cached.status;
2858
- lastError = cached.lastError ?? void 0;
2859
- lastErrorTimestamp = cached.lastErrorTimestamp ?? void 0;
2860
- } else {
2861
- lastOrdinal = (record.startFrom ?? "beginning") === "current" ? this.lastOrdinal : 0;
2862
- status = "active";
2863
- lastError = void 0;
2864
- lastErrorTimestamp = void 0;
2865
- }
2866
- const tracked = {
2867
- processorId,
2868
- factoryId: identifier,
2869
- driveId,
2870
- processorIndex: i,
2871
- record,
2872
- lastOrdinal,
2873
- status,
2874
- lastError,
2875
- lastErrorTimestamp,
2876
- retry: () => this.retryProcessor(tracked)
3190
+ }
3191
+ track(slot, record, processorIndex, processorId, creationOrdinal, lowestRoutedOrdinal) {
3192
+ const cached = this.cursorCache.get(processorId);
3193
+ let floor = 0;
3194
+ let cursor;
3195
+ if (cached) cursor = {
3196
+ lastOrdinal: cached.lastOrdinal,
3197
+ status: cached.status,
3198
+ lastError: cached.lastError ?? void 0,
3199
+ lastErrorTimestamp: cached.lastErrorTimestamp ?? void 0
3200
+ };
3201
+ else {
3202
+ if (record.startFrom === "current") floor = creationOrdinal - 1;
3203
+ cursor = {
3204
+ lastOrdinal: floor,
3205
+ status: "active",
3206
+ lastError: void 0,
3207
+ lastErrorTimestamp: void 0
2877
3208
  };
2878
- trackedList.push(tracked);
2879
- await this.saveProcessorCursor(tracked);
2880
- }
2881
- const liveIds = new Set(trackedList.map((t) => t.processorId));
2882
- await this.db.deleteFrom("ProcessorCursor").where("factoryId", "=", identifier).where("driveId", "=", driveId).where("processorId", "not in", [...liveIds]).execute();
2883
- for (const [id, row] of this.cursorCache) if (row.factoryId === identifier && row.driveId === driveId && !liveIds.has(id)) this.cursorCache.delete(id);
2884
- const factoryProcessors = this.factoryToProcessors.get(identifier);
2885
- if (factoryProcessors) factoryProcessors.set(driveId, trackedList);
2886
- const existingDriveProcessors = this.processorsByDrive.get(driveId) ?? [];
2887
- this.processorsByDrive.set(driveId, [...existingDriveProcessors, ...trackedList]);
2888
- for (const tracked of trackedList) if (tracked.status === "active" && tracked.lastOrdinal < this.lastOrdinal) await this.backfillProcessor(tracked);
2889
- }
2890
- async backfillProcessor(tracked) {
2891
- let page = await this.operationIndex.getSinceOrdinal(tracked.lastOrdinal);
2892
- while (page.results.length > 0) {
2893
- const matching = page.results.filter((op) => matchesFilter(op, tracked.record.filter));
2894
- if (matching.length > 0) try {
2895
- await tracked.record.processor.onOperations(matching);
2896
- } catch (error) {
2897
- tracked.status = "errored";
2898
- tracked.lastError = error instanceof Error ? error.message : String(error);
2899
- tracked.lastErrorTimestamp = /* @__PURE__ */ new Date();
2900
- await this.safeSaveProcessorCursor(tracked);
2901
- this.logger.error("Processor '@ProcessorId' failed during backfill at ordinal @Ordinal: @Error", tracked.processorId, tracked.lastOrdinal, error);
2902
- return;
2903
- }
2904
- tracked.lastOrdinal = page.results[page.results.length - 1].context.ordinal;
2905
- await this.safeSaveProcessorCursor(tracked);
2906
- if (!page.next) break;
2907
- page = await page.next();
2908
- }
2909
- }
2910
- async retryProcessor(tracked) {
2911
- if (tracked.status !== "errored") return;
2912
- tracked.status = "active";
2913
- tracked.lastError = void 0;
2914
- tracked.lastErrorTimestamp = void 0;
2915
- await this.saveProcessorCursor(tracked);
2916
- await this.backfillProcessor(tracked);
2917
- }
2918
- async cleanupDriveProcessors(driveId) {
2919
- const processors = this.processorsByDrive.get(driveId);
2920
- if (!processors) return;
2921
- for (const tracked of processors) await this.safeDisconnect(tracked.record.processor);
2922
- this.processorsByDrive.delete(driveId);
2923
- for (const factoryProcessors of this.factoryToProcessors.values()) factoryProcessors.delete(driveId);
2924
- await this.deleteProcessorCursors({ driveId });
2925
- }
2926
- async safeDisconnect(processor) {
3209
+ }
3210
+ if (lowestRoutedOrdinal !== void 0) cursor.lastOrdinal = Math.max(floor, Math.min(cursor.lastOrdinal, lowestRoutedOrdinal - 1));
3211
+ const tracked = {
3212
+ processorId,
3213
+ factoryId: slot.factoryId,
3214
+ driveId: slot.driveId,
3215
+ processorIndex,
3216
+ record,
3217
+ ...cursor,
3218
+ retry: () => queue.retry()
3219
+ };
3220
+ const queue = new ProcessorQueue({
3221
+ processorId,
3222
+ processor: record.processor,
3223
+ filter: record.filter,
3224
+ cursor: tracked,
3225
+ floor,
3226
+ readSince: (ordinal) => this.operationIndex.getSinceOrdinal(ordinal),
3227
+ routedThrough: () => this.highWater(),
3228
+ persist: (state) => this.writeCursor(tracked, state),
3229
+ logger: this.logger
3230
+ });
3231
+ return {
3232
+ tracked,
3233
+ queue
3234
+ };
3235
+ }
3236
+ async discard(slot, disconnect) {
2927
3237
  try {
2928
- await processor.onDisconnect();
3238
+ await disconnect();
2929
3239
  } catch (error) {
2930
- this.logger.error("Error disconnecting processor: @Error", error);
3240
+ this.logger.error("Error disconnecting discarded processor for '@FactoryId' on '@DriveId': @Error", slot.factoryId, slot.driveId, error);
2931
3241
  }
2932
3242
  }
2933
- async routeOperationsToProcessors(operations) {
2934
- let maxOrdinal = 0;
2935
- for (const op of operations) maxOrdinal = Math.max(maxOrdinal, op.context.ordinal);
2936
- const allTracked = Array.from(this.allTrackedProcessors());
2937
- await Promise.all(allTracked.map(async (tracked) => {
2938
- if (tracked.status !== "active") return;
2939
- const matching = operations.filter((op) => op.context.ordinal > tracked.lastOrdinal).filter((op) => matchesFilter(op, tracked.record.filter));
2940
- if (matching.length > 0) try {
2941
- await tracked.record.processor.onOperations(matching);
2942
- } catch (error) {
2943
- tracked.status = "errored";
2944
- tracked.lastError = error instanceof Error ? error.message : String(error);
2945
- tracked.lastErrorTimestamp = /* @__PURE__ */ new Date();
2946
- await this.safeSaveProcessorCursor(tracked);
2947
- this.logger.error("Processor '@ProcessorId' failed at ordinal @Ordinal: @Error", tracked.processorId, tracked.lastOrdinal, error);
2948
- return;
2949
- }
2950
- tracked.lastOrdinal = maxOrdinal;
2951
- await this.safeSaveProcessorCursor(tracked);
2952
- }));
3243
+ highWater() {
3244
+ return Math.max(this.lastOrdinal, this.highestRoutedOrdinal);
3245
+ }
3246
+ *allBound() {
3247
+ for (const drive of this.processorsByDrive.values()) yield* drive;
3248
+ }
3249
+ *allTrackedProcessors() {
3250
+ for (const { tracked } of this.allBound()) yield tracked;
3251
+ }
3252
+ isDriveCreation(op) {
3253
+ return op.operation.action.type === "CREATE_DOCUMENT" && this.driveContainerTypes.has(op.context.documentType);
3254
+ }
3255
+ async discoverExistingDrives() {
3256
+ const drives = await this.db.selectFrom("DocumentSnapshot").select(["documentId", "documentType"]).where("documentType", "in", [...this.driveContainerTypes]).where("isDeleted", "=", false).execute();
3257
+ for (const drive of drives) this.knownDrives.set(drive.documentId, drive.documentType);
2953
3258
  }
2954
3259
  async loadAllCursors() {
2955
3260
  const rows = await this.db.selectFrom("ProcessorCursor").selectAll().execute();
2956
3261
  for (const row of rows) this.cursorCache.set(row.processorId, row);
2957
3262
  }
2958
- async safeSaveProcessorCursor(tracked) {
2959
- try {
2960
- await this.saveProcessorCursor(tracked);
2961
- } catch (error) {
2962
- this.logger.error("Failed to persist cursor for '@ProcessorId': @Error", tracked.processorId, error);
2963
- }
3263
+ lane(processorId, write) {
3264
+ const next = (this.cursorWrites.get(processorId) ?? Promise.resolve()).then(write).then(() => void 0, (error) => {
3265
+ this.logger.error("Failed to write cursor for '@ProcessorId': @Error", processorId, error);
3266
+ });
3267
+ this.cursorWrites.set(processorId, next);
3268
+ next.then(() => {
3269
+ if (this.cursorWrites.get(processorId) === next) this.cursorWrites.delete(processorId);
3270
+ });
3271
+ return next;
2964
3272
  }
2965
- async saveProcessorCursor(tracked) {
2966
- await this.db.insertInto("ProcessorCursor").values({
3273
+ writeCursor(tracked, state) {
3274
+ const now = /* @__PURE__ */ new Date();
3275
+ const row = {
2967
3276
  processorId: tracked.processorId,
2968
3277
  factoryId: tracked.factoryId,
2969
3278
  driveId: tracked.driveId,
2970
3279
  processorIndex: tracked.processorIndex,
2971
- lastOrdinal: tracked.lastOrdinal,
2972
- status: tracked.status,
2973
- lastError: tracked.lastError ?? null,
2974
- lastErrorTimestamp: tracked.lastErrorTimestamp ?? null,
2975
- updatedAt: /* @__PURE__ */ new Date()
3280
+ lastOrdinal: state.lastOrdinal,
3281
+ status: state.status,
3282
+ lastError: state.lastError ?? null,
3283
+ lastErrorTimestamp: state.lastErrorTimestamp ?? null,
3284
+ createdAt: now,
3285
+ updatedAt: now
3286
+ };
3287
+ this.cursorCache.set(row.processorId, row);
3288
+ return this.lane(row.processorId, () => this.db.insertInto("ProcessorCursor").values({
3289
+ processorId: row.processorId,
3290
+ factoryId: row.factoryId,
3291
+ driveId: row.driveId,
3292
+ processorIndex: row.processorIndex,
3293
+ lastOrdinal: row.lastOrdinal,
3294
+ status: row.status,
3295
+ lastError: row.lastError,
3296
+ lastErrorTimestamp: row.lastErrorTimestamp,
3297
+ updatedAt: now
2976
3298
  }).onConflict((oc) => oc.column("processorId").doUpdateSet({
2977
- lastOrdinal: tracked.lastOrdinal,
2978
- status: tracked.status,
2979
- lastError: tracked.lastError ?? null,
2980
- lastErrorTimestamp: tracked.lastErrorTimestamp ?? null,
2981
- updatedAt: /* @__PURE__ */ new Date()
2982
- })).execute();
2983
- this.cursorCache.set(tracked.processorId, {
2984
- processorId: tracked.processorId,
2985
- factoryId: tracked.factoryId,
2986
- driveId: tracked.driveId,
2987
- processorIndex: tracked.processorIndex,
2988
- lastOrdinal: tracked.lastOrdinal,
2989
- status: tracked.status,
2990
- lastError: tracked.lastError ?? null,
2991
- lastErrorTimestamp: tracked.lastErrorTimestamp ?? null,
2992
- createdAt: /* @__PURE__ */ new Date(),
2993
- updatedAt: /* @__PURE__ */ new Date()
2994
- });
2995
- }
2996
- async deleteProcessorCursors(filter) {
2997
- if ("factoryId" in filter) {
2998
- await this.db.deleteFrom("ProcessorCursor").where("factoryId", "=", filter.factoryId).execute();
2999
- for (const [id, row] of this.cursorCache) if (row.factoryId === filter.factoryId) this.cursorCache.delete(id);
3000
- } else {
3001
- await this.db.deleteFrom("ProcessorCursor").where("driveId", "=", filter.driveId).execute();
3002
- for (const [id, row] of this.cursorCache) if (row.driveId === filter.driveId) this.cursorCache.delete(id);
3003
- }
3299
+ lastOrdinal: row.lastOrdinal,
3300
+ status: row.status,
3301
+ lastError: row.lastError,
3302
+ lastErrorTimestamp: row.lastErrorTimestamp,
3303
+ updatedAt: now
3304
+ })).execute());
3305
+ }
3306
+ deleteCursors(matches) {
3307
+ const deletes = [];
3308
+ for (const [processorId, row] of this.cursorCache) {
3309
+ if (!matches(row)) continue;
3310
+ this.cursorCache.delete(processorId);
3311
+ deletes.push(this.lane(processorId, () => this.db.deleteFrom("ProcessorCursor").where("processorId", "=", processorId).execute()));
3312
+ }
3313
+ return deletes;
3004
3314
  }
3005
3315
  };
3006
3316
  //#endregion
@@ -8506,7 +8816,7 @@ var ReactorBuilder = class {
8506
8816
  }
8507
8817
  this.logger.info("Shutdown complete");
8508
8818
  nodeProcess.exit = realExit;
8509
- realExit(pendingExitCode ?? 0);
8819
+ realExit(pendingExitCode || Number(nodeProcess.exitCode ?? 0));
8510
8820
  };
8511
8821
  nodeProcess.prependListener("SIGINT", () => void handler("SIGINT"));
8512
8822
  nodeProcess.prependListener("SIGTERM", () => void handler("SIGTERM"));