@swirls/looms 0.1.0 → 0.3.0
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/CHANGELOG.md +37 -0
- package/dist/actor/host.js +4 -5
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +5 -0
- package/dist/actor/routing.js +14 -0
- package/dist/agent/agent-session.js +1 -0
- package/dist/agent/debugger/agent-chat.d.ts +2 -0
- package/dist/agent/debugger/agent-chat.js +210 -0
- package/dist/agent/debugger/index.d.ts +5 -0
- package/dist/agent/debugger/index.js +19 -0
- package/dist/agent/debugger/summarize.d.ts +2 -0
- package/dist/agent/debugger/summarize.js +53 -0
- package/dist/agent/debugger/token-usage.d.ts +3 -0
- package/dist/agent/debugger/token-usage.js +11 -0
- package/dist/agent/definitions.d.ts +3 -11
- package/dist/agent/definitions.js +5 -6
- package/dist/agent/effects.d.ts +30 -0
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/events.js +2 -0
- package/dist/agent/llm.js +1 -8
- package/dist/agent/module.d.ts +604 -534
- package/dist/agent/module.js +11 -7
- package/dist/agent/projections.d.ts +5 -0
- package/dist/agent/projections.js +43 -2
- package/dist/agent/scope.d.ts +2 -0
- package/dist/agent/signals.d.ts +7 -0
- package/dist/agent/signals.js +25 -16
- package/dist/agent/threads.js +35 -21
- package/dist/agent/tool-schema.d.ts +1 -17
- package/dist/agent/tool-schema.js +3 -42
- package/dist/agent/transcript.d.ts +7 -0
- package/dist/agent/transcript.js +19 -0
- package/dist/agent/types.d.ts +10 -0
- package/dist/agent/types.js +4 -0
- package/dist/ai-vercel/index.d.ts +1 -1
- package/dist/ai-vercel/vercel-evaluator.d.ts +21 -8
- package/dist/ai-vercel/vercel-evaluator.js +7 -2
- package/dist/approval/debugger/approvals-view.d.ts +3 -0
- package/dist/approval/debugger/approvals-view.js +18 -0
- package/dist/approval/debugger/index.d.ts +4 -0
- package/dist/approval/debugger/index.js +11 -0
- package/dist/approval/debugger/summarize.d.ts +2 -0
- package/dist/approval/debugger/summarize.js +17 -0
- package/dist/approval/effects.d.ts +12 -0
- package/dist/approval/module.d.ts +24 -0
- package/dist/client/client.d.ts +310 -0
- package/dist/client/client.js +20 -2
- package/dist/cloudflare/router.d.ts +1 -1
- package/dist/cloudflare/router.js +6 -8
- package/dist/core/compose.js +1 -0
- package/dist/core/debugger-host.d.ts +16 -0
- package/dist/core/debugger-host.js +1 -0
- package/dist/core/effects.d.ts +7 -0
- package/dist/core/effects.js +1 -0
- package/dist/core/fold.js +12 -6
- package/dist/core/fork.d.ts +35 -0
- package/dist/core/fork.js +129 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -0
- package/dist/core/module-scope.d.ts +1 -0
- package/dist/core/module.d.ts +4 -2
- package/dist/core/projection.js +1 -0
- package/dist/core/protocol.d.ts +10 -1
- package/dist/core/protocol.js +11 -0
- package/dist/core/published.d.ts +43 -0
- package/dist/core/published.js +44 -0
- package/dist/core/replay.d.ts +8 -0
- package/dist/core/schema.d.ts +3 -1
- package/dist/core/schema.js +38 -0
- package/dist/core/snapshot-store.d.ts +4 -0
- package/dist/core/snapshot-store.js +7 -0
- package/dist/core/snapshots.js +3 -17
- package/dist/core/sqlite-store.js +39 -0
- package/dist/core/state.d.ts +14 -0
- package/dist/core/state.js +25 -2
- package/dist/core/store.d.ts +1 -0
- package/dist/debugger/app/assets/index-BevQYa-U.js +22 -0
- package/dist/debugger/app/assets/index-CgDzWJg7.css +2 -0
- package/dist/debugger/app/assets/rolldown-runtime-CbXtAM7H.js +1 -0
- package/dist/debugger/app/assets/routes-B17Om93i.js +47 -0
- package/dist/debugger/app/index.html +14 -0
- package/dist/debugger/components/composer.d.ts +20 -0
- package/dist/debugger/components/composer.js +26 -0
- package/dist/debugger/components/debugger-split.d.ts +2 -1
- package/dist/debugger/components/debugger-split.js +2 -2
- package/dist/debugger/components/event-inspector.d.ts +2 -1
- package/dist/debugger/components/event-inspector.js +25 -16
- package/dist/debugger/components/event-stream.d.ts +2 -1
- package/dist/debugger/components/event-stream.js +12 -71
- package/dist/debugger/components/follow-list.d.ts +19 -0
- package/dist/debugger/components/follow-list.js +96 -0
- package/dist/debugger/components/json-tree.d.ts +11 -0
- package/dist/debugger/components/json-tree.js +120 -0
- package/dist/debugger/components/json-view.js +2 -2
- package/dist/debugger/components/run-tree.js +50 -20
- package/dist/debugger/components/status-dot.d.ts +4 -0
- package/dist/debugger/components/status-dot.js +6 -2
- package/dist/debugger/context.d.ts +14 -0
- package/dist/debugger/context.js +17 -0
- package/dist/debugger/contracts.d.ts +7 -4
- package/dist/debugger/events/builtin-families.d.ts +3 -0
- package/dist/debugger/events/builtin-families.js +9 -0
- package/dist/debugger/events/catalog.d.ts +3 -4
- package/dist/debugger/events/catalog.js +25 -17
- package/dist/debugger/events/protocol-summarize.d.ts +1 -2
- package/dist/debugger/events/protocol-summarize.js +36 -37
- package/dist/debugger/index.d.ts +20 -6
- package/dist/debugger/index.js +19 -5
- package/dist/debugger/lib/cn.d.ts +1 -0
- package/dist/debugger/lib/cn.js +3 -0
- package/dist/debugger/lib/event-counts.d.ts +2 -2
- package/dist/debugger/lib/event-counts.js +5 -9
- package/dist/debugger/lib/event-meta.d.ts +2 -3
- package/dist/debugger/lib/event-meta.js +6 -11
- package/dist/debugger/lib/json.d.ts +25 -0
- package/dist/debugger/lib/json.js +87 -0
- package/dist/debugger/lib/payload.d.ts +7 -0
- package/dist/debugger/lib/payload.js +11 -0
- package/dist/debugger/lib/time.d.ts +3 -0
- package/dist/debugger/lib/time.js +24 -0
- package/dist/debugger/plugin.d.ts +36 -0
- package/dist/debugger/plugin.js +14 -0
- package/dist/debugger/server/index.d.ts +14 -0
- package/dist/debugger/server/index.js +114 -0
- package/dist/debugger/shell/debugger-shell.d.ts +15 -0
- package/dist/debugger/shell/debugger-shell.js +43 -0
- package/dist/debugger/shell/definition-key.d.ts +3 -0
- package/dist/debugger/shell/definition-key.js +4 -0
- package/dist/debugger/shell/events-pane.d.ts +12 -0
- package/dist/debugger/shell/events-pane.js +25 -0
- package/dist/debugger/shell/projections-panel.d.ts +4 -0
- package/dist/debugger/shell/projections-panel.js +59 -0
- package/dist/debugger/shell/run-controls.d.ts +5 -0
- package/dist/debugger/shell/run-controls.js +99 -0
- package/dist/debugger/shell/run-search.d.ts +6 -0
- package/dist/debugger/shell/run-search.js +100 -0
- package/dist/debugger/shell/run-type-menu.d.ts +6 -0
- package/dist/debugger/shell/run-type-menu.js +11 -0
- package/dist/debugger/shell/schema-form.d.ts +31 -0
- package/dist/debugger/shell/schema-form.js +117 -0
- package/dist/debugger/shell/start-form.d.ts +3 -0
- package/dist/debugger/shell/start-form.js +75 -0
- package/dist/debugger/shell/use-start-run.d.ts +14 -0
- package/dist/debugger/shell/use-start-run.js +32 -0
- package/dist/debugger/shell/workspace.d.ts +6 -0
- package/dist/debugger/shell/workspace.js +15 -0
- package/dist/debugger/styles.css +111 -27
- package/dist/debugger/ui/button.d.ts +24 -0
- package/dist/debugger/ui/button.js +24 -0
- package/dist/debugger/ui/command.d.ts +9 -0
- package/dist/debugger/ui/command.js +24 -0
- package/dist/debugger/ui/dropdown-menu.d.ts +13 -0
- package/dist/debugger/ui/dropdown-menu.js +37 -0
- package/dist/debugger/ui/panel.d.ts +14 -0
- package/dist/debugger/ui/panel.js +16 -0
- package/dist/debugger/ui/popover.d.ts +7 -0
- package/dist/debugger/ui/popover.js +17 -0
- package/dist/debugger/ui/resizable.d.ts +7 -0
- package/dist/debugger/ui/resizable.js +14 -0
- package/dist/debugger/ui/scroll-area.d.ts +2 -1
- package/dist/debugger/ui/scroll-area.js +5 -3
- package/dist/debugger/ui/select.d.ts +10 -0
- package/dist/debugger/ui/select.js +21 -0
- package/dist/debugger/ui/tabs.js +2 -2
- package/dist/debugger/ui/textarea.d.ts +3 -0
- package/dist/debugger/ui/textarea.js +7 -0
- package/dist/evaluator/effects.d.ts +12 -0
- package/dist/evaluator/module.d.ts +24 -0
- package/dist/projectors/index-model.d.ts +13 -7
- package/dist/projectors/index-model.js +21 -26
- package/dist/projectors/memory.js +19 -11
- package/dist/projectors/postgres.js +20 -19
- package/dist/projectors/schema.js +20 -18
- package/dist/projectors/sqlite.js +32 -27
- package/dist/react/materialize.js +16 -11
- package/dist/react/react.d.ts +6 -2
- package/dist/react/react.js +5 -2
- package/dist/react/store.d.ts +1 -1
- package/dist/react/store.js +1 -1
- package/dist/react/tables.d.ts +3 -2
- package/dist/runtime/cancellation.js +14 -27
- package/dist/runtime/errors.d.ts +39 -0
- package/dist/runtime/errors.js +32 -0
- package/dist/runtime/helpers.d.ts +2 -0
- package/dist/runtime/helpers.js +23 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/looms.d.ts +13 -1
- package/dist/runtime/looms.js +5 -2
- package/dist/runtime/repository.js +16 -1
- package/dist/runtime/run-start.js +1 -3
- package/dist/runtime/runtime.d.ts +1 -1
- package/dist/runtime/runtime.js +60 -18
- package/dist/runtime/server/auth.d.ts +1 -0
- package/dist/runtime/server/auth.js +5 -0
- package/dist/runtime/server/debugger-host.d.ts +11 -0
- package/dist/runtime/server/debugger-host.js +32 -0
- package/dist/runtime/server/fetch-handler.d.ts +5 -1
- package/dist/runtime/server/fetch-handler.js +24 -13
- package/dist/runtime/server/http.d.ts +3 -0
- package/dist/runtime/server/http.js +21 -2
- package/dist/runtime/server/routes/definitions.d.ts +3 -0
- package/dist/runtime/server/routes/definitions.js +20 -0
- package/dist/runtime/server/routes/runs.js +43 -3
- package/dist/runtime/server.d.ts +3 -1
- package/dist/runtime/server.js +3 -1
- package/dist/runtime/types.d.ts +11 -1
- package/dist/runtime/wake-engine.js +5 -2
- package/dist/s2/snapshot-store.js +45 -7
- package/dist/workflow/debugger/index.d.ts +4 -0
- package/dist/workflow/debugger/index.js +11 -0
- package/dist/workflow/debugger/nodes-view.d.ts +3 -0
- package/dist/workflow/debugger/nodes-view.js +13 -0
- package/dist/workflow/debugger/summarize.d.ts +2 -0
- package/dist/workflow/debugger/summarize.js +30 -0
- package/dist/workflow/effects.d.ts +24 -0
- package/dist/workflow/effects.js +1 -0
- package/dist/workflow/module.d.ts +48 -0
- package/package.json +20 -1
- package/dist/debugger/events/domain-summarize.d.ts +0 -3
- package/dist/debugger/events/domain-summarize.js +0 -107
- package/dist/debugger/events/family.d.ts +0 -8
- package/dist/debugger/events/family.js +0 -44
package/dist/runtime/looms.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Data, Effect, Predicate } from 'effect';
|
|
2
2
|
import { EventStoreTag, makeMemoryEventStore, snapshotStoreOf, SnapshotStoreTag, } from '@swirls/looms/core';
|
|
3
3
|
import { createRuntime, } from './runtime.js';
|
|
4
|
-
import { createFetchHandler,
|
|
4
|
+
import { createFetchHandler, handlesPath, mountDebugger, serveHttp, } from './server.js';
|
|
5
5
|
class LoomsInitializationError extends Data.TaggedError('LoomsInitializationError') {
|
|
6
6
|
constructor(cause) {
|
|
7
7
|
super({
|
|
@@ -13,6 +13,7 @@ class LoomsInitializationError extends Data.TaggedError('LoomsInitializationErro
|
|
|
13
13
|
}
|
|
14
14
|
export function createLooms(options = {}) {
|
|
15
15
|
const modules = options.modules ?? [];
|
|
16
|
+
const debuggerMount = options.debugger ? mountDebugger(options.debugger) : undefined;
|
|
16
17
|
let initPromise;
|
|
17
18
|
let runningServer;
|
|
18
19
|
const getInit = () => {
|
|
@@ -52,6 +53,7 @@ export function createLooms(options = {}) {
|
|
|
52
53
|
runtime,
|
|
53
54
|
store,
|
|
54
55
|
authorize: options.authorize,
|
|
56
|
+
debugger: debuggerMount,
|
|
55
57
|
});
|
|
56
58
|
return { runtime, store, snapshotStore: snapshotStoreOf(store), fetchHandler };
|
|
57
59
|
}));
|
|
@@ -96,9 +98,10 @@ export function createLooms(options = {}) {
|
|
|
96
98
|
project: (runId, definition) => runEffect((i) => i.runtime.project(runId, definition)),
|
|
97
99
|
replayTo: (runId, seq) => runEffect((i) => i.runtime.replayTo(runId, seq)),
|
|
98
100
|
cancel: (runId, threadId) => runEffect((i) => i.runtime.cancel(runId, threadId)),
|
|
101
|
+
fork: (sourceRunId, forkOpts) => runEffect((i) => i.runtime.fork(sourceRunId, forkOpts)),
|
|
99
102
|
workerCallback: (runId, input) => runEffect((i) => i.runtime.workerCallback(runId, input)),
|
|
100
103
|
fetch: (req) => {
|
|
101
|
-
if (!
|
|
104
|
+
if (!handlesPath(new URL(req.url).pathname, debuggerMount)) {
|
|
102
105
|
return Promise.resolve(null);
|
|
103
106
|
}
|
|
104
107
|
return getInit().then(({ fetchHandler }) => fetchHandler(req));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Clock, Data, Effect, Option } from 'effect';
|
|
2
|
-
import { advanceCursor, assignSequences, createCoalescingAppender, emptyCursor, EventStoreTrimmedError, hashRunState, trimEventStoreSafely, } from '@swirls/looms/core';
|
|
2
|
+
import { advanceCursor, assignSequences, createCoalescingAppender, emptyCursor, EventStoreTrimmedError, hashRunState, summarizeRun, trimEventStoreSafely, } from '@swirls/looms/core';
|
|
3
3
|
import { RuntimeExecutionError } from './errors.js';
|
|
4
4
|
import { stripSeq } from './helpers.js';
|
|
5
5
|
import { notifyObserver } from './observer.js';
|
|
@@ -36,7 +36,18 @@ function readRemaining(store, runId, events, expectedTail) {
|
|
|
36
36
|
}
|
|
37
37
|
export function createRuntimeRepository(options) {
|
|
38
38
|
const { registry, snapshotStore, trimAfterSnapshot, keepSnapshots, runCache, executionContexts } = options;
|
|
39
|
+
const headerFingerprints = new Map();
|
|
39
40
|
const observe = (event) => notifyObserver(options.observer, event);
|
|
41
|
+
const rememberHeader = (state) => {
|
|
42
|
+
const header = summarizeRun(state);
|
|
43
|
+
const fingerprint = `${header.status}\0${header.kind ?? ''}\0${header.definitionName ?? ''}`;
|
|
44
|
+
if (headerFingerprints.get(header.runId) === fingerprint) {
|
|
45
|
+
return Effect.void;
|
|
46
|
+
}
|
|
47
|
+
return snapshotStore.saveHeader(header).pipe(Effect.tap(() => Effect.sync(() => {
|
|
48
|
+
headerFingerprints.set(header.runId, fingerprint);
|
|
49
|
+
})), Effect.catch((error) => Effect.logWarning(`Run header persistence failed for "${header.runId}"`, error)));
|
|
50
|
+
};
|
|
40
51
|
const appendObserved = (store, runId, events, appendOptions) => Effect.suspend(() => {
|
|
41
52
|
const startedAt = currentTimeMillis();
|
|
42
53
|
return store.append(runId, events, appendOptions).pipe(Effect.tap(() => Effect.sync(() => observe({
|
|
@@ -156,6 +167,7 @@ export function createRuntimeRepository(options) {
|
|
|
156
167
|
return yield* loadCursorCold(store, runId);
|
|
157
168
|
}
|
|
158
169
|
if (tail === cached.seq) {
|
|
170
|
+
yield* rememberHeader(cached.state);
|
|
159
171
|
return cached;
|
|
160
172
|
}
|
|
161
173
|
const next = yield* catchUp(store, runId, cached, tail).pipe(Effect.catchTag('EventStoreTrimmedError', () => {
|
|
@@ -163,10 +175,12 @@ export function createRuntimeRepository(options) {
|
|
|
163
175
|
return loadCursorCold(store, runId);
|
|
164
176
|
}));
|
|
165
177
|
runCache.set(runId, next);
|
|
178
|
+
yield* rememberHeader(next.state);
|
|
166
179
|
return next;
|
|
167
180
|
}
|
|
168
181
|
const next = yield* loadCursorCold(store, runId);
|
|
169
182
|
runCache.set(runId, next);
|
|
183
|
+
yield* rememberHeader(next.state);
|
|
170
184
|
return next;
|
|
171
185
|
});
|
|
172
186
|
/**
|
|
@@ -241,6 +255,7 @@ export function createRuntimeRepository(options) {
|
|
|
241
255
|
next = yield* catchUp(store, runId, next, appendResult.tail);
|
|
242
256
|
}
|
|
243
257
|
runCache.set(runId, next);
|
|
258
|
+
yield* rememberHeader(next.state);
|
|
244
259
|
return next;
|
|
245
260
|
});
|
|
246
261
|
/**
|
|
@@ -5,9 +5,7 @@ function canonicalJson(value) {
|
|
|
5
5
|
return `[${value.map(canonicalJson).join(',')}]`;
|
|
6
6
|
}
|
|
7
7
|
if (Predicate.isObject(value)) {
|
|
8
|
-
const keys = Object.keys(value);
|
|
9
|
-
// oxlint-disable-next-line unicorn/no-array-sort
|
|
10
|
-
keys.sort();
|
|
8
|
+
const keys = Object.keys(value).toSorted();
|
|
11
9
|
return `{${keys
|
|
12
10
|
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key] ?? null)}`)
|
|
13
11
|
.join(',')}}`;
|
|
@@ -2,7 +2,7 @@ import { type AnyRuntimeModule } from '@swirls/looms/core';
|
|
|
2
2
|
export type { RegisteredDefinition } from '@swirls/looms/core';
|
|
3
3
|
export type { WakeScheduler } from './wake-scheduler';
|
|
4
4
|
export { createTimeoutScheduler } from './wake-scheduler';
|
|
5
|
-
export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors';
|
|
5
|
+
export { DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunTerminalError, StartRunConflictError, } from './errors';
|
|
6
6
|
export { RunOverloadedError } from './ingress';
|
|
7
7
|
export type { CreateRuntimeOptions, EffectWorker, EffectWorkerTask, LoomsRuntime, RunOperationalStatus, StartResult, StartRunArgs, WakeError, } from './types';
|
|
8
8
|
export type { WorkerCallback, WorkerCallbackInput } from '@swirls/looms/core';
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Clock, Effect, ManagedRuntime, Predicate } from 'effect';
|
|
2
|
-
import { composeModules, createEvent, createThreadId, createRunId, DEFAULT_DEFINITION_VERSION, DEFAULT_SNAPSHOT_EVERY, definitionKey, EventStoreTag, foldRun, project, replayTo, } from '@swirls/looms/core';
|
|
2
|
+
import { composeModules, createEvent, createThreadId, createRunId, DEFAULT_DEFINITION_VERSION, DEFAULT_SNAPSHOT_EVERY, definitionKey, descendantThreadIds, EventStoreTag, foldRun, isRunTerminal, planFork, project, replayTo, summarizeRun, } from '@swirls/looms/core';
|
|
3
3
|
import { createCancellation } from './cancellation.js';
|
|
4
4
|
import { nextRuntimeDeadline } from './deadline-transition-planner.js';
|
|
5
|
-
import { RuntimeExecutionError } from './errors.js';
|
|
5
|
+
import { ForkConflictError, RunNotFinishedError, RunNotFoundError, RunTerminalError, RuntimeExecutionError, } from './errors.js';
|
|
6
6
|
import { admitExternalEvents, moduleServices, resolveSnapshotStore, RunCursorCache, threadStartedEvents, UnknownDefinitionError, validateAndCreateEvents, waitSatisfiedEvents, } from './helpers.js';
|
|
7
7
|
import { createRunIngress } from './ingress.js';
|
|
8
8
|
import { notifyObserver } from './observer.js';
|
|
@@ -13,26 +13,21 @@ import { createWakeEngine } from './wake-engine.js';
|
|
|
13
13
|
import { createTimeoutScheduler } from './wake-scheduler.js';
|
|
14
14
|
import { createWorkerCallbacks } from './worker-callbacks.js';
|
|
15
15
|
export { createTimeoutScheduler } from './wake-scheduler.js';
|
|
16
|
-
export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors.js';
|
|
16
|
+
export { DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunTerminalError, StartRunConflictError, } from './errors.js';
|
|
17
17
|
export { RunOverloadedError } from './ingress.js';
|
|
18
18
|
function currentTimeMillis() {
|
|
19
19
|
return Math.floor(performance.timeOrigin + performance.now());
|
|
20
20
|
}
|
|
21
|
-
/** Threads whose in-process effects an admitted signal interrupts. */
|
|
22
|
-
function interruptedThreadIds(events, rules) {
|
|
23
|
-
const
|
|
24
|
-
for (const event of events) {
|
|
21
|
+
/** Threads whose in-process effects an admitted signal interrupts: each named one and its subtree. */
|
|
22
|
+
function interruptedThreadIds(events, rules, state) {
|
|
23
|
+
const named = events.flatMap((event) => {
|
|
25
24
|
const payload = Predicate.isObject(event.payload) ? event.payload : {};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return threadIds;
|
|
25
|
+
const cancelled = event.type === 'runtime.thread.cancel.requested' && Predicate.isString(payload.threadId)
|
|
26
|
+
? [payload.threadId]
|
|
27
|
+
: [];
|
|
28
|
+
return [...cancelled, ...rules.flatMap((rule) => rule(event))];
|
|
29
|
+
});
|
|
30
|
+
return new Set(named.flatMap((threadId) => [threadId, ...descendantThreadIds(state, threadId)]));
|
|
36
31
|
}
|
|
37
32
|
const DEFAULT_RUN_CACHE_SIZE = 0;
|
|
38
33
|
const RESCAN_CONCURRENCY = 16;
|
|
@@ -83,6 +78,9 @@ export function createRuntime(options) {
|
|
|
83
78
|
...input,
|
|
84
79
|
idempotencyKey: input.idempotencyKey ?? signalKey,
|
|
85
80
|
})), { origin: { type: 'external' } });
|
|
81
|
+
if (isRunTerminal(current.state)) {
|
|
82
|
+
return yield* new RunTerminalError(runId, current.state.status);
|
|
83
|
+
}
|
|
86
84
|
const folded = foldRun(batch, registry, { runId, initial: current.state });
|
|
87
85
|
const satisfiedEvents = waitSatisfiedEvents(folded, batch).map((input) => createEvent(runId, input));
|
|
88
86
|
return {
|
|
@@ -92,7 +90,7 @@ export function createRuntime(options) {
|
|
|
92
90
|
};
|
|
93
91
|
}));
|
|
94
92
|
if (committed.append) {
|
|
95
|
-
executionContexts.abort(runId, interruptedThreadIds(events, registry.interruptRules), 'Effect interrupted');
|
|
93
|
+
executionContexts.abort(runId, interruptedThreadIds(events, registry.interruptRules, committed.value), 'Effect interrupted');
|
|
96
94
|
}
|
|
97
95
|
return committed.value;
|
|
98
96
|
});
|
|
@@ -134,6 +132,21 @@ export function createRuntime(options) {
|
|
|
134
132
|
const store = yield* EventStoreTag;
|
|
135
133
|
return yield* store.listRuns;
|
|
136
134
|
}),
|
|
135
|
+
listRunSummaries: Effect.gen(function* () {
|
|
136
|
+
const store = yield* EventStoreTag;
|
|
137
|
+
const runIds = yield* store.listRuns;
|
|
138
|
+
const indexed = yield* snapshotStore.listHeaders;
|
|
139
|
+
const byId = new Map(indexed.map((header) => [header.runId, header]));
|
|
140
|
+
const missing = runIds.filter((runId) => !byId.has(runId));
|
|
141
|
+
const filled = yield* Effect.forEach(missing, (runId) => runtime.getRun(runId).pipe(Effect.map(summarizeRun)), { concurrency: 8 });
|
|
142
|
+
for (const header of filled) {
|
|
143
|
+
byId.set(header.runId, header);
|
|
144
|
+
}
|
|
145
|
+
return runIds.flatMap((runId) => {
|
|
146
|
+
const header = byId.get(runId);
|
|
147
|
+
return header ? [header] : [];
|
|
148
|
+
});
|
|
149
|
+
}),
|
|
137
150
|
getEvents: (runId, readOptions) => Effect.gen(function* () {
|
|
138
151
|
const store = yield* EventStoreTag;
|
|
139
152
|
return yield* store.read(runId, readOptions);
|
|
@@ -280,6 +293,35 @@ export function createRuntime(options) {
|
|
|
280
293
|
},
|
|
281
294
|
signal: (runId, events, signalOpts) => admitExternalEvents(events).pipe(Effect.flatMap((admitted) => withRunIngress(runId, signalAdmitted(runId, admitted, signalOpts))), Effect.andThen(runtime.wake(runId))),
|
|
282
295
|
cancel: cancellation,
|
|
296
|
+
fork: (sourceRunId, forkOptions) => {
|
|
297
|
+
const runId = forkOptions?.runId ?? createRunId();
|
|
298
|
+
return withRunIngress(runId, Effect.gen(function* () {
|
|
299
|
+
const store = yield* EventStoreTag;
|
|
300
|
+
const source = yield* loadCursor(store, sourceRunId);
|
|
301
|
+
if (source.seq === 0) {
|
|
302
|
+
return yield* new RunNotFoundError(sourceRunId);
|
|
303
|
+
}
|
|
304
|
+
if (!isRunTerminal(source.state)) {
|
|
305
|
+
return yield* new RunNotFinishedError(sourceRunId, source.state.status);
|
|
306
|
+
}
|
|
307
|
+
const plan = yield* planFork({
|
|
308
|
+
sourceRunId,
|
|
309
|
+
runId,
|
|
310
|
+
events: yield* store.read(sourceRunId),
|
|
311
|
+
registry,
|
|
312
|
+
});
|
|
313
|
+
// Appending at tail 0 numbers the copied events 1..n, the seqs they had in the source.
|
|
314
|
+
yield* commit(store, runId, (existing) => {
|
|
315
|
+
if (existing.seq === 0) {
|
|
316
|
+
return Effect.succeed({ events: plan.events, value: undefined });
|
|
317
|
+
}
|
|
318
|
+
return existing.state.forkedFrom?.runId === sourceRunId
|
|
319
|
+
? Effect.succeed({ events: [], value: undefined })
|
|
320
|
+
: Effect.fail(new ForkConflictError(runId, sourceRunId));
|
|
321
|
+
});
|
|
322
|
+
return plan.rootThreadId;
|
|
323
|
+
})).pipe(Effect.flatMap((threadId) => runtime.wake(runId).pipe(Effect.map((state) => ({ runId, threadId, state })))));
|
|
324
|
+
},
|
|
283
325
|
wake: wakeEngine,
|
|
284
326
|
dispose: Effect.sync(() => scheduler.dispose?.()).pipe(Effect.andThen(services.disposeEffect)),
|
|
285
327
|
};
|
|
@@ -22,6 +22,7 @@ export type AuthDecision = {
|
|
|
22
22
|
export type Authorize = (request: Request, route: RouteInfo) => Effect.Effect<AuthDecision>;
|
|
23
23
|
export declare const authAllowed: AuthDecision;
|
|
24
24
|
export declare function authDenied(status: number, error: string): AuthDecision;
|
|
25
|
+
export declare function authResponse(decision: AuthDecision): Response | null;
|
|
25
26
|
/**
|
|
26
27
|
* Used when a host supplies no `authorize`: run reads and writes stay open, while worker callbacks
|
|
27
28
|
* and operational endpoints are unavailable until a host opts in.
|
|
@@ -3,6 +3,11 @@ export const authAllowed = { allowed: true };
|
|
|
3
3
|
export function authDenied(status, error) {
|
|
4
4
|
return { allowed: false, status, error };
|
|
5
5
|
}
|
|
6
|
+
export function authResponse(decision) {
|
|
7
|
+
return decision.allowed
|
|
8
|
+
? null
|
|
9
|
+
: Response.json({ error: decision.error }, { status: decision.status });
|
|
10
|
+
}
|
|
6
11
|
const unauthorized = authDenied(401, 'Unauthorized');
|
|
7
12
|
function notConfigured(access) {
|
|
8
13
|
return authDenied(503, access === 'worker'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import type { DebuggerHost } from '@swirls/looms/core';
|
|
3
|
+
import { type Authorize } from './auth';
|
|
4
|
+
export interface DebuggerMount {
|
|
5
|
+
readonly basePath: string;
|
|
6
|
+
readonly host: DebuggerHost;
|
|
7
|
+
}
|
|
8
|
+
export declare function mountDebugger(host: DebuggerHost): DebuggerMount;
|
|
9
|
+
/** Path below the mount, or null when `pathname` is outside it. */
|
|
10
|
+
export declare function debuggerPath(mount: DebuggerMount, pathname: string): string | null;
|
|
11
|
+
export declare function serveDebugger(req: Request, mount: DebuggerMount, path: string, authorize: Authorize): Effect.Effect<Response | null>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { authResponse } from './auth.js';
|
|
3
|
+
import { isLoomsApiPath } from './http.js';
|
|
4
|
+
export function mountDebugger(host) {
|
|
5
|
+
const path = host.basePath;
|
|
6
|
+
if (!path.startsWith('/') || path.includes('//') || path.split('/').includes('..')) {
|
|
7
|
+
throw new Error(`debugger basePath must be an absolute URL path: ${path}`);
|
|
8
|
+
}
|
|
9
|
+
const basePath = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
10
|
+
if (basePath === '') {
|
|
11
|
+
throw new Error('debugger basePath must not be /');
|
|
12
|
+
}
|
|
13
|
+
if (isLoomsApiPath(basePath)) {
|
|
14
|
+
throw new Error(`debugger basePath overlaps the HTTP API: ${basePath}`);
|
|
15
|
+
}
|
|
16
|
+
return { basePath, host };
|
|
17
|
+
}
|
|
18
|
+
/** Path below the mount, or null when `pathname` is outside it. */
|
|
19
|
+
export function debuggerPath(mount, pathname) {
|
|
20
|
+
if (pathname === mount.basePath) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
return pathname.startsWith(`${mount.basePath}/`) ? pathname.slice(mount.basePath.length) : null;
|
|
24
|
+
}
|
|
25
|
+
export function serveDebugger(req, mount, path, authorize) {
|
|
26
|
+
return authorize(req, { access: 'read', name: 'debugger' }).pipe(Effect.flatMap((decision) => {
|
|
27
|
+
const denied = authResponse(decision);
|
|
28
|
+
return denied
|
|
29
|
+
? Effect.succeed(denied)
|
|
30
|
+
: Effect.promise(() => mount.host.fetch(req, { basePath: mount.basePath, path }));
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EventStore } from '@swirls/looms/core';
|
|
2
2
|
import type { LoomsRuntime } from '../runtime';
|
|
3
3
|
import { type Authorize } from './auth';
|
|
4
|
+
import { type DebuggerMount } from './debugger-host';
|
|
4
5
|
export interface FetchHandlerOptions {
|
|
5
6
|
readonly runtime: LoomsRuntime;
|
|
6
7
|
readonly store: EventStore;
|
|
@@ -9,6 +10,9 @@ export interface FetchHandlerOptions {
|
|
|
9
10
|
* operations routes unavailable; use `bearerAuth` or your own policy to change that.
|
|
10
11
|
*/
|
|
11
12
|
readonly authorize?: Authorize;
|
|
13
|
+
/** Debugger UI served beside the API and authorized as `{ access: 'read', name: 'debugger' }`. */
|
|
14
|
+
readonly debugger?: DebuggerMount;
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
/** Whether the fetch handler answers `pathname`: the HTTP API or the debugger mount. */
|
|
17
|
+
export declare function handlesPath(pathname: string, mount: DebuggerMount | undefined): boolean;
|
|
14
18
|
export declare function createFetchHandler(options: FetchHandlerOptions): (req: Request) => Promise<Response | null>;
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { Effect, Predicate } from 'effect';
|
|
2
|
-
import { defaultAuthorize } from './auth.js';
|
|
3
|
-
import {
|
|
2
|
+
import { authResponse, defaultAuthorize } from './auth.js';
|
|
3
|
+
import { debuggerPath, serveDebugger } from './debugger-host.js';
|
|
4
|
+
import { acceptsEventStream, isLoomsApiPath, toErrorResponse } from './http.js';
|
|
4
5
|
import { matchRoute } from './route-table.js';
|
|
6
|
+
import { definitionRoutes } from './routes/definitions.js';
|
|
5
7
|
import { effectRoutes } from './routes/effects.js';
|
|
6
8
|
import { operationRoutes } from './routes/operations.js';
|
|
7
9
|
import { runRoutes } from './routes/runs.js';
|
|
8
10
|
function createRoutes(options) {
|
|
9
|
-
return [
|
|
11
|
+
return [
|
|
12
|
+
...definitionRoutes(options),
|
|
13
|
+
...runRoutes(options),
|
|
14
|
+
...effectRoutes(options),
|
|
15
|
+
...operationRoutes(options),
|
|
16
|
+
];
|
|
10
17
|
}
|
|
11
18
|
function authorizeRoute(req, matched, authorize) {
|
|
12
19
|
const { access, name } = matched.route;
|
|
@@ -14,21 +21,22 @@ function authorizeRoute(req, matched, authorize) {
|
|
|
14
21
|
return Effect.succeed(null);
|
|
15
22
|
}
|
|
16
23
|
const runId = matched.params.runId;
|
|
17
|
-
return authorize(req, Predicate.isString(runId) ? { access, name, runId } : { access, name }).pipe(Effect.map(
|
|
18
|
-
? null
|
|
19
|
-
: Response.json({ error: decision.error }, { status: decision.status })));
|
|
24
|
+
return authorize(req, Predicate.isString(runId) ? { access, name, runId } : { access, name }).pipe(Effect.map(authResponse));
|
|
20
25
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
path.startsWith('/runs/') ||
|
|
25
|
-
path.startsWith('/operations/'));
|
|
26
|
+
/** Whether the fetch handler answers `pathname`: the HTTP API or the debugger mount. */
|
|
27
|
+
export function handlesPath(pathname, mount) {
|
|
28
|
+
return isLoomsApiPath(pathname) || (mount !== undefined && debuggerPath(mount, pathname) !== null);
|
|
26
29
|
}
|
|
27
30
|
export function createFetchHandler(options) {
|
|
28
31
|
const routes = createRoutes(options);
|
|
29
32
|
const authorize = options.authorize ?? defaultAuthorize;
|
|
33
|
+
const mount = options.debugger;
|
|
30
34
|
return (req) => {
|
|
31
35
|
const url = new URL(req.url);
|
|
36
|
+
const mountedPath = mount ? debuggerPath(mount, url.pathname) : null;
|
|
37
|
+
if (mount && mountedPath !== null) {
|
|
38
|
+
return respond(serveDebugger(req, mount, mountedPath, authorize));
|
|
39
|
+
}
|
|
32
40
|
if (!isLoomsApiPath(url.pathname)) {
|
|
33
41
|
return Promise.resolve(null);
|
|
34
42
|
}
|
|
@@ -36,7 +44,7 @@ export function createFetchHandler(options) {
|
|
|
36
44
|
if (req.method === 'GET' && accept.includes('text/html') && !acceptsEventStream(req)) {
|
|
37
45
|
return Promise.resolve(null);
|
|
38
46
|
}
|
|
39
|
-
return
|
|
47
|
+
return respond(Effect.gen(function* () {
|
|
40
48
|
const matched = matchRoute(routes, req.method, url.pathname);
|
|
41
49
|
if (!matched) {
|
|
42
50
|
return new Response('Not Found', { status: 404 });
|
|
@@ -46,6 +54,9 @@ export function createFetchHandler(options) {
|
|
|
46
54
|
return denied;
|
|
47
55
|
}
|
|
48
56
|
return yield* matched.route.handle({ req, url, params: matched.params });
|
|
49
|
-
})
|
|
57
|
+
}));
|
|
50
58
|
};
|
|
51
59
|
}
|
|
60
|
+
function respond(effect) {
|
|
61
|
+
return Effect.runPromise(effect.pipe(Effect.catch((error) => Effect.succeed(toErrorResponse(error))), Effect.catchCause((cause) => Effect.logError('[@swirls/looms/runtime] request failed', cause).pipe(Effect.as(Response.json({ error: 'Internal server error' }, { status: 500 }))))));
|
|
62
|
+
}
|
|
@@ -26,8 +26,11 @@ export declare const EventInputBodySchema: Schema.Struct<{
|
|
|
26
26
|
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
27
27
|
readonly threadId: Schema.optional<Schema.NullOr<Schema.String>>;
|
|
28
28
|
}>;
|
|
29
|
+
export declare function isLoomsApiPath(path: string): boolean;
|
|
29
30
|
export declare function provideStore<A, E>(effect: Effect.Effect<A, E, EventStoreTag>, store: EventStore): Effect.Effect<A, E>;
|
|
30
31
|
export declare const readJson: (req: Request) => Effect.Effect<JsonValue, RequestBodyError>;
|
|
32
|
+
/** `readJson` for routes whose body is optional: a request without one reads as `{}`. */
|
|
33
|
+
export declare const readOptionalJson: (req: Request) => Effect.Effect<JsonValue, RequestBodyError>;
|
|
31
34
|
export declare function extractIdempotencyKey(req: Request, bodyKey?: string): string | undefined;
|
|
32
35
|
export declare function acceptsEventStream(req: Request): boolean;
|
|
33
36
|
export declare function readIntegerQuery(url: URL, name: string, options: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Data, Effect, Schema } from 'effect';
|
|
2
|
-
import { EventStoreTag, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
|
|
3
|
-
import { RunOverloadedError, StartRunConflictError } from '../runtime.js';
|
|
2
|
+
import { EventStoreTag, ForkSourceError, InvalidEventError, InvalidInputError, JsonValueSchema, } from '@swirls/looms/core';
|
|
3
|
+
import { ForkConflictError, RunNotFinishedError, RunNotFoundError, RunOverloadedError, RunTerminalError, StartRunConflictError, } from '../runtime.js';
|
|
4
4
|
export class RequestBodyError extends Data.TaggedError('RequestBodyError') {
|
|
5
5
|
constructor(cause) {
|
|
6
6
|
super({ cause, message: 'Invalid JSON request body' });
|
|
@@ -14,6 +14,10 @@ export const EventInputBodySchema = Schema.Struct({
|
|
|
14
14
|
payload: JsonValueSchema,
|
|
15
15
|
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
16
16
|
});
|
|
17
|
+
const LOOMS_API_ROOTS = ['/health', '/runs', '/definitions', '/operations'];
|
|
18
|
+
export function isLoomsApiPath(path) {
|
|
19
|
+
return LOOMS_API_ROOTS.some((root) => path === root || path.startsWith(`${root}/`));
|
|
20
|
+
}
|
|
17
21
|
export function provideStore(effect, store) {
|
|
18
22
|
return Effect.provideService(effect, EventStoreTag, store);
|
|
19
23
|
}
|
|
@@ -21,6 +25,10 @@ export const readJson = (req) => Effect.tryPromise({
|
|
|
21
25
|
try: () => req.json(),
|
|
22
26
|
catch: (cause) => new RequestBodyError(cause),
|
|
23
27
|
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(JsonValueSchema)), Effect.mapError((cause) => cause instanceof RequestBodyError ? cause : new RequestBodyError(cause)));
|
|
28
|
+
/** `readJson` for routes whose body is optional: a request without one reads as `{}`. */
|
|
29
|
+
export const readOptionalJson = (req) => req.body === null || req.headers.get('content-length') === '0'
|
|
30
|
+
? Effect.succeed({})
|
|
31
|
+
: readJson(req);
|
|
24
32
|
export function extractIdempotencyKey(req, bodyKey) {
|
|
25
33
|
return (bodyKey ??
|
|
26
34
|
req.headers.get('idempotency-key') ??
|
|
@@ -55,8 +63,19 @@ export function toErrorResponse(err) {
|
|
|
55
63
|
if (err instanceof RunOverloadedError) {
|
|
56
64
|
return Response.json({ error: err.message }, { status: 429 });
|
|
57
65
|
}
|
|
66
|
+
if (err instanceof RunNotFoundError) {
|
|
67
|
+
return Response.json({ error: err.message }, { status: 404 });
|
|
68
|
+
}
|
|
69
|
+
if (err instanceof RunTerminalError) {
|
|
70
|
+
return Response.json({ error: err.message, status: err.status }, { status: 409 });
|
|
71
|
+
}
|
|
58
72
|
if (err instanceof StartRunConflictError) {
|
|
59
73
|
return Response.json({ error: err.message, mismatches: err.mismatches }, { status: 409 });
|
|
60
74
|
}
|
|
75
|
+
if (err instanceof RunNotFinishedError ||
|
|
76
|
+
err instanceof ForkConflictError ||
|
|
77
|
+
err instanceof ForkSourceError) {
|
|
78
|
+
return Response.json({ error: err.message }, { status: 409 });
|
|
79
|
+
}
|
|
61
80
|
return Response.json({ error: err.message }, { status: 500 });
|
|
62
81
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { publishDefinition } from '@swirls/looms/core';
|
|
3
|
+
import { route } from '../route-table.js';
|
|
4
|
+
export function definitionRoutes({ runtime }) {
|
|
5
|
+
return [
|
|
6
|
+
route({
|
|
7
|
+
method: 'GET',
|
|
8
|
+
path: '/definitions',
|
|
9
|
+
name: 'definitions.list',
|
|
10
|
+
access: 'read',
|
|
11
|
+
handle: () => {
|
|
12
|
+
const catalog = {
|
|
13
|
+
definitions: runtime.registry.definitions.map(publishDefinition),
|
|
14
|
+
projections: [...runtime.registry.projections.keys()],
|
|
15
|
+
};
|
|
16
|
+
return Effect.succeed(Response.json(catalog));
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Schema } from 'effect';
|
|
2
2
|
import { createRunId, EventStoreTag, JsonValueSchema, stringifyJson, } from '@swirls/looms/core';
|
|
3
3
|
import { createEventStreamResponse } from '../../sse.js';
|
|
4
|
-
import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, RequestQueryError, } from '../http.js';
|
|
4
|
+
import { acceptsEventStream, EventInputBodySchema, extractIdempotencyKey, provideStore, readIntegerQuery, readJson, readOptionalJson, RequestQueryError, } from '../http.js';
|
|
5
5
|
import { route } from '../route-table.js';
|
|
6
6
|
const StartRunBodySchema = Schema.Struct({
|
|
7
7
|
kind: Schema.optional(Schema.String),
|
|
@@ -19,6 +19,12 @@ const SignalBodySchema = Schema.Struct({
|
|
|
19
19
|
threadId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
20
20
|
idempotencyKey: Schema.optional(Schema.String),
|
|
21
21
|
});
|
|
22
|
+
const CancelBodySchema = Schema.Struct({
|
|
23
|
+
threadId: Schema.optional(Schema.String),
|
|
24
|
+
});
|
|
25
|
+
const ForkBodySchema = Schema.Struct({
|
|
26
|
+
runId: Schema.optional(Schema.String),
|
|
27
|
+
});
|
|
22
28
|
function wantsRunStream(req, url) {
|
|
23
29
|
return url.searchParams.get('stream') === 'true' || acceptsEventStream(req);
|
|
24
30
|
}
|
|
@@ -72,7 +78,14 @@ export function runRoutes({ runtime, store }) {
|
|
|
72
78
|
path: '/runs',
|
|
73
79
|
name: 'runs.list',
|
|
74
80
|
access: 'read',
|
|
75
|
-
handle: () => provideStore(runtime.listRuns
|
|
81
|
+
handle: () => provideStore(runtime.listRuns.pipe(Effect.map((runIds) => Response.json({ runIds }))), store),
|
|
82
|
+
}),
|
|
83
|
+
route({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
path: '/runs/summaries',
|
|
86
|
+
name: 'runs.summaries',
|
|
87
|
+
access: 'read',
|
|
88
|
+
handle: () => provideStore(runtime.listRunSummaries.pipe(Effect.map((runs) => Response.json({ runs }))), store),
|
|
76
89
|
}),
|
|
77
90
|
route({
|
|
78
91
|
method: 'POST',
|
|
@@ -186,7 +199,34 @@ export function runRoutes({ runtime, store }) {
|
|
|
186
199
|
path: '/runs/:runId/wake',
|
|
187
200
|
name: 'runs.wake',
|
|
188
201
|
access: 'write',
|
|
189
|
-
handle: ({ params }) =>
|
|
202
|
+
handle: ({ params }) => Effect.gen(function* () {
|
|
203
|
+
const before = yield* store.tail(params.runId);
|
|
204
|
+
const state = yield* provideStore(runtime.wake(params.runId), store);
|
|
205
|
+
const appended = (yield* store.tail(params.runId)) - before;
|
|
206
|
+
return Response.json({ runId: params.runId, state, appended });
|
|
207
|
+
}),
|
|
208
|
+
}),
|
|
209
|
+
route({
|
|
210
|
+
method: 'POST',
|
|
211
|
+
path: '/runs/:runId/cancel',
|
|
212
|
+
name: 'runs.cancel',
|
|
213
|
+
access: 'write',
|
|
214
|
+
handle: ({ req, params }) => Effect.gen(function* () {
|
|
215
|
+
const body = yield* readOptionalJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(CancelBodySchema)));
|
|
216
|
+
const state = yield* provideStore(runtime.cancel(params.runId, body.threadId), store);
|
|
217
|
+
return Response.json({ runId: params.runId, state });
|
|
218
|
+
}),
|
|
219
|
+
}),
|
|
220
|
+
route({
|
|
221
|
+
method: 'POST',
|
|
222
|
+
path: '/runs/:runId/fork',
|
|
223
|
+
name: 'runs.fork',
|
|
224
|
+
access: 'write',
|
|
225
|
+
handle: ({ req, params }) => Effect.gen(function* () {
|
|
226
|
+
const body = yield* readOptionalJson(req).pipe(Effect.flatMap(Schema.decodeUnknownEffect(ForkBodySchema)));
|
|
227
|
+
const result = yield* provideStore(runtime.fork(params.runId, { runId: body.runId }), store);
|
|
228
|
+
return Response.json(result);
|
|
229
|
+
}),
|
|
190
230
|
}),
|
|
191
231
|
];
|
|
192
232
|
}
|
package/dist/runtime/server.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { createFetchHandler,
|
|
1
|
+
export { createFetchHandler, handlesPath, type FetchHandlerOptions } from './server/fetch-handler';
|
|
2
|
+
export { isLoomsApiPath } from './server/http';
|
|
3
|
+
export { mountDebugger, type DebuggerMount } from './server/debugger-host';
|
|
2
4
|
export { authAllowed, authDenied, bearerAuth, defaultAuthorize, type AuthDecision, type AuthorizedAccess, type Authorize, type BearerTokens, type RouteAccess, type RouteInfo, } from './server/auth';
|
|
3
5
|
export interface RunningServer {
|
|
4
6
|
port: number;
|
package/dist/runtime/server.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { createFetchHandler,
|
|
1
|
+
export { createFetchHandler, handlesPath } from './server/fetch-handler.js';
|
|
2
|
+
export { isLoomsApiPath } from './server/http.js';
|
|
3
|
+
export { mountDebugger } from './server/debugger-host.js';
|
|
2
4
|
export { authAllowed, authDenied, bearerAuth, defaultAuthorize, } from './server/auth.js';
|
|
3
5
|
export function serveHttp(fetchHandler, options) {
|
|
4
6
|
const port = options?.port ?? 8787;
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Effect } from 'effect';
|
|
2
|
-
import type { AnyRuntimeModule, ComposedRegistry, EventEnvelope, EventInput, WorkerCallbackInput, EventStore, EventStoreAppendError, EventStoreError, EventStoreFencedError, EventStoreTag, EventStoreTrimCoverage, EventStoreTruncationError, JsonValue, ProjectionDefinition, ReplayStep, RunState, SnapshotStore } from '@swirls/looms/core';
|
|
2
|
+
import type { AnyRuntimeModule, ComposedRegistry, EventEnvelope, EventInput, WorkerCallbackInput, EventStore, EventStoreAppendError, EventStoreError, EventStoreFencedError, EventStoreTag, EventStoreTrimCoverage, EventStoreTruncationError, JsonValue, ProjectionDefinition, ReplayStep, RunState, RunSummary, SnapshotStore, SnapshotStoreError } from '@swirls/looms/core';
|
|
3
3
|
import type { EventStoreTrimmedError } from '@swirls/looms/core';
|
|
4
4
|
import type { DuplicateEffectDispatchError, MaxWakeIterationsError } from './errors';
|
|
5
5
|
import type { RuntimeObserver } from './observer';
|
|
@@ -105,9 +105,19 @@ export interface LoomsRuntime<TModules extends readonly AnyRuntimeModule[] = rea
|
|
|
105
105
|
inspectRun(runId: string): Effect.Effect<RunOperationalStatus, EventStoreError | EventStoreTrimmedError | EventStoreTruncationError, EventStoreTag>;
|
|
106
106
|
retryEffect(runId: string, effectId: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
|
|
107
107
|
cancel(runId: string, threadId?: string): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
|
|
108
|
+
/**
|
|
109
|
+
* Start a new run that continues where a finished run ended. The fork copies the source's
|
|
110
|
+
* history up to the point it finished and re-issues the effects outstanding there. Retrying
|
|
111
|
+
* with the same `runId` returns the existing fork.
|
|
112
|
+
*/
|
|
113
|
+
fork(sourceRunId: string, options?: {
|
|
114
|
+
runId?: string;
|
|
115
|
+
}): Effect.Effect<StartResult, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
|
|
108
116
|
/** Applies a worker's report about one effect attempt; stale or foreign reports are ignored. */
|
|
109
117
|
workerCallback(runId: string, input: WorkerCallbackInput): Effect.Effect<RunState, Error | EventStoreAppendError | EventStoreError, EventStoreTag>;
|
|
110
118
|
readonly listRuns: Effect.Effect<string[], EventStoreError, EventStoreTag>;
|
|
119
|
+
/** Indexed run rows. Folds a run only when its header has not been written yet. */
|
|
120
|
+
readonly listRunSummaries: Effect.Effect<readonly RunSummary[], EventStoreError | EventStoreTrimmedError | EventStoreTruncationError | SnapshotStoreError, EventStoreTag>;
|
|
111
121
|
readonly rescanTimers: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
|
|
112
122
|
/** Reschedule every durable timer and effect deadline after restart or repair. */
|
|
113
123
|
readonly recoverDeadlines: Effect.Effect<number, EventStoreTruncationError | EventStoreTrimmedError | EventStoreError, EventStoreTag>;
|
|
@@ -2,7 +2,7 @@ import { Clock, Effect, Exit, Option } from 'effect';
|
|
|
2
2
|
import { createEvent, EventStoreTag, foldRun, isRunTerminal, } from '@swirls/looms/core';
|
|
3
3
|
import { nextRuntimeDeadline, planDeadlineTransitions, planTerminalCancellationTransitions, } from './deadline-transition-planner.js';
|
|
4
4
|
import { DuplicateEffectDispatchError, MaxWakeIterationsError, RuntimeExecutionError, } from './errors.js';
|
|
5
|
-
import { stripSeq, synthesizedThreadFailed, waitSatisfiedEvents } from './helpers.js';
|
|
5
|
+
import { cascadedCancellations, stripSeq, synthesizedThreadFailed, waitSatisfiedEvents, } from './helpers.js';
|
|
6
6
|
import { executeLocalEffects } from './local-effect-executor.js';
|
|
7
7
|
import { notifyObserver } from './observer.js';
|
|
8
8
|
import { dispatchWorkerEffect, isWorkerEffect, planWorkerEffects } from './worker-effect-dispatch.js';
|
|
@@ -225,7 +225,10 @@ export function createWakeEngine(options) {
|
|
|
225
225
|
yield* liveAppends.drain(runId);
|
|
226
226
|
const beforeFold = cursor.state;
|
|
227
227
|
const intermediateState = foldRun(produced, registry, { runId, initial: beforeFold });
|
|
228
|
-
const synthesized =
|
|
228
|
+
const synthesized = [
|
|
229
|
+
...synthesizedThreadFailed(beforeFold, intermediateState, produced),
|
|
230
|
+
...cascadedCancellations(intermediateState, produced),
|
|
231
|
+
];
|
|
229
232
|
const synthesizedEvents = synthesized.map((input) => createEvent(runId, input));
|
|
230
233
|
const stateAfterSynth = synthesizedEvents.length > 0
|
|
231
234
|
? foldRun(synthesizedEvents, registry, { runId, initial: intermediateState })
|