@swirls/looms 0.0.1
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/LICENSE +190 -0
- package/README.md +9 -0
- package/dist/actor/cell.d.ts +27 -0
- package/dist/actor/cell.js +41 -0
- package/dist/actor/host.d.ts +27 -0
- package/dist/actor/host.js +82 -0
- package/dist/actor/index.d.ts +3 -0
- package/dist/actor/index.js +3 -0
- package/dist/actor/routing.d.ts +15 -0
- package/dist/actor/routing.js +80 -0
- package/dist/agent/definitions-store.d.ts +11 -0
- package/dist/agent/definitions-store.js +10 -0
- package/dist/agent/definitions.d.ts +152 -0
- package/dist/agent/definitions.js +74 -0
- package/dist/agent/effects.d.ts +425 -0
- package/dist/agent/effects.js +318 -0
- package/dist/agent/events.d.ts +170 -0
- package/dist/agent/events.js +61 -0
- package/dist/agent/index.d.ts +13 -0
- package/dist/agent/index.js +13 -0
- package/dist/agent/llm.d.ts +35 -0
- package/dist/agent/llm.js +48 -0
- package/dist/agent/module.d.ts +1037 -0
- package/dist/agent/module.js +27 -0
- package/dist/agent/projections.d.ts +17 -0
- package/dist/agent/projections.js +81 -0
- package/dist/agent/scope.d.ts +171 -0
- package/dist/agent/scope.js +7 -0
- package/dist/agent/signals.d.ts +17 -0
- package/dist/agent/signals.js +19 -0
- package/dist/agent/stop-when.d.ts +12 -0
- package/dist/agent/stop-when.js +6 -0
- package/dist/agent/threads.d.ts +34 -0
- package/dist/agent/threads.js +407 -0
- package/dist/agent/tool-schema.d.ts +21 -0
- package/dist/agent/tool-schema.js +67 -0
- package/dist/agent/types.d.ts +87 -0
- package/dist/agent/types.js +32 -0
- package/dist/ai-vercel/index.d.ts +2 -0
- package/dist/ai-vercel/index.js +2 -0
- package/dist/ai-vercel/messages.d.ts +9 -0
- package/dist/ai-vercel/messages.js +68 -0
- package/dist/ai-vercel/vercel-llm.d.ts +8 -0
- package/dist/ai-vercel/vercel-llm.js +98 -0
- package/dist/approval/effects.d.ts +133 -0
- package/dist/approval/effects.js +13 -0
- package/dist/approval/events.d.ts +52 -0
- package/dist/approval/events.js +28 -0
- package/dist/approval/index.d.ts +6 -0
- package/dist/approval/index.js +6 -0
- package/dist/approval/module.d.ts +342 -0
- package/dist/approval/module.js +10 -0
- package/dist/approval/projections.d.ts +42 -0
- package/dist/approval/projections.js +57 -0
- package/dist/approval/scope.d.ts +45 -0
- package/dist/approval/scope.js +7 -0
- package/dist/approval/signals.d.ts +19 -0
- package/dist/approval/signals.js +34 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +6 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +133 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/client/client.d.ts +69 -0
- package/dist/client/client.js +179 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/sse.d.ts +10 -0
- package/dist/client/sse.js +103 -0
- package/dist/cloudflare/alarm-scheduler.d.ts +9 -0
- package/dist/cloudflare/alarm-scheduler.js +13 -0
- package/dist/cloudflare/durable-object.d.ts +24 -0
- package/dist/cloudflare/durable-object.js +43 -0
- package/dist/cloudflare/event-store.d.ts +10 -0
- package/dist/cloudflare/event-store.js +18 -0
- package/dist/cloudflare/index.d.ts +4 -0
- package/dist/cloudflare/index.js +4 -0
- package/dist/cloudflare/router.d.ts +12 -0
- package/dist/cloudflare/router.js +26 -0
- package/dist/core/bun-sqlite.d.ts +13 -0
- package/dist/core/bun-sqlite.js +19 -0
- package/dist/core/catalog.d.ts +49 -0
- package/dist/core/catalog.js +39 -0
- package/dist/core/coalesce.d.ts +21 -0
- package/dist/core/coalesce.js +178 -0
- package/dist/core/compose.d.ts +59 -0
- package/dist/core/compose.js +97 -0
- package/dist/core/cursor.d.ts +16 -0
- package/dist/core/cursor.js +46 -0
- package/dist/core/effects.d.ts +110 -0
- package/dist/core/effects.js +99 -0
- package/dist/core/envelope.d.ts +79 -0
- package/dist/core/envelope.js +68 -0
- package/dist/core/event-codec.d.ts +8 -0
- package/dist/core/event-codec.js +82 -0
- package/dist/core/event-index.d.ts +28 -0
- package/dist/core/event-index.js +86 -0
- package/dist/core/fold.d.ts +19 -0
- package/dist/core/fold.js +362 -0
- package/dist/core/ids.d.ts +11 -0
- package/dist/core/ids.js +47 -0
- package/dist/core/index.d.ts +26 -0
- package/dist/core/index.js +25 -0
- package/dist/core/match.d.ts +8 -0
- package/dist/core/match.js +58 -0
- package/dist/core/module-scope.d.ts +99 -0
- package/dist/core/module-scope.js +72 -0
- package/dist/core/module.d.ts +74 -0
- package/dist/core/module.js +3 -0
- package/dist/core/projection.d.ts +83 -0
- package/dist/core/projection.js +241 -0
- package/dist/core/protocol.d.ts +99 -0
- package/dist/core/protocol.js +101 -0
- package/dist/core/replay.d.ts +13 -0
- package/dist/core/replay.js +34 -0
- package/dist/core/schema.d.ts +35 -0
- package/dist/core/schema.js +130 -0
- package/dist/core/serialize.d.ts +14 -0
- package/dist/core/serialize.js +42 -0
- package/dist/core/snapshot-store.d.ts +39 -0
- package/dist/core/snapshot-store.js +53 -0
- package/dist/core/snapshots.d.ts +26 -0
- package/dist/core/snapshots.js +100 -0
- package/dist/core/sqlite-store.d.ts +31 -0
- package/dist/core/sqlite-store.js +328 -0
- package/dist/core/state.d.ts +43 -0
- package/dist/core/state.js +31 -0
- package/dist/core/store.d.ts +146 -0
- package/dist/core/store.js +229 -0
- package/dist/core/thread.d.ts +41 -0
- package/dist/core/thread.js +13 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.js +42 -0
- package/dist/core/validate-event.d.ts +19 -0
- package/dist/core/validate-event.js +42 -0
- package/dist/debugger/components/debugger-split.d.ts +23 -0
- package/dist/debugger/components/debugger-split.js +25 -0
- package/dist/debugger/components/event-inspector.d.ts +5 -0
- package/dist/debugger/components/event-inspector.js +38 -0
- package/dist/debugger/components/event-stream.d.ts +13 -0
- package/dist/debugger/components/event-stream.js +119 -0
- package/dist/debugger/components/json-view.d.ts +5 -0
- package/dist/debugger/components/json-view.js +5 -0
- package/dist/debugger/components/run-tree.d.ts +14 -0
- package/dist/debugger/components/run-tree.js +38 -0
- package/dist/debugger/components/status-dot.d.ts +3 -0
- package/dist/debugger/components/status-dot.js +5 -0
- package/dist/debugger/contracts.d.ts +22 -0
- package/dist/debugger/contracts.js +1 -0
- package/dist/debugger/events/catalog.d.ts +6 -0
- package/dist/debugger/events/catalog.js +27 -0
- package/dist/debugger/events/domain-summarize.d.ts +3 -0
- package/dist/debugger/events/domain-summarize.js +107 -0
- package/dist/debugger/events/family.d.ts +8 -0
- package/dist/debugger/events/family.js +44 -0
- package/dist/debugger/events/protocol-summarize.d.ts +3 -0
- package/dist/debugger/events/protocol-summarize.js +70 -0
- package/dist/debugger/index.d.ts +17 -0
- package/dist/debugger/index.js +16 -0
- package/dist/debugger/lib/cn.d.ts +5 -0
- package/dist/debugger/lib/cn.js +17 -0
- package/dist/debugger/lib/event-counts.d.ts +5 -0
- package/dist/debugger/lib/event-counts.js +23 -0
- package/dist/debugger/lib/event-meta.d.ts +8 -0
- package/dist/debugger/lib/event-meta.js +20 -0
- package/dist/debugger/lib/project-run.d.ts +9 -0
- package/dist/debugger/lib/project-run.js +14 -0
- package/dist/debugger/lib/status.d.ts +3 -0
- package/dist/debugger/lib/status.js +31 -0
- package/dist/debugger/lib/visible-events.d.ts +8 -0
- package/dist/debugger/lib/visible-events.js +21 -0
- package/dist/debugger/styles.css +71 -0
- package/dist/debugger/ui/checkbox.d.ts +4 -0
- package/dist/debugger/ui/checkbox.js +9 -0
- package/dist/debugger/ui/input.d.ts +3 -0
- package/dist/debugger/ui/input.js +7 -0
- package/dist/debugger/ui/scroll-area.d.ts +9 -0
- package/dist/debugger/ui/scroll-area.js +10 -0
- package/dist/debugger/ui/tabs.d.ts +7 -0
- package/dist/debugger/ui/tabs.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/projectors/index-model.d.ts +56 -0
- package/dist/projectors/index-model.js +117 -0
- package/dist/projectors/index.d.ts +4 -0
- package/dist/projectors/index.js +3 -0
- package/dist/projectors/memory.d.ts +5 -0
- package/dist/projectors/memory.js +77 -0
- package/dist/projectors/postgres.d.ts +9 -0
- package/dist/projectors/postgres.js +108 -0
- package/dist/projectors/projector.d.ts +11 -0
- package/dist/projectors/projector.js +1 -0
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +43 -0
- package/dist/projectors/sqlite.d.ts +9 -0
- package/dist/projectors/sqlite.js +103 -0
- package/dist/projectors/with-projectors.d.ts +17 -0
- package/dist/projectors/with-projectors.js +55 -0
- package/dist/react/derived.d.ts +19 -0
- package/dist/react/derived.js +50 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/materialize.d.ts +3 -0
- package/dist/react/materialize.js +86 -0
- package/dist/react/notify.d.ts +18 -0
- package/dist/react/notify.js +80 -0
- package/dist/react/react.d.ts +61 -0
- package/dist/react/react.js +187 -0
- package/dist/react/register.d.ts +31 -0
- package/dist/react/register.js +1 -0
- package/dist/react/sse.d.ts +1 -0
- package/dist/react/sse.js +1 -0
- package/dist/react/store.d.ts +56 -0
- package/dist/react/store.js +340 -0
- package/dist/react/tables.d.ts +21 -0
- package/dist/react/tables.js +6 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -0
- package/dist/runtime/dispatch-effect.js +113 -0
- package/dist/runtime/events-api.d.ts +4 -0
- package/dist/runtime/events-api.js +84 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/looms.d.ts +74 -0
- package/dist/runtime/looms.js +109 -0
- package/dist/runtime/runtime-helpers.d.ts +51 -0
- package/dist/runtime/runtime-helpers.js +230 -0
- package/dist/runtime/runtime.d.ts +91 -0
- package/dist/runtime/runtime.js +531 -0
- package/dist/runtime/server.d.ts +17 -0
- package/dist/runtime/server.js +226 -0
- package/dist/runtime/sse.d.ts +15 -0
- package/dist/runtime/sse.js +96 -0
- package/dist/runtime/wake-scheduler.d.ts +8 -0
- package/dist/runtime/wake-scheduler.js +38 -0
- package/dist/s2/config.d.ts +27 -0
- package/dist/s2/config.js +54 -0
- package/dist/s2/index.d.ts +6 -0
- package/dist/s2/index.js +6 -0
- package/dist/s2/lite.d.ts +26 -0
- package/dist/s2/lite.js +150 -0
- package/dist/s2/projector.d.ts +8 -0
- package/dist/s2/projector.js +28 -0
- package/dist/s2/serialize.d.ts +1 -0
- package/dist/s2/serialize.js +1 -0
- package/dist/s2/snapshot-store.d.ts +17 -0
- package/dist/s2/snapshot-store.js +320 -0
- package/dist/s2/store.d.ts +35 -0
- package/dist/s2/store.js +337 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +63 -0
- package/dist/workflow/definitions-store.d.ts +11 -0
- package/dist/workflow/definitions-store.js +8 -0
- package/dist/workflow/definitions.d.ts +82 -0
- package/dist/workflow/definitions.js +32 -0
- package/dist/workflow/effects.d.ts +305 -0
- package/dist/workflow/effects.js +208 -0
- package/dist/workflow/events.d.ts +60 -0
- package/dist/workflow/events.js +39 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/module.d.ts +926 -0
- package/dist/workflow/module.js +19 -0
- package/dist/workflow/projections.d.ts +30 -0
- package/dist/workflow/projections.js +43 -0
- package/dist/workflow/scope.d.ts +61 -0
- package/dist/workflow/scope.js +7 -0
- package/dist/workflow/threads.d.ts +171 -0
- package/dist/workflow/threads.js +287 -0
- package/package.json +183 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { EventStoreError, EventStoreFencedError, EventStoreTruncationError } from '@swirls/looms/core';
|
|
3
|
+
import { EventStoreTag, EventStoreTrimmedError, type AnyRuntimeModule, type ComposedRegistry, type EventEnvelope, type EventInput, type EventStore, type JsonValue, type ProjectionDefinition, type ReplayStep, type RunState, type SnapshotStore } from '@swirls/looms/core';
|
|
4
|
+
import { type WakeScheduler } from './wake-scheduler';
|
|
5
|
+
export type { RegisteredDefinition } from '@swirls/looms/core';
|
|
6
|
+
export type { WakeScheduler } from './wake-scheduler';
|
|
7
|
+
export { createTimeoutScheduler } from './wake-scheduler';
|
|
8
|
+
declare const DuplicateEffectDispatchError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly _tag: "DuplicateEffectDispatchError";
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
export declare class DuplicateEffectDispatchError extends DuplicateEffectDispatchError_base<{
|
|
12
|
+
readonly runId: string;
|
|
13
|
+
readonly effectId: string;
|
|
14
|
+
readonly threadId: string;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
}> {
|
|
17
|
+
constructor(runId: string, effectId: string, threadId: string);
|
|
18
|
+
}
|
|
19
|
+
declare const MaxWakeIterationsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
20
|
+
readonly _tag: "MaxWakeIterationsError";
|
|
21
|
+
} & Readonly<A>;
|
|
22
|
+
export declare class MaxWakeIterationsError extends MaxWakeIterationsError_base<{
|
|
23
|
+
readonly runId: string;
|
|
24
|
+
readonly maxIterations: number;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}> {
|
|
27
|
+
constructor(runId: string, maxIterations: number);
|
|
28
|
+
}
|
|
29
|
+
export type WakeError = DuplicateEffectDispatchError | MaxWakeIterationsError | EventStoreTruncationError | EventStoreTrimmedError | EventStoreFencedError | EventStoreError | Error;
|
|
30
|
+
export interface CreateRuntimeOptions<TModules extends readonly AnyRuntimeModule[] = readonly AnyRuntimeModule[]> {
|
|
31
|
+
readonly modules: TModules;
|
|
32
|
+
readonly store?: EventStore;
|
|
33
|
+
/**
|
|
34
|
+
* Durable events between mid-wake snapshots. Default 200; `0` disables.
|
|
35
|
+
* A snapshot is always taken when a wake parks, so this only bounds replay
|
|
36
|
+
* after a crash inside one long wake.
|
|
37
|
+
*/
|
|
38
|
+
readonly snapshotEvery?: number;
|
|
39
|
+
readonly maxWakeIterations?: number;
|
|
40
|
+
/** Defaults to the EventStore's attached store or an in-memory SnapshotStore. */
|
|
41
|
+
readonly snapshotStore?: SnapshotStore;
|
|
42
|
+
/** Trim the event log behind the oldest kept snapshot. Also sets how many snapshots to keep. */
|
|
43
|
+
readonly trimAfterSnapshot?: {
|
|
44
|
+
keepSnapshots: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Cursors to keep in process between calls. Default `0`: every call starts
|
|
48
|
+
* from the store (snapshot + delta). A cached cursor is validated against
|
|
49
|
+
* `store.tail` before use.
|
|
50
|
+
*/
|
|
51
|
+
readonly runCacheSize?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Pluggable wake scheduler for timer waits.
|
|
54
|
+
* Defaults to an in-process setTimeout scheduler.
|
|
55
|
+
*/
|
|
56
|
+
readonly scheduler?: WakeScheduler;
|
|
57
|
+
}
|
|
58
|
+
export interface StartRunArgs {
|
|
59
|
+
kind: string;
|
|
60
|
+
definitionName: string;
|
|
61
|
+
input?: JsonValue;
|
|
62
|
+
runId?: string;
|
|
63
|
+
threadId?: string;
|
|
64
|
+
idempotencyKey?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface StartResult {
|
|
67
|
+
runId: string;
|
|
68
|
+
threadId: string;
|
|
69
|
+
state: RunState;
|
|
70
|
+
}
|
|
71
|
+
export interface LoomsRuntime<TModules extends readonly AnyRuntimeModule[] = readonly AnyRuntimeModule[]> {
|
|
72
|
+
readonly modules: TModules;
|
|
73
|
+
readonly registry: ComposedRegistry;
|
|
74
|
+
startRun(args: StartRunArgs): Effect.Effect<StartResult, Error | EventStoreError, EventStoreTag>;
|
|
75
|
+
signal(runId: string, events: ReadonlyArray<EventInput>, options?: {
|
|
76
|
+
idempotencyKey?: string;
|
|
77
|
+
}): Effect.Effect<RunState, Error | EventStoreError, EventStoreTag>;
|
|
78
|
+
wake(runId: string): Effect.Effect<RunState, WakeError, EventStoreTag>;
|
|
79
|
+
getRun(runId: string): Effect.Effect<RunState, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
|
|
80
|
+
getEvents(runId: string, options?: {
|
|
81
|
+
fromSeq?: number;
|
|
82
|
+
limit?: number;
|
|
83
|
+
}): Effect.Effect<EventEnvelope[], EventStoreError, EventStoreTag>;
|
|
84
|
+
project<S>(runId: string, definition: ProjectionDefinition<S>): Effect.Effect<S, EventStoreError, EventStoreTag>;
|
|
85
|
+
replayTo(runId: string, seq: number): Effect.Effect<ReplayStep | null, EventStoreError, EventStoreTag>;
|
|
86
|
+
cancel(runId: string, threadId?: string): Effect.Effect<RunState, Error | EventStoreError, EventStoreTag>;
|
|
87
|
+
listRuns(): Effect.Effect<string[], EventStoreError, EventStoreTag>;
|
|
88
|
+
rescanTimers(): Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
|
|
89
|
+
dispose(): void;
|
|
90
|
+
}
|
|
91
|
+
export declare function createRuntime<const TModules extends readonly AnyRuntimeModule[]>(options: CreateRuntimeOptions<TModules>): LoomsRuntime<TModules>;
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { Data, Effect, Option, Predicate } from 'effect';
|
|
2
|
+
import { advanceCursor, assignSequences, composeModules, createCoalescingAppender, createEvent, createThreadId, createRunId, DEFAULT_SNAPSHOT_EVERY, emptyCursor, EventStoreTag, EventStoreTrimmedError, foldRun, hashRunState, isRunTerminal, isWaitOnTimer, project, replayTo, validateEventInput, withdrawnError, } from '@swirls/looms/core';
|
|
3
|
+
import { dispatchEffect } from './dispatch-effect.js';
|
|
4
|
+
import { createEffectFailedEvent, createLiveEvent, groupOutstanding, materializeEffectOutcome, moduleServices, resolveSnapshotStore, RunCursorCache, stripSeq, synthesizedThreadFailed, threadStartedEvents, validateAndCreateEvents, waitSatisfiedEvents, } from './runtime-helpers.js';
|
|
5
|
+
import { createTimeoutScheduler, toEffectVoid } from './wake-scheduler.js';
|
|
6
|
+
export { createTimeoutScheduler } from './wake-scheduler.js';
|
|
7
|
+
export class DuplicateEffectDispatchError extends Data.TaggedError('DuplicateEffectDispatchError') {
|
|
8
|
+
constructor(runId, effectId, threadId) {
|
|
9
|
+
super({
|
|
10
|
+
runId,
|
|
11
|
+
effectId,
|
|
12
|
+
threadId,
|
|
13
|
+
message: `Duplicate effect dispatch detected for effectId "${effectId}" on thread "${threadId}"; refusing to re-dispatch already executed effect`,
|
|
14
|
+
});
|
|
15
|
+
this.name = 'DuplicateEffectDispatchError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class MaxWakeIterationsError extends Data.TaggedError('MaxWakeIterationsError') {
|
|
19
|
+
constructor(runId, maxIterations) {
|
|
20
|
+
super({
|
|
21
|
+
runId,
|
|
22
|
+
maxIterations,
|
|
23
|
+
message: `Max wake iterations exceeded (${maxIterations}) for run "${runId}"; possible infinite loop`,
|
|
24
|
+
});
|
|
25
|
+
this.name = 'MaxWakeIterationsError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const DEFAULT_RUN_CACHE_SIZE = 0;
|
|
29
|
+
const DEFAULT_KEEP_SNAPSHOTS = 1;
|
|
30
|
+
export function createRuntime(options) {
|
|
31
|
+
const registry = composeModules(options.modules);
|
|
32
|
+
const registeredDefinitions = registry.definitions;
|
|
33
|
+
const definitions = new Map(registeredDefinitions.map((def) => [`${def.kind}:${def.name}`, def]));
|
|
34
|
+
const services = moduleServices(options.modules);
|
|
35
|
+
const waking = new Set();
|
|
36
|
+
/** Coalesce wake requests that arrive while a wake is in flight for the same run. */
|
|
37
|
+
const wakeAgain = new Set();
|
|
38
|
+
const snapshotEvery = options.snapshotEvery ?? DEFAULT_SNAPSHOT_EVERY;
|
|
39
|
+
const maxWakeIterations = options.maxWakeIterations ?? 100;
|
|
40
|
+
const snapshotStore = resolveSnapshotStore(options.snapshotStore, options.store);
|
|
41
|
+
const trimAfterSnapshot = options.trimAfterSnapshot;
|
|
42
|
+
const keepSnapshots = trimAfterSnapshot?.keepSnapshots ?? DEFAULT_KEEP_SNAPSHOTS;
|
|
43
|
+
const runCache = new RunCursorCache(options.runCacheSize ?? DEFAULT_RUN_CACHE_SIZE);
|
|
44
|
+
let liveStore = options.store;
|
|
45
|
+
const liveCount = new Map();
|
|
46
|
+
const liveAppends = createCoalescingAppender((runId, batch) => {
|
|
47
|
+
const store = liveStore;
|
|
48
|
+
if (!store) {
|
|
49
|
+
return Effect.fail(new Error('Cannot append live events without a store'));
|
|
50
|
+
}
|
|
51
|
+
return store.append(runId, batch).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
52
|
+
liveCount.set(runId, (liveCount.get(runId) ?? 0) + result.sequences.length);
|
|
53
|
+
})), Effect.asVoid);
|
|
54
|
+
});
|
|
55
|
+
const readRemaining = (store, runId, events, expectedTail) => Effect.gen(function* () {
|
|
56
|
+
let collected = events;
|
|
57
|
+
while (collected.length > 0) {
|
|
58
|
+
const lastSeq = collected[collected.length - 1].seq;
|
|
59
|
+
const target = expectedTail ?? (yield* store.tail(runId));
|
|
60
|
+
if (lastSeq >= target) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const more = yield* store.read(runId, {
|
|
64
|
+
fromSeq: lastSeq + 1,
|
|
65
|
+
limit: expectedTail !== undefined ? Math.max(0, target - lastSeq) : undefined,
|
|
66
|
+
});
|
|
67
|
+
if (more.length === 0 || more[0].seq <= lastSeq) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
collected = [...collected, ...more];
|
|
71
|
+
}
|
|
72
|
+
return collected;
|
|
73
|
+
});
|
|
74
|
+
const catchUp = (store, runId, cursor, expectedTail) => Effect.gen(function* () {
|
|
75
|
+
if (expectedTail !== undefined && cursor.seq === expectedTail) {
|
|
76
|
+
return cursor;
|
|
77
|
+
}
|
|
78
|
+
const fromSeq = cursor.seq + 1;
|
|
79
|
+
const limit = expectedTail !== undefined ? Math.max(0, expectedTail - cursor.seq) : undefined;
|
|
80
|
+
let delta = yield* store.read(runId, { fromSeq, limit });
|
|
81
|
+
if (delta.length === 0) {
|
|
82
|
+
return cursor;
|
|
83
|
+
}
|
|
84
|
+
const firstSeq = delta[0].seq;
|
|
85
|
+
if (firstSeq > fromSeq) {
|
|
86
|
+
if (store.bounds) {
|
|
87
|
+
const bounds = yield* store.bounds(runId);
|
|
88
|
+
if (bounds.head > fromSeq) {
|
|
89
|
+
return yield* Effect.fail(new EventStoreTrimmedError(runId, bounds.head, fromSeq));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return yield* Effect.fail(new EventStoreTrimmedError(runId, firstSeq, fromSeq));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
delta = yield* readRemaining(store, runId, delta, expectedTail);
|
|
97
|
+
return advanceCursor(cursor, delta, registry);
|
|
98
|
+
});
|
|
99
|
+
const loadCursorCold = (store, runId) => Effect.gen(function* () {
|
|
100
|
+
let cursor = emptyCursor(runId);
|
|
101
|
+
if (snapshotStore) {
|
|
102
|
+
const snap = yield* snapshotStore
|
|
103
|
+
.loadLatest(runId)
|
|
104
|
+
.pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
105
|
+
if (Option.isSome(snap)) {
|
|
106
|
+
cursor = {
|
|
107
|
+
state: snap.value.state,
|
|
108
|
+
seq: snap.value.cursor,
|
|
109
|
+
durableSinceSnapshot: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const fromSeq = cursor.seq + 1;
|
|
114
|
+
let delta = yield* store.read(runId, { fromSeq });
|
|
115
|
+
if (cursor.seq === 0 && delta[0] && delta[0].seq > 1) {
|
|
116
|
+
if (store.bounds) {
|
|
117
|
+
const bounds = yield* store.bounds(runId);
|
|
118
|
+
if (bounds.head > 1) {
|
|
119
|
+
return yield* Effect.fail(new EventStoreTrimmedError(runId, bounds.head, 1));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
return yield* Effect.fail(new EventStoreTrimmedError(runId, delta[0].seq, 1));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (delta.length > 0) {
|
|
127
|
+
delta = yield* readRemaining(store, runId, delta);
|
|
128
|
+
}
|
|
129
|
+
return advanceCursor(cursor, delta, registry);
|
|
130
|
+
});
|
|
131
|
+
const loadCursor = (store, runId) => Effect.gen(function* () {
|
|
132
|
+
const cached = runCache.get(runId);
|
|
133
|
+
if (cached) {
|
|
134
|
+
const tail = yield* store.tail(runId);
|
|
135
|
+
// A tail behind the cursor means the stream was recreated; the cache is poison.
|
|
136
|
+
if (tail < cached.seq) {
|
|
137
|
+
runCache.delete(runId);
|
|
138
|
+
return yield* loadCursorCold(store, runId);
|
|
139
|
+
}
|
|
140
|
+
if (tail === cached.seq) {
|
|
141
|
+
return cached;
|
|
142
|
+
}
|
|
143
|
+
const next = yield* catchUp(store, runId, cached, tail).pipe(Effect.catchTag('EventStoreTrimmedError', () => {
|
|
144
|
+
runCache.delete(runId);
|
|
145
|
+
return loadCursorCold(store, runId);
|
|
146
|
+
}));
|
|
147
|
+
runCache.set(runId, next);
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
const next = yield* loadCursorCold(store, runId);
|
|
151
|
+
runCache.set(runId, next);
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Save the current state externally, keep only `keepSnapshots`, and optionally
|
|
156
|
+
* trim the event log behind the oldest kept snapshot. Failures are swallowed:
|
|
157
|
+
* a snapshot is an optimization, the log stays authoritative.
|
|
158
|
+
*/
|
|
159
|
+
const persistSnapshot = (store, runId, cursor) => Effect.gen(function* () {
|
|
160
|
+
if (cursor.durableSinceSnapshot === 0) {
|
|
161
|
+
return cursor;
|
|
162
|
+
}
|
|
163
|
+
yield* snapshotStore.save({
|
|
164
|
+
runId,
|
|
165
|
+
cursor: cursor.seq,
|
|
166
|
+
stateHash: hashRunState(cursor.state),
|
|
167
|
+
takenAt: Date.now(),
|
|
168
|
+
state: cursor.state,
|
|
169
|
+
});
|
|
170
|
+
if (snapshotStore.prune) {
|
|
171
|
+
yield* snapshotStore.prune(runId, keepSnapshots);
|
|
172
|
+
}
|
|
173
|
+
if (trimAfterSnapshot && store.trim && snapshotStore.listCursors) {
|
|
174
|
+
const cursors = yield* snapshotStore.listCursors(runId);
|
|
175
|
+
const oldestKept = cursors[Math.max(0, cursors.length - keepSnapshots)];
|
|
176
|
+
if (oldestKept !== undefined) {
|
|
177
|
+
yield* store.trim(runId, oldestKept + 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { ...cursor, durableSinceSnapshot: 0 };
|
|
181
|
+
}).pipe(Effect.catch(() => Effect.succeed(cursor)));
|
|
182
|
+
const applyAppend = (store, runId, cursor, batch, appendResult, commandRecords = 0) => Effect.gen(function* () {
|
|
183
|
+
const echo = assignSequences(batch, appendResult);
|
|
184
|
+
const first = echo[0];
|
|
185
|
+
let next = cursor;
|
|
186
|
+
if (first && first.seq !== cursor.seq + 1 + commandRecords) {
|
|
187
|
+
const gap = first.seq - 1 - cursor.seq;
|
|
188
|
+
const live = liveCount.get(runId) ?? 0;
|
|
189
|
+
if (live === gap) {
|
|
190
|
+
next = { ...cursor, seq: first.seq - 1 };
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
next = yield* catchUp(store, runId, cursor, first.seq - 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
next = advanceCursor(next, echo, registry);
|
|
197
|
+
liveCount.set(runId, 0);
|
|
198
|
+
if (next.seq !== appendResult.tail) {
|
|
199
|
+
next = yield* catchUp(store, runId, next, appendResult.tail);
|
|
200
|
+
}
|
|
201
|
+
runCache.set(runId, next);
|
|
202
|
+
return next;
|
|
203
|
+
});
|
|
204
|
+
let scheduler;
|
|
205
|
+
const runtime = {
|
|
206
|
+
modules: options.modules,
|
|
207
|
+
registry,
|
|
208
|
+
listRuns: () => Effect.gen(function* () {
|
|
209
|
+
const store = yield* EventStoreTag;
|
|
210
|
+
return yield* store.listRuns();
|
|
211
|
+
}),
|
|
212
|
+
getEvents: (runId, readOptions) => Effect.gen(function* () {
|
|
213
|
+
const store = yield* EventStoreTag;
|
|
214
|
+
return yield* store.read(runId, readOptions);
|
|
215
|
+
}),
|
|
216
|
+
getRun: (runId) => Effect.gen(function* () {
|
|
217
|
+
const store = yield* EventStoreTag;
|
|
218
|
+
const cursor = yield* loadCursor(store, runId);
|
|
219
|
+
return cursor.state;
|
|
220
|
+
}),
|
|
221
|
+
project: (runId, definition) => Effect.gen(function* () {
|
|
222
|
+
const events = yield* runtime.getEvents(runId);
|
|
223
|
+
return project(definition, events);
|
|
224
|
+
}),
|
|
225
|
+
replayTo: (runId, seq) => Effect.gen(function* () {
|
|
226
|
+
const events = yield* runtime.getEvents(runId);
|
|
227
|
+
return replayTo(events, registry, seq);
|
|
228
|
+
}),
|
|
229
|
+
rescanTimers: () => Effect.gen(function* () {
|
|
230
|
+
const store = yield* EventStoreTag;
|
|
231
|
+
const runIds = yield* store.listRuns();
|
|
232
|
+
let count = 0;
|
|
233
|
+
for (const runId of runIds) {
|
|
234
|
+
const runState = yield* runtime.getRun(runId);
|
|
235
|
+
if (isRunTerminal(runState)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
let earliestTimerAt = null;
|
|
239
|
+
for (const waitRecord of Object.values(runState.waits)) {
|
|
240
|
+
if (isWaitOnTimer(waitRecord.on)) {
|
|
241
|
+
if (earliestTimerAt === null || waitRecord.on.timerAt < earliestTimerAt) {
|
|
242
|
+
earliestTimerAt = waitRecord.on.timerAt;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (earliestTimerAt !== null) {
|
|
247
|
+
yield* toEffectVoid(scheduler.schedule(runId, earliestTimerAt));
|
|
248
|
+
count += 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return count;
|
|
252
|
+
}),
|
|
253
|
+
startRun: (args) => Effect.gen(function* () {
|
|
254
|
+
if (!definitions.get(`${args.kind}:${args.definitionName}`)) {
|
|
255
|
+
return yield* Effect.fail(new Error(`Unknown definition ${args.kind}:${args.definitionName}`));
|
|
256
|
+
}
|
|
257
|
+
const store = yield* EventStoreTag;
|
|
258
|
+
const runId = args.runId ?? createRunId();
|
|
259
|
+
const existing = yield* loadCursor(store, runId);
|
|
260
|
+
if (existing.seq > 0) {
|
|
261
|
+
if ((args.idempotencyKey &&
|
|
262
|
+
existing.state.processedIdempotencyKeys?.includes(args.idempotencyKey)) ||
|
|
263
|
+
existing.state.rootThreadId) {
|
|
264
|
+
return {
|
|
265
|
+
runId,
|
|
266
|
+
threadId: existing.state.rootThreadId ?? '',
|
|
267
|
+
state: existing.state,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const threadId = args.threadId ?? createThreadId();
|
|
272
|
+
const startedEvents = yield* threadStartedEvents(definitions, {
|
|
273
|
+
kind: args.kind,
|
|
274
|
+
definitionName: args.definitionName,
|
|
275
|
+
input: args.input ?? null,
|
|
276
|
+
threadId,
|
|
277
|
+
parentThreadId: null,
|
|
278
|
+
});
|
|
279
|
+
const started = startedEvents[0];
|
|
280
|
+
const startedInput = started && Predicate.isObject(started.payload)
|
|
281
|
+
? (started.payload.input ?? null)
|
|
282
|
+
: (args.input ?? null);
|
|
283
|
+
const batch = yield* validateAndCreateEvents(registry.catalogs, runId, [
|
|
284
|
+
{
|
|
285
|
+
type: 'runtime.run.started',
|
|
286
|
+
payload: {
|
|
287
|
+
rootThreadId: threadId,
|
|
288
|
+
kind: args.kind,
|
|
289
|
+
definitionName: args.definitionName,
|
|
290
|
+
input: startedInput,
|
|
291
|
+
},
|
|
292
|
+
threadId: null,
|
|
293
|
+
idempotencyKey: args.idempotencyKey,
|
|
294
|
+
},
|
|
295
|
+
...startedEvents,
|
|
296
|
+
], { origin: { type: 'system' } });
|
|
297
|
+
yield* store.append(runId, stripSeq(batch));
|
|
298
|
+
const state = yield* runtime.wake(runId);
|
|
299
|
+
return { runId, threadId, state };
|
|
300
|
+
}),
|
|
301
|
+
signal: (runId, events, signalOpts) => Effect.gen(function* () {
|
|
302
|
+
const store = yield* EventStoreTag;
|
|
303
|
+
const current = yield* loadCursor(store, runId);
|
|
304
|
+
const currentState = current.state;
|
|
305
|
+
if (signalOpts?.idempotencyKey &&
|
|
306
|
+
currentState.processedIdempotencyKeys?.includes(signalOpts.idempotencyKey)) {
|
|
307
|
+
return currentState;
|
|
308
|
+
}
|
|
309
|
+
const filteredEvents = events.filter((input) => {
|
|
310
|
+
const key = input.idempotencyKey ?? signalOpts?.idempotencyKey;
|
|
311
|
+
return !key || !currentState.processedIdempotencyKeys?.includes(key);
|
|
312
|
+
});
|
|
313
|
+
if (filteredEvents.length === 0) {
|
|
314
|
+
return currentState;
|
|
315
|
+
}
|
|
316
|
+
const batch = yield* validateAndCreateEvents(registry.catalogs, runId, filteredEvents.map((input) => ({
|
|
317
|
+
...input,
|
|
318
|
+
idempotencyKey: input.idempotencyKey ?? signalOpts?.idempotencyKey,
|
|
319
|
+
})), { origin: { type: 'external' } });
|
|
320
|
+
const folded = foldRun(batch, registry, { runId, initial: currentState });
|
|
321
|
+
const satisfied = waitSatisfiedEvents(folded, batch);
|
|
322
|
+
const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
|
|
323
|
+
yield* store.append(runId, stripSeq([...batch, ...satisfiedEvents]));
|
|
324
|
+
return yield* runtime.wake(runId);
|
|
325
|
+
}),
|
|
326
|
+
cancel: (runId, threadId) => Effect.gen(function* () {
|
|
327
|
+
const store = yield* EventStoreTag;
|
|
328
|
+
const targetThreadId = threadId ?? (yield* loadCursor(store, runId)).state.rootThreadId ?? '';
|
|
329
|
+
return yield* runtime.signal(runId, [
|
|
330
|
+
{
|
|
331
|
+
type: 'runtime.thread.cancelled',
|
|
332
|
+
payload: { threadId: targetThreadId, reason: 'cancelled' },
|
|
333
|
+
threadId: targetThreadId || null,
|
|
334
|
+
origin: { type: 'external' },
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
337
|
+
}),
|
|
338
|
+
wake: (runId) => Effect.gen(function* () {
|
|
339
|
+
// Claim synchronously before any yield so concurrent fibers cannot both pass.
|
|
340
|
+
if (waking.has(runId)) {
|
|
341
|
+
wakeAgain.add(runId);
|
|
342
|
+
return yield* runtime.getRun(runId);
|
|
343
|
+
}
|
|
344
|
+
waking.add(runId);
|
|
345
|
+
const store = yield* EventStoreTag;
|
|
346
|
+
liveStore = store;
|
|
347
|
+
try {
|
|
348
|
+
let finalState;
|
|
349
|
+
do {
|
|
350
|
+
let cursor = yield* loadCursor(store, runId);
|
|
351
|
+
if (isRunTerminal(cursor.state)) {
|
|
352
|
+
finalState = cursor.state;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
liveCount.set(runId, 0);
|
|
356
|
+
const dispatchedEffectIds = new Set();
|
|
357
|
+
let guard = 0;
|
|
358
|
+
while (!isRunTerminal(cursor.state)) {
|
|
359
|
+
if (guard >= maxWakeIterations) {
|
|
360
|
+
return yield* Effect.fail(new MaxWakeIterationsError(runId, maxWakeIterations));
|
|
361
|
+
}
|
|
362
|
+
guard += 1;
|
|
363
|
+
const state = cursor.state;
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const due = Object.values(state.waits).filter((record) => isWaitOnTimer(record.on) && record.on.timerAt <= now + 5);
|
|
366
|
+
if (due.length > 0) {
|
|
367
|
+
const batch = [];
|
|
368
|
+
for (const record of due) {
|
|
369
|
+
if (!isWaitOnTimer(record.on)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
batch.push(createEvent(runId, {
|
|
373
|
+
type: 'runtime.timer.fired',
|
|
374
|
+
payload: { timerId: record.waitId, waitId: record.waitId },
|
|
375
|
+
threadId: record.threadId,
|
|
376
|
+
origin: { type: 'system' },
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
const stateAfterFired = foldRun(batch, registry, { runId, initial: state });
|
|
380
|
+
const satisfied = waitSatisfiedEvents(stateAfterFired, batch);
|
|
381
|
+
const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
|
|
382
|
+
const fullTimerBatch = [...batch, ...satisfiedEvents];
|
|
383
|
+
const appendResult = yield* store.append(runId, stripSeq(fullTimerBatch));
|
|
384
|
+
cursor = yield* applyAppend(store, runId, cursor, stripSeq(fullTimerBatch), appendResult);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const outstanding = state.outstandingEffects;
|
|
388
|
+
if (outstanding.length === 0) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
for (const item of outstanding) {
|
|
392
|
+
if (dispatchedEffectIds.has(item.effectId)) {
|
|
393
|
+
return yield* Effect.fail(new DuplicateEffectDispatchError(runId, item.effectId, item.threadId));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const item of outstanding) {
|
|
397
|
+
dispatchedEffectIds.add(item.effectId);
|
|
398
|
+
}
|
|
399
|
+
const groupKeys = groupOutstanding(outstanding);
|
|
400
|
+
const groupResults = yield* Effect.forEach(groupKeys, (groupKey) => Effect.gen(function* () {
|
|
401
|
+
const group = outstanding.filter((item) => `${item.threadId}:${item.causingSeq}` === groupKey);
|
|
402
|
+
const groupProduced = [];
|
|
403
|
+
let failedEffectId;
|
|
404
|
+
for (const item of group) {
|
|
405
|
+
if (failedEffectId && item.effect.type === 'runtime.wait') {
|
|
406
|
+
groupProduced.push(createEffectFailedEvent(runId, item, withdrawnError(failedEffectId)));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const appendLive = async (input) => {
|
|
410
|
+
const validated = await Effect.runPromise(validateEventInput(registry.catalogs, input));
|
|
411
|
+
const liveEvent = createLiveEvent(runId, item, validated);
|
|
412
|
+
if (liveEvent) {
|
|
413
|
+
await Effect.runPromise(liveAppends.push(runId, liveEvent));
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const outcomes = yield* dispatchEffect(registry, services, definitions, item.effect, {
|
|
417
|
+
effectId: item.effectId,
|
|
418
|
+
runId,
|
|
419
|
+
threadId: item.threadId,
|
|
420
|
+
causingEventId: item.causingEventId,
|
|
421
|
+
emit: appendLive,
|
|
422
|
+
});
|
|
423
|
+
const before = groupProduced.length;
|
|
424
|
+
if (outcomes.length === 0) {
|
|
425
|
+
groupProduced.push(createEffectFailedEvent(runId, item, 'empty-outcome'));
|
|
426
|
+
}
|
|
427
|
+
for (const input of outcomes) {
|
|
428
|
+
groupProduced.push(yield* materializeEffectOutcome(registry.catalogs, runId, item, input));
|
|
429
|
+
}
|
|
430
|
+
const groupOutcomes = groupProduced.slice(before);
|
|
431
|
+
if (item.effect.type !== 'runtime.wait' &&
|
|
432
|
+
groupOutcomes.some((event) => event.type === 'runtime.effect.failed')) {
|
|
433
|
+
failedEffectId = item.effectId;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return groupProduced;
|
|
437
|
+
}), { concurrency: 'unbounded' });
|
|
438
|
+
const produced = groupResults.flat();
|
|
439
|
+
if (produced.length === 0) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
yield* liveAppends.drain(runId);
|
|
443
|
+
const beforeFold = cursor.state;
|
|
444
|
+
const intermediateState = foldRun(produced, registry, { runId, initial: beforeFold });
|
|
445
|
+
const synthesized = synthesizedThreadFailed(beforeFold, intermediateState, produced);
|
|
446
|
+
const synthesizedEvents = synthesized.map((input) => createEvent(runId, input));
|
|
447
|
+
const stateAfterSynth = synthesizedEvents.length > 0
|
|
448
|
+
? foldRun(synthesizedEvents, registry, { runId, initial: intermediateState })
|
|
449
|
+
: intermediateState;
|
|
450
|
+
const allNewEvents = [...produced, ...synthesizedEvents];
|
|
451
|
+
const satisfied = waitSatisfiedEvents(stateAfterSynth, allNewEvents);
|
|
452
|
+
const satisfiedEvents = satisfied.map((input) => createEvent(runId, input));
|
|
453
|
+
const fullBatch = [...allNewEvents, ...satisfiedEvents];
|
|
454
|
+
const appendResult = yield* store.append(runId, stripSeq(fullBatch));
|
|
455
|
+
cursor = yield* applyAppend(store, runId, cursor, stripSeq(fullBatch), appendResult);
|
|
456
|
+
// Bound replay after a crash inside a long wake.
|
|
457
|
+
if (snapshotEvery > 0 && cursor.durableSinceSnapshot >= snapshotEvery) {
|
|
458
|
+
cursor = yield* persistSnapshot(store, runId, cursor);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const root = cursor.state.rootThreadId
|
|
462
|
+
? cursor.state.threads[cursor.state.rootThreadId]
|
|
463
|
+
: undefined;
|
|
464
|
+
if (root &&
|
|
465
|
+
(root.status === 'completed' ||
|
|
466
|
+
root.status === 'failed' ||
|
|
467
|
+
root.status === 'cancelled') &&
|
|
468
|
+
!isRunTerminal(cursor.state)) {
|
|
469
|
+
const completed = createEvent(runId, {
|
|
470
|
+
type: 'runtime.run.completed',
|
|
471
|
+
payload: {
|
|
472
|
+
output: root.output,
|
|
473
|
+
error: root.error,
|
|
474
|
+
},
|
|
475
|
+
threadId: null,
|
|
476
|
+
origin: { type: 'system' },
|
|
477
|
+
});
|
|
478
|
+
const appendResult = yield* store.append(runId, stripSeq([completed]));
|
|
479
|
+
cursor = yield* applyAppend(store, runId, cursor, stripSeq([completed]), appendResult);
|
|
480
|
+
}
|
|
481
|
+
// Snapshot on park: the next wake (on any instance) starts from here.
|
|
482
|
+
cursor = yield* persistSnapshot(store, runId, cursor);
|
|
483
|
+
const pendingTimers = Object.values(cursor.state.waits).filter((record) => isWaitOnTimer(record.on) && record.on.timerAt > Date.now());
|
|
484
|
+
if (pendingTimers.length > 0 && !isRunTerminal(cursor.state)) {
|
|
485
|
+
// SAFETY: pendingTimers is filtered by isWaitOnTimer above.
|
|
486
|
+
const earliestTimerAt = Math.min(...pendingTimers.map((r) => r.on.timerAt));
|
|
487
|
+
yield* toEffectVoid(scheduler.schedule(runId, earliestTimerAt));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
yield* toEffectVoid(scheduler.cancel(runId));
|
|
491
|
+
}
|
|
492
|
+
if (isRunTerminal(cursor.state)) {
|
|
493
|
+
runCache.delete(runId);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
runCache.set(runId, cursor);
|
|
497
|
+
}
|
|
498
|
+
finalState = cursor.state;
|
|
499
|
+
} while (wakeAgain.delete(runId));
|
|
500
|
+
// Lost race: a waiter queued after the drain loop but before unlock.
|
|
501
|
+
if (wakeAgain.has(runId)) {
|
|
502
|
+
waking.delete(runId);
|
|
503
|
+
liveAppends.clear(runId);
|
|
504
|
+
liveCount.delete(runId);
|
|
505
|
+
return yield* runtime.wake(runId);
|
|
506
|
+
}
|
|
507
|
+
return finalState;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
if (waking.has(runId)) {
|
|
511
|
+
waking.delete(runId);
|
|
512
|
+
}
|
|
513
|
+
liveAppends.clear(runId);
|
|
514
|
+
liveCount.delete(runId);
|
|
515
|
+
}
|
|
516
|
+
}),
|
|
517
|
+
dispose: () => {
|
|
518
|
+
scheduler.dispose?.();
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
scheduler =
|
|
522
|
+
options.scheduler ??
|
|
523
|
+
createTimeoutScheduler((runId) => {
|
|
524
|
+
if (options.store) {
|
|
525
|
+
Effect.runPromise(Effect.provideService(runtime.wake(runId), EventStoreTag, options.store)).catch((err) => {
|
|
526
|
+
console.error('[timer wake error]', err);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return runtime;
|
|
531
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type EventStore } from '@swirls/looms/core';
|
|
2
|
+
import type { LoomsRuntime } from './runtime';
|
|
3
|
+
export interface FetchHandlerOptions {
|
|
4
|
+
runtime: LoomsRuntime;
|
|
5
|
+
store: EventStore;
|
|
6
|
+
}
|
|
7
|
+
export interface RunningServer {
|
|
8
|
+
port: number;
|
|
9
|
+
stop: () => void;
|
|
10
|
+
}
|
|
11
|
+
export type LoomsFetchResult = Response | null;
|
|
12
|
+
export declare function isLoomsApiPath(path: string): boolean;
|
|
13
|
+
export declare function createFetchHandler(options: FetchHandlerOptions): (req: Request) => Promise<Response | null>;
|
|
14
|
+
export declare function serveHttp(fetchHandler: (req: Request) => Promise<Response | null>, options?: {
|
|
15
|
+
port?: number;
|
|
16
|
+
hostname?: string;
|
|
17
|
+
}): RunningServer;
|