@powerhousedao/reactor 6.2.3-dev.21 → 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/{document-indexer-C1oY6CaR.js → document-indexer-CQzTEE1o.js} +4 -3
- package/dist/document-indexer-CQzTEE1o.js.map +1 -0
- package/dist/index.d.ts +38 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +514 -204
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +1 -1
- package/package.json +7 -4
- package/dist/document-indexer-C1oY6CaR.js.map +0 -1
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-
|
|
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
|
-
|
|
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
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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
|
-
|
|
3020
|
+
const removals = this.removeFactory(identifier);
|
|
2773
3021
|
this.factoryRegistry.set(identifier, factory);
|
|
2774
|
-
this.
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
for (const op of
|
|
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)
|
|
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
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
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
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
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
|
-
|
|
2834
|
-
|
|
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
|
|
2837
|
-
let records;
|
|
3183
|
+
async runFactory(slot, factory, driveHeader) {
|
|
2838
3184
|
try {
|
|
2839
|
-
|
|
3185
|
+
return await factory(driveHeader);
|
|
2840
3186
|
} catch (error) {
|
|
2841
|
-
this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error",
|
|
3187
|
+
this.logger.error("Factory '@FactoryId' failed for drive '@DriveId': @Error", slot.factoryId, slot.driveId, error);
|
|
2842
3188
|
return;
|
|
2843
3189
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
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
|
|
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
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
}
|
|
2962
|
-
|
|
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
|
-
|
|
2966
|
-
|
|
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:
|
|
2972
|
-
status:
|
|
2973
|
-
lastError:
|
|
2974
|
-
lastErrorTimestamp:
|
|
2975
|
-
|
|
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:
|
|
2978
|
-
status:
|
|
2979
|
-
lastError:
|
|
2980
|
-
lastErrorTimestamp:
|
|
2981
|
-
updatedAt:
|
|
2982
|
-
})).execute();
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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"));
|