@swirls/looms 0.0.2 → 0.1.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 +9 -2
- package/dist/actor/cell.d.ts +9 -4
- package/dist/actor/cell.js +7 -11
- package/dist/actor/host.d.ts +7 -3
- package/dist/actor/host.js +55 -32
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +12 -1
- package/dist/actor/routing.js +57 -51
- package/dist/agent/agent-session.d.ts +5 -0
- package/dist/agent/agent-session.js +270 -0
- package/dist/agent/definitions-store.d.ts +2 -3
- package/dist/agent/definitions-store.js +2 -4
- package/dist/agent/definitions.d.ts +46 -20
- package/dist/agent/definitions.js +36 -8
- package/dist/agent/effects.d.ts +359 -2
- package/dist/agent/effects.js +60 -34
- package/dist/agent/events.d.ts +175 -19
- package/dist/agent/events.js +45 -10
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/llm.d.ts +11 -2
- package/dist/agent/llm.js +13 -3
- package/dist/agent/module.d.ts +812 -78
- package/dist/agent/module.js +16 -4
- package/dist/agent/scope.d.ts +174 -18
- package/dist/agent/signals.d.ts +24 -1
- package/dist/agent/signals.js +77 -0
- package/dist/agent/threads.d.ts +3 -33
- package/dist/agent/threads.js +20 -25
- package/dist/agent/tool-schema.js +0 -1
- package/dist/agent/types.d.ts +49 -4
- package/dist/agent/types.js +5 -5
- package/dist/ai-vercel/index.d.ts +1 -0
- package/dist/ai-vercel/index.js +1 -0
- package/dist/ai-vercel/messages.js +22 -7
- package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
- package/dist/ai-vercel/vercel-evaluator.js +68 -0
- package/dist/ai-vercel/vercel-llm.js +64 -44
- package/dist/approval/effects.d.ts +89 -0
- package/dist/approval/module.d.ts +179 -29
- package/dist/approval/projections.d.ts +0 -14
- package/dist/approval/projections.js +20 -3
- package/dist/approval/signals.d.ts +2 -0
- package/dist/approval/signals.js +18 -2
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands.js +12 -16
- package/dist/client/client.d.ts +1195 -18
- package/dist/client/client.js +118 -120
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/sse.d.ts +13 -0
- package/dist/client/sse.js +38 -44
- package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
- package/dist/cloudflare/alarm-scheduler.js +34 -7
- package/dist/cloudflare/durable-object.d.ts +10 -2
- package/dist/cloudflare/durable-object.js +59 -28
- package/dist/cloudflare/event-store.d.ts +7 -2
- package/dist/cloudflare/event-store.js +8 -7
- package/dist/cloudflare/index.d.ts +2 -2
- package/dist/cloudflare/index.js +2 -2
- package/dist/cloudflare/router.js +13 -14
- package/dist/core/catalog.d.ts +3 -3
- package/dist/core/catalog.js +2 -5
- package/dist/core/coalesce.js +5 -3
- package/dist/core/compose.d.ts +2 -1
- package/dist/core/compose.js +18 -2
- package/dist/core/definition-store.d.ts +11 -0
- package/dist/core/definition-store.js +6 -0
- package/dist/core/effects.d.ts +127 -15
- package/dist/core/effects.js +89 -17
- package/dist/core/envelope.d.ts +27 -7
- package/dist/core/envelope.js +20 -6
- package/dist/core/event-codec.d.ts +4 -2
- package/dist/core/event-codec.js +10 -5
- package/dist/core/event-index.js +1 -3
- package/dist/core/fold.d.ts +3 -0
- package/dist/core/fold.js +171 -24
- package/dist/core/ids.d.ts +5 -0
- package/dist/core/ids.js +35 -2
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.js +5 -1
- package/dist/core/kind.d.ts +22 -0
- package/dist/core/kind.js +17 -0
- package/dist/core/limits.d.ts +34 -0
- package/dist/core/limits.js +46 -0
- package/dist/core/match.d.ts +1 -0
- package/dist/core/match.js +10 -1
- package/dist/core/module-scope.d.ts +22 -20
- package/dist/core/module-scope.js +38 -28
- package/dist/core/module.d.ts +16 -5
- package/dist/core/module.js +4 -0
- package/dist/core/projection.d.ts +13 -13
- package/dist/core/projection.js +9 -5
- package/dist/core/protocol.d.ts +68 -11
- package/dist/core/protocol.js +82 -11
- package/dist/core/replay.d.ts +312 -3
- package/dist/core/replay.js +11 -1
- package/dist/core/schema.d.ts +8 -19
- package/dist/core/schema.js +25 -80
- package/dist/core/serialize.js +2 -4
- package/dist/core/snapshot-store.d.ts +2 -1
- package/dist/core/snapshot-store.js +8 -2
- package/dist/core/snapshots.d.ts +5 -1
- package/dist/core/snapshots.js +10 -15
- package/dist/core/sqlite-store.js +56 -11
- package/dist/core/state.d.ts +142 -4
- package/dist/core/state.js +78 -0
- package/dist/core/store.d.ts +38 -3
- package/dist/core/store.js +77 -22
- package/dist/core/thread.d.ts +10 -9
- package/dist/core/types.d.ts +5 -8
- package/dist/core/types.js +28 -24
- package/dist/core/validate-event.js +14 -4
- package/dist/core/worker-callback.d.ts +17 -0
- package/dist/core/worker-callback.js +1 -0
- package/dist/debugger/components/event-stream.js +9 -10
- package/dist/debugger/events/protocol-summarize.js +26 -0
- package/dist/debugger/lib/event-meta.js +1 -2
- package/dist/evaluator/definitions-store.d.ts +11 -0
- package/dist/evaluator/definitions-store.js +8 -0
- package/dist/evaluator/definitions.d.ts +18 -0
- package/dist/evaluator/definitions.js +4 -0
- package/dist/evaluator/effects.d.ts +226 -0
- package/dist/evaluator/effects.js +132 -0
- package/dist/evaluator/evaluator.d.ts +24 -0
- package/dist/evaluator/evaluator.js +67 -0
- package/dist/evaluator/events.d.ts +59 -0
- package/dist/evaluator/events.js +21 -0
- package/dist/evaluator/index.d.ts +11 -0
- package/dist/evaluator/index.js +11 -0
- package/dist/evaluator/module.d.ts +606 -0
- package/dist/evaluator/module.js +25 -0
- package/dist/evaluator/projections.d.ts +50 -0
- package/dist/evaluator/projections.js +20 -0
- package/dist/evaluator/scope.d.ts +59 -0
- package/dist/evaluator/scope.js +7 -0
- package/dist/evaluator/signals.d.ts +3 -0
- package/dist/evaluator/signals.js +17 -0
- package/dist/evaluator/threads.d.ts +62 -0
- package/dist/evaluator/threads.js +117 -0
- package/dist/evaluator/types.d.ts +125 -0
- package/dist/evaluator/types.js +51 -0
- package/dist/projectors/delivery.d.ts +83 -0
- package/dist/projectors/delivery.js +182 -0
- package/dist/projectors/index-model.d.ts +1 -1
- package/dist/projectors/index-model.js +4 -6
- package/dist/projectors/index.d.ts +2 -0
- package/dist/projectors/index.js +2 -0
- package/dist/projectors/memory.d.ts +1 -1
- package/dist/projectors/memory.js +10 -8
- package/dist/projectors/postgres.js +44 -58
- package/dist/projectors/projector.d.ts +17 -0
- package/dist/projectors/projector.js +26 -1
- package/dist/projectors/sqlite-delivery.d.ts +4 -0
- package/dist/projectors/sqlite-delivery.js +147 -0
- package/dist/projectors/sqlite.js +23 -23
- package/dist/projectors/with-projectors.d.ts +7 -2
- package/dist/projectors/with-projectors.js +27 -28
- package/dist/react/notify.js +11 -8
- package/dist/react/react.d.ts +2 -3
- package/dist/react/react.js +14 -30
- package/dist/react/sse.d.ts +1 -1
- package/dist/react/sse.js +1 -1
- package/dist/react/store.d.ts +2 -2
- package/dist/react/store.js +109 -119
- package/dist/runtime/cancellation.d.ts +16 -0
- package/dist/runtime/cancellation.js +129 -0
- package/dist/runtime/deadline-transition-planner.d.ts +31 -0
- package/dist/runtime/deadline-transition-planner.js +139 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -2
- package/dist/runtime/dispatch-effect.js +7 -13
- package/dist/runtime/errors.d.ts +51 -0
- package/dist/runtime/errors.js +39 -0
- package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
- package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
- package/dist/runtime/index.d.ts +3 -3
- package/dist/runtime/index.js +3 -3
- package/dist/runtime/ingress.d.ts +15 -0
- package/dist/runtime/ingress.js +42 -0
- package/dist/runtime/local-effect-executor.d.ts +21 -0
- package/dist/runtime/local-effect-executor.js +137 -0
- package/dist/runtime/looms.d.ts +22 -4
- package/dist/runtime/looms.js +54 -36
- package/dist/runtime/observer.d.ts +45 -0
- package/dist/runtime/observer.js +11 -0
- package/dist/runtime/repository.d.ts +55 -0
- package/dist/runtime/repository.js +278 -0
- package/dist/runtime/retry-policy.d.ts +16 -0
- package/dist/runtime/retry-policy.js +29 -0
- package/dist/runtime/run-execution-context.d.ts +30 -0
- package/dist/runtime/run-execution-context.js +56 -0
- package/dist/runtime/run-start.d.ts +4 -0
- package/dist/runtime/run-start.js +44 -0
- package/dist/runtime/runtime.d.ts +6 -87
- package/dist/runtime/runtime.js +233 -470
- package/dist/runtime/server/auth.d.ts +40 -0
- package/dist/runtime/server/auth.js +40 -0
- package/dist/runtime/server/fetch-handler.d.ts +14 -0
- package/dist/runtime/server/fetch-handler.js +51 -0
- package/dist/runtime/server/http.d.ts +37 -0
- package/dist/runtime/server/http.js +62 -0
- package/dist/runtime/server/route-table.d.ts +55 -0
- package/dist/runtime/server/route-table.js +74 -0
- package/dist/runtime/server/routes/effects.d.ts +3 -0
- package/dist/runtime/server/routes/effects.js +59 -0
- package/dist/runtime/server/routes/operations.d.ts +3 -0
- package/dist/runtime/server/routes/operations.js +14 -0
- package/dist/runtime/server/routes/runs.d.ts +3 -0
- package/dist/runtime/server/routes/runs.js +192 -0
- package/dist/runtime/server.d.ts +2 -8
- package/dist/runtime/server.js +4 -211
- package/dist/runtime/sse.d.ts +2 -1
- package/dist/runtime/sse.js +24 -17
- package/dist/runtime/types.d.ts +115 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/wake-engine.d.ts +20 -0
- package/dist/runtime/wake-engine.js +311 -0
- package/dist/runtime/wake-scheduler.d.ts +3 -4
- package/dist/runtime/wake-scheduler.js +25 -31
- package/dist/runtime/worker-callbacks.d.ts +16 -0
- package/dist/runtime/worker-callbacks.js +167 -0
- package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
- package/dist/runtime/worker-effect-dispatch.js +126 -0
- package/dist/s2/config.d.ts +1 -1
- package/dist/s2/config.js +1 -1
- package/dist/s2/lite.js +70 -82
- package/dist/s2/pagination.d.ts +15 -0
- package/dist/s2/pagination.js +36 -0
- package/dist/s2/projector.js +6 -5
- package/dist/s2/snapshot-store.js +148 -163
- package/dist/s2/store.d.ts +3 -21
- package/dist/s2/store.js +71 -165
- package/dist/testing/index.d.ts +15 -0
- package/dist/testing/index.js +53 -16
- package/dist/workflow/definitions-store.d.ts +2 -3
- package/dist/workflow/definitions-store.js +2 -2
- package/dist/workflow/definitions.d.ts +78 -22
- package/dist/workflow/definitions.js +80 -14
- package/dist/workflow/effects.d.ts +453 -107
- package/dist/workflow/effects.js +214 -115
- package/dist/workflow/events.d.ts +149 -5
- package/dist/workflow/events.js +27 -5
- package/dist/workflow/graph.d.ts +56 -0
- package/dist/workflow/graph.js +207 -0
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +1 -0
- package/dist/workflow/module.d.ts +1047 -266
- package/dist/workflow/projections.d.ts +14 -12
- package/dist/workflow/projections.js +30 -8
- package/dist/workflow/scope.d.ts +146 -4
- package/dist/workflow/threads.d.ts +383 -155
- package/dist/workflow/threads.js +308 -110
- package/package.json +7 -2
- package/dist/runtime/events-api.d.ts +0 -4
- package/dist/runtime/events-api.js +0 -84
package/dist/workflow/threads.js
CHANGED
|
@@ -1,35 +1,235 @@
|
|
|
1
|
-
import { Predicate, Schema } from 'effect';
|
|
2
|
-
import {
|
|
3
|
-
import {} from './definitions.js';
|
|
1
|
+
import { Effect, Predicate, Schema } from 'effect';
|
|
2
|
+
import { cancel, createWaitId, DEFAULT_DEFINITION_VERSION, EventInputSchema, emit, fail, invoke, isJsonObject, isWithdrawnError, RuntimeEffectSchema, spawn, stableEffectId, wait, utf8JsonBytes, waitOutcome, } from '@swirls/looms/core';
|
|
3
|
+
import { NodeStateSchema } from './definitions.js';
|
|
4
4
|
import { runNodeEffect, scheduleEffect } from './effects.js';
|
|
5
|
+
import { iterationChildWorkflowId, nodeRunKey, WorkflowValueTooLargeError } from './graph.js';
|
|
5
6
|
import { workflowModule } from './scope.js';
|
|
6
7
|
export { NodeStateSchema, NodeStatusSchema } from './definitions.js';
|
|
7
|
-
|
|
8
|
-
export const
|
|
8
|
+
const attemptNumber = Schema.Finite.pipe(Schema.withDecodingDefaultKey(Effect.succeed(1)));
|
|
9
|
+
export const WorkflowPendingSpawnSchema = Schema.Struct({
|
|
10
|
+
childThreadId: Schema.String,
|
|
11
|
+
kind: Schema.String,
|
|
12
|
+
definitionName: Schema.String,
|
|
13
|
+
definitionVersion: Schema.String,
|
|
14
|
+
nodeId: Schema.String,
|
|
15
|
+
attempt: attemptNumber,
|
|
16
|
+
input: Schema.Json,
|
|
17
|
+
loopIndex: Schema.NullOr(Schema.Finite).pipe(Schema.withDecodingDefaultKey(Effect.succeed(null))),
|
|
18
|
+
});
|
|
19
|
+
export const WorkflowPendingMapSchema = Schema.Struct({
|
|
20
|
+
nodeId: Schema.String,
|
|
21
|
+
attempt: attemptNumber,
|
|
22
|
+
kind: Schema.String,
|
|
23
|
+
definitionName: Schema.String,
|
|
24
|
+
definitionVersion: Schema.String,
|
|
25
|
+
items: Schema.Array(Schema.Json),
|
|
26
|
+
concurrency: Schema.Finite,
|
|
27
|
+
limitBytes: Schema.Finite,
|
|
28
|
+
/** Items before this index have been started; later ones wait for a free slot. */
|
|
29
|
+
nextIndex: Schema.Finite,
|
|
30
|
+
outputs: Schema.Record(Schema.String, Schema.Json),
|
|
31
|
+
});
|
|
32
|
+
export const WorkflowPendingSleepSchema = Schema.Struct({
|
|
33
|
+
waitId: Schema.String,
|
|
34
|
+
wakeAt: Schema.Finite,
|
|
35
|
+
nodeId: Schema.String,
|
|
36
|
+
});
|
|
37
|
+
export const WorkflowPendingEffectsSchema = Schema.Struct({
|
|
38
|
+
nodeId: Schema.String,
|
|
39
|
+
effects: Schema.mutable(Schema.Array(RuntimeEffectSchema)),
|
|
40
|
+
});
|
|
41
|
+
export const WorkflowPendingEmitSchema = Schema.Struct({
|
|
42
|
+
id: Schema.String,
|
|
43
|
+
event: EventInputSchema,
|
|
44
|
+
});
|
|
45
|
+
export const WorkflowStateSchema = Schema.Struct({
|
|
46
|
+
definitionName: Schema.optional(Schema.String),
|
|
47
|
+
definitionVersion: Schema.optional(Schema.String),
|
|
48
|
+
nodes: Schema.Record(Schema.String, NodeStateSchema),
|
|
49
|
+
concurrency: Schema.Finite,
|
|
50
|
+
input: Schema.Json,
|
|
51
|
+
nodeIds: Schema.mutable(Schema.Array(Schema.String)),
|
|
52
|
+
needsSchedule: Schema.Boolean,
|
|
53
|
+
runningNodes: Schema.mutable(Schema.Array(Schema.String)),
|
|
54
|
+
pendingSpawns: Schema.mutable(Schema.Array(WorkflowPendingSpawnSchema)),
|
|
55
|
+
pendingMaps: Schema.mutable(Schema.Array(WorkflowPendingMapSchema)).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
56
|
+
pendingSleeps: Schema.mutable(Schema.Array(WorkflowPendingSleepSchema)),
|
|
57
|
+
pendingEffects: Schema.mutable(Schema.Array(WorkflowPendingEffectsSchema)),
|
|
58
|
+
pendingEmits: Schema.mutable(Schema.Array(WorkflowPendingEmitSchema)),
|
|
59
|
+
/** Children of stopped nodes that were still running; each is cancelled once. */
|
|
60
|
+
pendingCancels: Schema.mutable(Schema.Array(Schema.String)).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
61
|
+
status: Schema.Union([
|
|
62
|
+
Schema.Literal('running'),
|
|
63
|
+
Schema.Literal('completed'),
|
|
64
|
+
Schema.Literal('failed'),
|
|
65
|
+
]),
|
|
66
|
+
error: Schema.NullOr(Schema.String),
|
|
67
|
+
});
|
|
68
|
+
const initialWorkflowStatus = 'running';
|
|
9
69
|
function scheduleInvocation(state) {
|
|
10
70
|
return invoke(scheduleEffect, {
|
|
11
71
|
definitionName: state.definitionName,
|
|
72
|
+
definitionVersion: state.definitionVersion ?? DEFAULT_DEFINITION_VERSION,
|
|
12
73
|
nodes: state.nodes,
|
|
13
74
|
input: state.input,
|
|
14
75
|
});
|
|
15
76
|
}
|
|
16
77
|
function runNodeInvocation(state, nodeId) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
results[id] = node.result;
|
|
20
|
-
}
|
|
78
|
+
const node = state.nodes[nodeId];
|
|
79
|
+
const attempts = node?.attempts ?? 1;
|
|
21
80
|
return invoke(runNodeEffect, {
|
|
22
81
|
definitionName: state.definitionName,
|
|
82
|
+
definitionVersion: state.definitionVersion ?? DEFAULT_DEFINITION_VERSION,
|
|
23
83
|
nodeId,
|
|
84
|
+
attempt: attempts,
|
|
85
|
+
iteration: node?.iteration,
|
|
24
86
|
input: state.input,
|
|
25
|
-
|
|
26
|
-
}, `runNode_${nodeId}`);
|
|
87
|
+
nodes: state.nodes,
|
|
88
|
+
}, `runNode_${nodeRunKey(nodeId, attempts, node?.iteration?.index)}`);
|
|
89
|
+
}
|
|
90
|
+
function spawnAndWait(threadId, child, childThreadId, input, tag) {
|
|
91
|
+
return [
|
|
92
|
+
spawn({
|
|
93
|
+
childThreadId,
|
|
94
|
+
kind: child.kind,
|
|
95
|
+
definitionName: child.definitionName,
|
|
96
|
+
definitionVersion: child.definitionVersion,
|
|
97
|
+
input,
|
|
98
|
+
}),
|
|
99
|
+
wait({
|
|
100
|
+
waitId: createWaitId(threadId, `spawn_${childThreadId}`),
|
|
101
|
+
on: {
|
|
102
|
+
type: ['runtime.thread.completed', 'runtime.thread.failed'],
|
|
103
|
+
match: { threadId: childThreadId },
|
|
104
|
+
},
|
|
105
|
+
tag,
|
|
106
|
+
}),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
function mapChildThreadId(threadId, map, index) {
|
|
110
|
+
return iterationChildWorkflowId(threadId, map.nodeId, 'map', map.attempt, index);
|
|
111
|
+
}
|
|
112
|
+
/** Every effect a node currently needs, attributed to that node. */
|
|
113
|
+
function nodeEffects(state, threadId) {
|
|
114
|
+
const effects = [];
|
|
115
|
+
const add = (nodeId, items) => {
|
|
116
|
+
for (const effect of items) {
|
|
117
|
+
effects.push({ nodeId, effect });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const nodeId of state.runningNodes) {
|
|
121
|
+
add(nodeId, [runNodeInvocation(state, nodeId)]);
|
|
122
|
+
}
|
|
123
|
+
for (const item of state.pendingSpawns) {
|
|
124
|
+
const tag = { nodeId: item.nodeId, attempt: item.attempt };
|
|
125
|
+
add(item.nodeId, spawnAndWait(threadId, item, item.childThreadId, item.input, {
|
|
126
|
+
...tag,
|
|
127
|
+
loopIndex: item.loopIndex,
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
for (const map of state.pendingMaps) {
|
|
131
|
+
for (let index = 0; index < map.nextIndex; index++) {
|
|
132
|
+
if (!(String(index) in map.outputs)) {
|
|
133
|
+
add(map.nodeId, spawnAndWait(threadId, map, mapChildThreadId(threadId, map, index), map.items[index] ?? null, { nodeId: map.nodeId, attempt: map.attempt, index }));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const sleep of state.pendingSleeps) {
|
|
138
|
+
add(sleep.nodeId, [
|
|
139
|
+
wait({ waitId: sleep.waitId, on: { timerAt: sleep.wakeAt }, tag: { nodeId: sleep.nodeId } }),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
for (const pending of state.pendingEffects) {
|
|
143
|
+
add(pending.nodeId, pending.effects);
|
|
144
|
+
}
|
|
145
|
+
return effects;
|
|
146
|
+
}
|
|
147
|
+
function nodeIdForEffect(state, threadId, effectId) {
|
|
148
|
+
const match = nodeEffects(state, threadId).find((item) => stableEffectId(threadId, item.effect) === effectId);
|
|
149
|
+
return match?.nodeId ?? null;
|
|
150
|
+
}
|
|
151
|
+
/** Children a node started that have not reported back yet. */
|
|
152
|
+
function unsettledChildren(state, nodeId, threadId) {
|
|
153
|
+
const children = state.pendingSpawns
|
|
154
|
+
.filter((item) => item.nodeId === nodeId)
|
|
155
|
+
.map((item) => item.childThreadId);
|
|
156
|
+
for (const map of state.pendingMaps) {
|
|
157
|
+
if (map.nodeId !== nodeId) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (let index = 0; index < map.nextIndex; index++) {
|
|
161
|
+
if (!(String(index) in map.outputs)) {
|
|
162
|
+
children.push(mapChildThreadId(threadId, map, index));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return children;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Drops a node's pending work and cancels the children it leaves running. `settled` is the child
|
|
170
|
+
* whose result is finishing the node, which must not be cancelled.
|
|
171
|
+
*/
|
|
172
|
+
function withoutPendingWork(state, nodeId, threadId, settled = null) {
|
|
173
|
+
const cancels = unsettledChildren(state, nodeId, threadId).filter((childThreadId) => childThreadId !== settled && !state.pendingCancels.includes(childThreadId));
|
|
174
|
+
return {
|
|
175
|
+
...state,
|
|
176
|
+
pendingCancels: cancels.length === 0 ? state.pendingCancels : [...state.pendingCancels, ...cancels],
|
|
177
|
+
pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
|
|
178
|
+
pendingMaps: state.pendingMaps.filter((item) => item.nodeId !== nodeId),
|
|
179
|
+
pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
|
|
180
|
+
pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function stopNode(state, nodeId, threadId) {
|
|
184
|
+
return {
|
|
185
|
+
...withoutPendingWork(state, nodeId, threadId),
|
|
186
|
+
runningNodes: state.runningNodes.filter((id) => id !== nodeId),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Drops a node's pending work and emits the event that durably finishes it. */
|
|
190
|
+
function finishNode(state, nodeId, result, error, threadId, settled = null) {
|
|
191
|
+
const id = `finished_${nodeId}`;
|
|
192
|
+
const event = {
|
|
193
|
+
type: 'workflow.node.finished',
|
|
194
|
+
payload: { nodeId, result, error },
|
|
195
|
+
threadId,
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
...withoutPendingWork(state, nodeId, threadId, settled),
|
|
199
|
+
pendingEmits: [...state.pendingEmits.filter((item) => item.id !== id), { id, event }],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function recordMapOutput(state, map, index, output, threadId) {
|
|
203
|
+
if (String(index) in map.outputs) {
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
const settled = mapChildThreadId(threadId, map, index);
|
|
207
|
+
const outputs = { ...map.outputs, [String(index)]: output };
|
|
208
|
+
const size = utf8JsonBytes(outputs);
|
|
209
|
+
if (size > map.limitBytes) {
|
|
210
|
+
const error = new WorkflowValueTooLargeError(`Node ${map.nodeId} outputs`, size, map.limitBytes);
|
|
211
|
+
return finishNode(state, map.nodeId, null, error.message, threadId, settled);
|
|
212
|
+
}
|
|
213
|
+
if (Object.keys(outputs).length === map.items.length) {
|
|
214
|
+
const ordered = map.items.map((_, i) => outputs[String(i)] ?? null);
|
|
215
|
+
return finishNode(state, map.nodeId, ordered, null, threadId, settled);
|
|
216
|
+
}
|
|
217
|
+
const nextIndex = Math.min(map.items.length, map.nextIndex + 1);
|
|
218
|
+
return {
|
|
219
|
+
...state,
|
|
220
|
+
pendingMaps: state.pendingMaps.map((item) => item.nodeId === map.nodeId ? { ...item, outputs, nextIndex } : item),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function isCurrentAttempt(state, nodeId, attempt) {
|
|
224
|
+
const node = state.nodes[nodeId];
|
|
225
|
+
return node?.status === 'running' && (attempt === undefined || attempt === node.attempts);
|
|
27
226
|
}
|
|
28
227
|
export const workflowThread = workflowModule.thread({
|
|
29
228
|
kind: 'workflow',
|
|
30
229
|
shape: WorkflowStateSchema,
|
|
31
230
|
initialState: (ctx) => ({
|
|
32
231
|
definitionName: ctx.definitionName,
|
|
232
|
+
definitionVersion: ctx.definitionVersion,
|
|
33
233
|
nodes: {},
|
|
34
234
|
concurrency: 8,
|
|
35
235
|
input: ctx.input,
|
|
@@ -37,10 +237,12 @@ export const workflowThread = workflowModule.thread({
|
|
|
37
237
|
needsSchedule: false,
|
|
38
238
|
runningNodes: [],
|
|
39
239
|
pendingSpawns: [],
|
|
240
|
+
pendingMaps: [],
|
|
40
241
|
pendingSleeps: [],
|
|
41
242
|
pendingEffects: [],
|
|
42
243
|
pendingEmits: [],
|
|
43
|
-
|
|
244
|
+
pendingCancels: [],
|
|
245
|
+
status: initialWorkflowStatus,
|
|
44
246
|
error: null,
|
|
45
247
|
}),
|
|
46
248
|
step(state, event, ctx) {
|
|
@@ -48,6 +250,7 @@ export const workflowThread = workflowModule.thread({
|
|
|
48
250
|
case 'runtime.thread.started': {
|
|
49
251
|
const payload = event.payload;
|
|
50
252
|
const defName = payload.definitionName || state.definitionName;
|
|
253
|
+
const defVersion = payload.definitionVersion ?? state.definitionVersion ?? DEFAULT_DEFINITION_VERSION;
|
|
51
254
|
const inputObj = Predicate.isObject(payload.input) ? payload.input : {};
|
|
52
255
|
const nodeIds = Array.isArray(inputObj.nodeIds)
|
|
53
256
|
? inputObj.nodeIds.filter(Predicate.isString)
|
|
@@ -59,6 +262,7 @@ export const workflowThread = workflowModule.thread({
|
|
|
59
262
|
return {
|
|
60
263
|
...state,
|
|
61
264
|
definitionName: defName,
|
|
265
|
+
definitionVersion: defVersion,
|
|
62
266
|
nodes,
|
|
63
267
|
nodeIds,
|
|
64
268
|
input: payload.input ?? state.input,
|
|
@@ -74,7 +278,12 @@ export const workflowThread = workflowModule.thread({
|
|
|
74
278
|
...state,
|
|
75
279
|
nodes: {
|
|
76
280
|
...state.nodes,
|
|
77
|
-
[nodeId]: {
|
|
281
|
+
[nodeId]: {
|
|
282
|
+
status: 'running',
|
|
283
|
+
result: null,
|
|
284
|
+
error: null,
|
|
285
|
+
attempts: (state.nodes[nodeId]?.attempts ?? 0) + 1,
|
|
286
|
+
},
|
|
78
287
|
},
|
|
79
288
|
runningNodes: state.runningNodes.includes(nodeId)
|
|
80
289
|
? state.runningNodes
|
|
@@ -83,46 +292,71 @@ export const workflowThread = workflowModule.thread({
|
|
|
83
292
|
};
|
|
84
293
|
}
|
|
85
294
|
case 'workflow.node.finished': {
|
|
86
|
-
const { nodeId, result, error } = event.payload;
|
|
295
|
+
const { nodeId, result, error, branch } = event.payload;
|
|
87
296
|
if (!nodeId) {
|
|
88
297
|
return state;
|
|
89
298
|
}
|
|
90
299
|
const failed = Predicate.isString(error) && error.length > 0;
|
|
91
300
|
return {
|
|
92
|
-
...state,
|
|
301
|
+
...stopNode(state, nodeId, ctx.threadId),
|
|
93
302
|
nodes: {
|
|
94
303
|
...state.nodes,
|
|
95
304
|
[nodeId]: {
|
|
96
305
|
status: failed ? 'failed' : 'completed',
|
|
97
306
|
result: result ?? null,
|
|
98
307
|
error: failed ? error : null,
|
|
308
|
+
attempts: state.nodes[nodeId]?.attempts ?? 1,
|
|
309
|
+
branch,
|
|
99
310
|
},
|
|
100
311
|
},
|
|
101
|
-
runningNodes: state.runningNodes.filter((id) => id !== nodeId),
|
|
102
|
-
pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
|
|
103
|
-
pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
|
|
104
|
-
pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
|
|
105
312
|
pendingEmits: state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
|
|
106
|
-
status: failed ? 'failed' : state.status,
|
|
107
313
|
error: failed ? error : state.error,
|
|
108
|
-
needsSchedule:
|
|
314
|
+
needsSchedule: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
case 'workflow.node.skipped': {
|
|
318
|
+
const { nodeId, reason, cause } = event.payload;
|
|
319
|
+
return {
|
|
320
|
+
...stopNode(state, nodeId, ctx.threadId),
|
|
321
|
+
nodes: {
|
|
322
|
+
...state.nodes,
|
|
323
|
+
[nodeId]: {
|
|
324
|
+
status: 'skipped',
|
|
325
|
+
result: null,
|
|
326
|
+
error: reason,
|
|
327
|
+
attempts: state.nodes[nodeId]?.attempts ?? 0,
|
|
328
|
+
skipCause: cause,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
needsSchedule: true,
|
|
109
332
|
};
|
|
110
333
|
}
|
|
111
334
|
case 'workflow.spawn.requested': {
|
|
112
|
-
const {
|
|
113
|
-
if (!
|
|
335
|
+
const { nodeId } = event.payload;
|
|
336
|
+
if (!isCurrentAttempt(state, nodeId, event.payload.attempt)) {
|
|
114
337
|
return state;
|
|
115
338
|
}
|
|
116
339
|
return {
|
|
117
340
|
...state,
|
|
118
341
|
pendingSpawns: [
|
|
119
342
|
...state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
|
|
343
|
+
event.payload,
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
case 'workflow.map.requested': {
|
|
348
|
+
const { nodeId } = event.payload;
|
|
349
|
+
if (!isCurrentAttempt(state, nodeId, event.payload.attempt)) {
|
|
350
|
+
return state;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
...state,
|
|
354
|
+
pendingMaps: [
|
|
355
|
+
...state.pendingMaps.filter((item) => item.nodeId !== nodeId),
|
|
120
356
|
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
nodeId,
|
|
125
|
-
input: input ?? null,
|
|
357
|
+
...event.payload,
|
|
358
|
+
nextIndex: Math.min(event.payload.concurrency, event.payload.items.length),
|
|
359
|
+
outputs: {},
|
|
126
360
|
},
|
|
127
361
|
],
|
|
128
362
|
};
|
|
@@ -146,13 +380,16 @@ export const workflowThread = workflowModule.thread({
|
|
|
146
380
|
if (!nodeId) {
|
|
147
381
|
return state;
|
|
148
382
|
}
|
|
383
|
+
const attempts = state.nodes[nodeId]?.attempts ?? 1;
|
|
384
|
+
const key = nodeRunKey(nodeId, attempts, state.nodes[nodeId]?.iteration?.index);
|
|
149
385
|
const taggedEffects = effects.map((effect, idx) => {
|
|
150
386
|
if (effect.type === 'runtime.wait' && 'on' in effect) {
|
|
151
|
-
const
|
|
387
|
+
const scope = { nodeId, attempt: attempts };
|
|
388
|
+
const tag = Predicate.isObject(effect.tag) ? { ...effect.tag, ...scope } : scope;
|
|
152
389
|
return { ...effect, tag };
|
|
153
390
|
}
|
|
154
391
|
if (!('tag' in effect && Predicate.isString(effect.tag))) {
|
|
155
|
-
return { ...effect, tag: `${
|
|
392
|
+
return { ...effect, tag: `${key}_${effect.type}_${idx}` };
|
|
156
393
|
}
|
|
157
394
|
return effect;
|
|
158
395
|
});
|
|
@@ -169,72 +406,58 @@ export const workflowThread = workflowModule.thread({
|
|
|
169
406
|
if (isWithdrawnError(error)) {
|
|
170
407
|
return state;
|
|
171
408
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
409
|
+
const effectId = event.payload.effectId;
|
|
410
|
+
const nodeId = Predicate.isString(effectId)
|
|
411
|
+
? nodeIdForEffect(state, ctx.threadId, effectId)
|
|
412
|
+
: null;
|
|
413
|
+
if (!nodeId) {
|
|
174
414
|
return state;
|
|
175
415
|
}
|
|
176
|
-
const nodeId = running[0];
|
|
177
416
|
const message = `Node ${nodeId} failed: ${error}`;
|
|
178
417
|
return {
|
|
179
|
-
...state,
|
|
418
|
+
...finishNode(stopNode(state, nodeId, ctx.threadId), nodeId, null, message, ctx.threadId),
|
|
180
419
|
nodes: {
|
|
181
420
|
...state.nodes,
|
|
182
|
-
[nodeId]: {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
|
|
188
|
-
pendingEmits: [
|
|
189
|
-
...state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
|
|
190
|
-
{
|
|
191
|
-
id: `finished_${nodeId}`,
|
|
192
|
-
event: {
|
|
193
|
-
type: 'workflow.node.finished',
|
|
194
|
-
payload: { nodeId, result: null, error: message },
|
|
195
|
-
threadId: ctx.threadId,
|
|
196
|
-
},
|
|
421
|
+
[nodeId]: {
|
|
422
|
+
status: 'failed',
|
|
423
|
+
result: null,
|
|
424
|
+
error: message,
|
|
425
|
+
attempts: state.nodes[nodeId]?.attempts,
|
|
197
426
|
},
|
|
198
|
-
|
|
427
|
+
},
|
|
199
428
|
};
|
|
200
429
|
}
|
|
201
430
|
case 'runtime.wait.satisfied': {
|
|
202
431
|
const tag = event.payload.tag;
|
|
203
|
-
if (!Predicate.
|
|
432
|
+
if (!isJsonObject(tag) || !Predicate.isString(tag.nodeId)) {
|
|
204
433
|
return state;
|
|
205
434
|
}
|
|
206
|
-
const nodeId =
|
|
207
|
-
if (!nodeId) {
|
|
435
|
+
const nodeId = tag.nodeId;
|
|
436
|
+
if (!isCurrentAttempt(state, nodeId, tag.attempt)) {
|
|
208
437
|
return state;
|
|
209
438
|
}
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
|
|
223
|
-
{
|
|
224
|
-
id: `finished_${nodeId}`,
|
|
225
|
-
event: {
|
|
226
|
-
type: 'workflow.node.finished',
|
|
227
|
-
payload: {
|
|
228
|
-
nodeId,
|
|
229
|
-
// SAFETY: result is serialized to a JSON-compatible node execution result.
|
|
230
|
-
result: asJson(result) ?? null,
|
|
231
|
-
error,
|
|
232
|
-
},
|
|
233
|
-
threadId: ctx.threadId,
|
|
234
|
-
},
|
|
439
|
+
const { result, error } = waitOutcome(event.payload.event, tag, { waited: true });
|
|
440
|
+
const map = state.pendingMaps.find((item) => item.nodeId === nodeId);
|
|
441
|
+
if (error === null && map && Predicate.isNumber(tag.index)) {
|
|
442
|
+
return recordMapOutput(state, map, tag.index, result, ctx.threadId);
|
|
443
|
+
}
|
|
444
|
+
const node = state.nodes[nodeId];
|
|
445
|
+
if (error === null && node && Predicate.isNumber(tag.loopIndex)) {
|
|
446
|
+
return {
|
|
447
|
+
...state,
|
|
448
|
+
nodes: {
|
|
449
|
+
...state.nodes,
|
|
450
|
+
[nodeId]: { ...node, iteration: { index: tag.loopIndex + 1, previous: result } },
|
|
235
451
|
},
|
|
236
|
-
|
|
237
|
-
|
|
452
|
+
pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const satisfied = event.payload.event;
|
|
456
|
+
const satisfiedPayload = Predicate.isObject(satisfied) ? satisfied.payload : undefined;
|
|
457
|
+
const settled = Predicate.isObject(satisfiedPayload) && Predicate.isString(satisfiedPayload.threadId)
|
|
458
|
+
? satisfiedPayload.threadId
|
|
459
|
+
: null;
|
|
460
|
+
return finishNode(state, nodeId, result, error, ctx.threadId, settled);
|
|
238
461
|
}
|
|
239
462
|
default:
|
|
240
463
|
return state;
|
|
@@ -245,40 +468,15 @@ export const workflowThread = workflowModule.thread({
|
|
|
245
468
|
if (state.needsSchedule) {
|
|
246
469
|
effects.push(scheduleInvocation(state));
|
|
247
470
|
}
|
|
248
|
-
for (const
|
|
249
|
-
effects.push(
|
|
250
|
-
}
|
|
251
|
-
for (const spawnReq of state.pendingSpawns) {
|
|
252
|
-
const waitId = createWaitId(ctx.threadId, `spawn_${spawnReq.childThreadId}`);
|
|
253
|
-
effects.push(spawn({
|
|
254
|
-
childThreadId: spawnReq.childThreadId,
|
|
255
|
-
kind: spawnReq.kind,
|
|
256
|
-
definitionName: spawnReq.definitionName,
|
|
257
|
-
input: spawnReq.input,
|
|
258
|
-
}), wait({
|
|
259
|
-
waitId,
|
|
260
|
-
on: {
|
|
261
|
-
type: ['runtime.thread.completed', 'runtime.thread.failed'],
|
|
262
|
-
match: { threadId: spawnReq.childThreadId },
|
|
263
|
-
},
|
|
264
|
-
tag: { nodeId: spawnReq.nodeId },
|
|
265
|
-
}));
|
|
266
|
-
}
|
|
267
|
-
for (const sleepReq of state.pendingSleeps) {
|
|
268
|
-
effects.push(wait({
|
|
269
|
-
waitId: sleepReq.waitId,
|
|
270
|
-
on: { timerAt: sleepReq.wakeAt },
|
|
271
|
-
tag: { nodeId: sleepReq.nodeId },
|
|
272
|
-
}));
|
|
273
|
-
}
|
|
274
|
-
for (const effectReq of state.pendingEffects) {
|
|
275
|
-
for (const eff of effectReq.effects) {
|
|
276
|
-
effects.push(eff);
|
|
277
|
-
}
|
|
471
|
+
for (const item of nodeEffects(state, ctx.threadId)) {
|
|
472
|
+
effects.push(item.effect);
|
|
278
473
|
}
|
|
279
474
|
for (const emitReq of state.pendingEmits) {
|
|
280
475
|
effects.push(emit(emitReq.event));
|
|
281
476
|
}
|
|
477
|
+
for (const childThreadId of state.pendingCancels) {
|
|
478
|
+
effects.push(cancel(childThreadId));
|
|
479
|
+
}
|
|
282
480
|
if (state.status === 'failed') {
|
|
283
481
|
effects.push(fail(state.error ?? 'Workflow execution failed'));
|
|
284
482
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swirls/looms",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Pluggable event-sourced runtime for durable agents and workflows",
|
|
5
5
|
"homepage": "https://github.com/ByteSliceHQ/looms#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
"types": "./dist/debugger/index.d.ts",
|
|
66
66
|
"import": "./dist/debugger/index.js"
|
|
67
67
|
},
|
|
68
|
+
"./evaluator": {
|
|
69
|
+
"types": "./dist/evaluator/index.d.ts",
|
|
70
|
+
"import": "./dist/evaluator/index.js"
|
|
71
|
+
},
|
|
68
72
|
"./projectors": {
|
|
69
73
|
"types": "./dist/projectors/index.d.ts",
|
|
70
74
|
"import": "./dist/projectors/index.js"
|
|
@@ -144,6 +148,7 @@
|
|
|
144
148
|
"@looms/cloudflare": "workspace:*",
|
|
145
149
|
"@looms/core": "workspace:*",
|
|
146
150
|
"@looms/debugger": "workspace:*",
|
|
151
|
+
"@looms/evaluator": "workspace:*",
|
|
147
152
|
"@looms/projectors": "workspace:*",
|
|
148
153
|
"@looms/react": "workspace:*",
|
|
149
154
|
"@looms/runtime": "workspace:*",
|
|
@@ -151,7 +156,7 @@
|
|
|
151
156
|
"@looms/testing": "workspace:*",
|
|
152
157
|
"@looms/workflow": "workspace:*",
|
|
153
158
|
"@types/bun": "^1.3.9",
|
|
154
|
-
"ai": "^7.0.
|
|
159
|
+
"ai": "^7.0.105",
|
|
155
160
|
"postgres": "^3.4.0",
|
|
156
161
|
"react": "19",
|
|
157
162
|
"react-dom": "19",
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { encodeLoomsEvent, type EncodedLoomsEvent, type EventStore } from '@swirls/looms/core';
|
|
2
|
-
import type { LoomsRuntime } from './runtime';
|
|
3
|
-
export { encodeLoomsEvent, type EncodedLoomsEvent };
|
|
4
|
-
export declare function handleEventsApi(req: Request, runtime: LoomsRuntime, store: EventStore): Promise<Response>;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { Effect, Predicate } from 'effect';
|
|
2
|
-
import { decodeAppendableEvent, encodeLoomsEvent, EventStoreConflictError, EventStoreTag, } from '@swirls/looms/core';
|
|
3
|
-
import { createEventStreamResponse } from './sse.js';
|
|
4
|
-
export { encodeLoomsEvent };
|
|
5
|
-
function readCursor(req, url) {
|
|
6
|
-
const lastEventId = req.headers.get('last-event-id');
|
|
7
|
-
const raw = lastEventId !== null && lastEventId !== ''
|
|
8
|
-
? lastEventId
|
|
9
|
-
: (url.searchParams.get('cursor') ?? '0');
|
|
10
|
-
const parsed = Number(raw);
|
|
11
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
12
|
-
}
|
|
13
|
-
function readRunId(url) {
|
|
14
|
-
return url.searchParams.get('runId');
|
|
15
|
-
}
|
|
16
|
-
export async function handleEventsApi(req, runtime, store) {
|
|
17
|
-
const url = new URL(req.url);
|
|
18
|
-
if (req.method === 'HEAD') {
|
|
19
|
-
return new Response(null, { status: 200 });
|
|
20
|
-
}
|
|
21
|
-
if (req.method === 'GET') {
|
|
22
|
-
const runId = readRunId(url);
|
|
23
|
-
if (!runId) {
|
|
24
|
-
return Response.json({ error: 'runId required' }, { status: 400 });
|
|
25
|
-
}
|
|
26
|
-
const cursor = readCursor(req, url);
|
|
27
|
-
const acceptHeader = req.headers.get('accept') ?? '';
|
|
28
|
-
const isLive = url.searchParams.get('live') === 'true' ||
|
|
29
|
-
url.searchParams.get('live') === '1' ||
|
|
30
|
-
acceptHeader.includes('text/event-stream');
|
|
31
|
-
if (!isLive) {
|
|
32
|
-
const events = await Effect.runPromise(store.read(runId, { fromSeq: cursor + 1 }));
|
|
33
|
-
const bounds = store.bounds
|
|
34
|
-
? await Effect.runPromise(store.bounds(runId))
|
|
35
|
-
: { head: events[0]?.seq ?? 1, tail: await Effect.runPromise(store.tail(runId)) };
|
|
36
|
-
return Response.json({
|
|
37
|
-
batch: events.map(encodeLoomsEvent),
|
|
38
|
-
head: bounds.head,
|
|
39
|
-
tail: bounds.tail,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return createEventStreamResponse({
|
|
43
|
-
signal: req.signal,
|
|
44
|
-
request: req,
|
|
45
|
-
store,
|
|
46
|
-
runId,
|
|
47
|
-
fromSeq: cursor + 1,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
if (req.method === 'POST') {
|
|
51
|
-
const body = await req.json();
|
|
52
|
-
const runId = readRunId(url) ??
|
|
53
|
-
(Predicate.isReadonlyObject(body) && Predicate.isString(body.runId) ? body.runId : null);
|
|
54
|
-
if (!runId) {
|
|
55
|
-
return Response.json({ error: 'runId required' }, { status: 400 });
|
|
56
|
-
}
|
|
57
|
-
const batch = Predicate.isReadonlyObject(body) && Array.isArray(body.batch) ? body.batch : [];
|
|
58
|
-
const expectedTail = Predicate.isReadonlyObject(body) && Predicate.isNumber(body.expectedTail)
|
|
59
|
-
? body.expectedTail
|
|
60
|
-
: 0;
|
|
61
|
-
const tail = await Effect.runPromise(store.tail(runId));
|
|
62
|
-
if (expectedTail !== tail) {
|
|
63
|
-
return Response.json({ _tag: 'ServerAheadError', expected: expectedTail, actual: tail }, { status: 409 });
|
|
64
|
-
}
|
|
65
|
-
const appendable = batch
|
|
66
|
-
.map((item) => {
|
|
67
|
-
// SAFETY: push batch items are host-encoded envelopes.
|
|
68
|
-
return decodeAppendableEvent(item, runId);
|
|
69
|
-
})
|
|
70
|
-
.filter((item) => item !== undefined);
|
|
71
|
-
try {
|
|
72
|
-
await Effect.runPromise(store.append(runId, appendable, { expectedTail }));
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
if (err instanceof EventStoreConflictError) {
|
|
76
|
-
return Response.json({ _tag: 'ServerAheadError', expected: err.expectedTail, actual: err.actualTail }, { status: 409 });
|
|
77
|
-
}
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
await Effect.runPromise(Effect.provideService(runtime.wake(runId), EventStoreTag, store));
|
|
81
|
-
return Response.json({ ok: true });
|
|
82
|
-
}
|
|
83
|
-
return new Response('Method Not Allowed', { status: 405 });
|
|
84
|
-
}
|