@swirls/looms 0.4.0 → 0.5.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 +55 -0
- package/dist/actor/cell.d.ts +21 -2
- package/dist/actor/cell.js +15 -3
- package/dist/actor/host.d.ts +10 -2
- package/dist/actor/host.js +27 -4
- package/dist/actor/index.d.ts +2 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/routing.d.ts +3 -0
- package/dist/actor/routing.js +13 -2
- package/dist/agent/definitions-store.d.ts +1 -1
- package/dist/agent/definitions-store.js +1 -1
- package/dist/agent/definitions.d.ts +48 -0
- package/dist/agent/definitions.js +44 -0
- package/dist/agent/effects.d.ts +184 -0
- package/dist/agent/effects.js +172 -40
- package/dist/agent/events.d.ts +89 -0
- package/dist/agent/events.js +25 -0
- package/dist/agent/module.d.ts +412 -0
- package/dist/agent/module.js +5 -1
- package/dist/agent/scope.d.ts +88 -0
- package/dist/agent/threads.js +39 -3
- package/dist/agent/types.d.ts +4 -0
- package/dist/approval/effects.d.ts +40 -0
- package/dist/approval/module.d.ts +80 -0
- package/dist/client/client.d.ts +468 -119
- package/dist/client/client.js +214 -21
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sse.d.ts +1 -1
- package/dist/client/sse.js +2 -2
- package/dist/cloudflare/durable-object.d.ts +66 -2
- package/dist/cloudflare/durable-object.js +96 -1
- package/dist/cloudflare/event-store.d.ts +7 -3
- package/dist/cloudflare/event-store.js +16 -3
- package/dist/cloudflare/index.d.ts +7 -1
- package/dist/cloudflare/index.js +7 -1
- package/dist/cloudflare/memory-queue.d.ts +18 -0
- package/dist/cloudflare/memory-queue.js +53 -0
- package/dist/cloudflare/queue-consumer.d.ts +52 -0
- package/dist/cloudflare/queue-consumer.js +102 -0
- package/dist/cloudflare/queue-message.d.ts +31 -0
- package/dist/cloudflare/queue-message.js +33 -0
- package/dist/cloudflare/queue-pull.d.ts +62 -0
- package/dist/cloudflare/queue-pull.js +130 -0
- package/dist/cloudflare/queue-worker.d.ts +39 -0
- package/dist/cloudflare/queue-worker.js +62 -0
- package/dist/cloudflare/r2-blob-store.d.ts +33 -0
- package/dist/cloudflare/r2-blob-store.js +60 -0
- package/dist/cloudflare/router.d.ts +4 -1
- package/dist/cloudflare/router.js +8 -3
- package/dist/cloudflare/websocket.d.ts +27 -0
- package/dist/cloudflare/websocket.js +215 -0
- package/dist/core/blob-offload.d.ts +21 -0
- package/dist/core/blob-offload.js +143 -0
- package/dist/core/blob-store.d.ts +45 -0
- package/dist/core/blob-store.js +164 -0
- package/dist/core/blobs.d.ts +37 -0
- package/dist/core/blobs.js +160 -0
- package/dist/core/definition-resolver.d.ts +40 -0
- package/dist/core/definition-resolver.js +129 -0
- package/dist/core/definition-store.d.ts +6 -0
- package/dist/core/definition-store.js +37 -1
- package/dist/core/effects.d.ts +86 -3
- package/dist/core/effects.js +99 -3
- package/dist/core/envelope.d.ts +9 -0
- package/dist/core/envelope.js +4 -0
- package/dist/core/event-codec.js +3 -0
- package/dist/core/fold.js +83 -1
- package/dist/core/ids.d.ts +2 -0
- package/dist/core/ids.js +4 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/launcher.d.ts +97 -0
- package/dist/core/launcher.js +67 -0
- package/dist/core/module-scope.d.ts +7 -3
- package/dist/core/module-scope.js +3 -0
- package/dist/core/module.d.ts +10 -0
- package/dist/core/projection.js +4 -0
- package/dist/core/protocol.d.ts +36 -1
- package/dist/core/protocol.js +35 -0
- package/dist/core/published.d.ts +2 -0
- package/dist/core/published.js +4 -1
- package/dist/core/replay.d.ts +96 -0
- package/dist/core/run-context.d.ts +33 -0
- package/dist/core/run-context.js +23 -0
- package/dist/core/sqlite-store.js +57 -5
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.js +19 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +7 -0
- package/dist/core/subscription.d.ts +97 -0
- package/dist/core/subscription.js +224 -0
- package/dist/core/thread.d.ts +7 -0
- package/dist/core/thread.js +1 -0
- package/dist/core/trace.d.ts +21 -0
- package/dist/core/trace.js +45 -0
- package/dist/core/worker-callback.d.ts +25 -1
- package/dist/debugger/app/assets/{index-CgDzWJg7.css → index-B7GdT1UA.css} +1 -1
- package/dist/debugger/app/assets/index-C31SnCuN.js +22 -0
- package/dist/debugger/app/assets/routes-DPA3O5AF.js +47 -0
- package/dist/debugger/app/index.html +2 -2
- package/dist/debugger/components/event-inspector.d.ts +3 -1
- package/dist/debugger/components/event-inspector.js +25 -4
- package/dist/debugger/components/event-stream.d.ts +4 -1
- package/dist/debugger/components/event-stream.js +2 -2
- package/dist/debugger/components/run-tree.d.ts +5 -2
- package/dist/debugger/components/run-tree.js +10 -4
- package/dist/debugger/context.d.ts +8 -1
- package/dist/debugger/context.js +15 -3
- package/dist/debugger/events/protocol-summarize.js +18 -3
- package/dist/debugger/index.d.ts +1 -1
- package/dist/debugger/index.js +1 -1
- package/dist/debugger/shell/debugger-shell.js +1 -1
- package/dist/debugger/shell/events-pane.js +4 -3
- package/dist/debugger/styles.css +1 -0
- package/dist/evaluator/definitions-store.d.ts +3 -4
- package/dist/evaluator/definitions-store.js +3 -3
- package/dist/evaluator/definitions.d.ts +4 -1
- package/dist/evaluator/definitions.js +5 -2
- package/dist/evaluator/effects.d.ts +74 -33
- package/dist/evaluator/effects.js +6 -2
- package/dist/evaluator/module.d.ts +150 -66
- package/dist/evaluator/module.js +7 -3
- package/dist/evaluator/threads.d.ts +2 -0
- package/dist/evaluator/threads.js +4 -0
- package/dist/projectors/event-log.d.ts +87 -0
- package/dist/projectors/event-log.js +396 -0
- package/dist/projectors/index-model.d.ts +12 -0
- package/dist/projectors/index-model.js +12 -3
- package/dist/projectors/memory.js +7 -0
- package/dist/projectors/postgres.d.ts +1 -0
- package/dist/projectors/postgres.js +32 -15
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +26 -1
- package/dist/projectors/sqlite.js +47 -19
- package/dist/projectors/with-projectors.d.ts +7 -1
- package/dist/projectors/with-projectors.js +13 -3
- package/dist/react/materialize.js +5 -1
- package/dist/react/react.d.ts +10 -2
- package/dist/react/react.js +43 -19
- package/dist/react/store.d.ts +11 -2
- package/dist/react/store.js +104 -12
- package/dist/react/tables.d.ts +4 -1
- package/dist/runtime/cancellation.d.ts +2 -1
- package/dist/runtime/cancellation.js +35 -2
- package/dist/runtime/child-run.d.ts +10 -0
- package/dist/runtime/child-run.js +58 -0
- package/dist/runtime/deadline-transition-planner.js +11 -2
- package/dist/runtime/dispatch-effect.d.ts +2 -2
- package/dist/runtime/dispatch-effect.js +43 -19
- package/dist/runtime/errors.d.ts +14 -1
- package/dist/runtime/errors.js +10 -0
- package/dist/runtime/filesystem-blob-store.d.ts +6 -0
- package/dist/runtime/filesystem-blob-store.js +59 -0
- package/dist/runtime/helpers.d.ts +15 -6
- package/dist/runtime/helpers.js +46 -6
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/local-effect-executor.d.ts +11 -2
- package/dist/runtime/local-effect-executor.js +80 -4
- package/dist/runtime/looms.d.ts +38 -4
- package/dist/runtime/looms.js +17 -3
- package/dist/runtime/repository.d.ts +5 -4
- package/dist/runtime/repository.js +5 -2
- package/dist/runtime/retry-policy.d.ts +5 -0
- package/dist/runtime/retry-policy.js +10 -2
- package/dist/runtime/run-start.d.ts +1 -1
- package/dist/runtime/run-start.js +10 -0
- package/dist/runtime/runtime.d.ts +2 -2
- package/dist/runtime/runtime.js +191 -68
- package/dist/runtime/server/auth.d.ts +3 -0
- package/dist/runtime/server/fetch-handler.d.ts +2 -2
- package/dist/runtime/server/fetch-handler.js +35 -4
- package/dist/runtime/server/grant.d.ts +49 -0
- package/dist/runtime/server/grant.js +177 -0
- package/dist/runtime/server/routes/effects.js +47 -5
- package/dist/runtime/server/routes/runs.js +6 -1
- package/dist/runtime/server/run-route.d.ts +18 -0
- package/dist/runtime/server/run-route.js +26 -0
- package/dist/runtime/server.d.ts +3 -1
- package/dist/runtime/server.js +3 -1
- package/dist/runtime/sse.js +26 -4
- package/dist/runtime/subscription.d.ts +8 -0
- package/dist/runtime/subscription.js +23 -0
- package/dist/runtime/types.d.ts +87 -8
- package/dist/runtime/wake-engine.d.ts +4 -2
- package/dist/runtime/wake-engine.js +57 -6
- package/dist/runtime/worker-callbacks.d.ts +12 -2
- package/dist/runtime/worker-callbacks.js +102 -72
- package/dist/runtime/worker-effect-dispatch.d.ts +14 -5
- package/dist/runtime/worker-effect-dispatch.js +84 -18
- package/dist/schedule/debugger/index.d.ts +4 -0
- package/dist/schedule/debugger/index.js +11 -0
- package/dist/schedule/debugger/schedule-view.d.ts +3 -0
- package/dist/schedule/debugger/schedule-view.js +68 -0
- package/dist/schedule/debugger/summarize.d.ts +2 -0
- package/dist/schedule/debugger/summarize.js +43 -0
- package/dist/schedule/definitions-store.d.ts +10 -0
- package/dist/schedule/definitions-store.js +8 -0
- package/dist/schedule/definitions.d.ts +57 -0
- package/dist/schedule/definitions.js +30 -0
- package/dist/schedule/effects.d.ts +756 -0
- package/dist/schedule/effects.js +244 -0
- package/dist/schedule/ensure.d.ts +55 -0
- package/dist/schedule/ensure.js +109 -0
- package/dist/schedule/events.d.ts +116 -0
- package/dist/schedule/events.js +49 -0
- package/dist/schedule/index.d.ts +11 -0
- package/dist/schedule/index.js +11 -0
- package/dist/schedule/module.d.ts +1474 -0
- package/dist/schedule/module.js +22 -0
- package/dist/schedule/projections.d.ts +55 -0
- package/dist/schedule/projections.js +88 -0
- package/dist/schedule/scope.d.ts +121 -0
- package/dist/schedule/scope.js +7 -0
- package/dist/schedule/signals.d.ts +25 -0
- package/dist/schedule/signals.js +33 -0
- package/dist/schedule/specs.d.ts +59 -0
- package/dist/schedule/specs.js +231 -0
- package/dist/schedule/threads.d.ts +25 -0
- package/dist/schedule/threads.js +259 -0
- package/dist/testing/corpus-module.d.ts +34 -0
- package/dist/testing/corpus-module.js +87 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +5 -6
- package/dist/testing/replay-corpus.d.ts +22 -0
- package/dist/testing/replay-corpus.js +50 -0
- package/dist/workflow/definitions-store.d.ts +1 -1
- package/dist/workflow/definitions-store.js +1 -1
- package/dist/workflow/definitions.d.ts +14 -0
- package/dist/workflow/effects.d.ts +122 -0
- package/dist/workflow/effects.js +16 -6
- package/dist/workflow/events.d.ts +42 -0
- package/dist/workflow/events.js +4 -0
- package/dist/workflow/module.d.ts +345 -32
- package/dist/workflow/module.js +7 -3
- package/dist/workflow/scope.d.ts +42 -0
- package/dist/workflow/threads.d.ts +72 -0
- package/dist/workflow/threads.js +106 -38
- package/package.json +10 -1
- package/dist/debugger/app/assets/index-D--9esm9.js +0 -22
- package/dist/debugger/app/assets/routes-Dakrdusf.js +0 -47
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DefinitionStore, type JsonValue, type ThreadDefinition } from '@swirls/looms/core';
|
|
2
|
+
import type { ScheduleDefinition, ScheduleMisfire, ScheduleOverlap } from './definitions';
|
|
3
|
+
import { type ScheduleSpec } from './specs';
|
|
4
|
+
export interface ScheduleState {
|
|
5
|
+
readonly spec: ScheduleSpec | null;
|
|
6
|
+
readonly input: JsonValue | null;
|
|
7
|
+
readonly endAt: number | null;
|
|
8
|
+
readonly maxFires: number | null;
|
|
9
|
+
readonly startAt: number | null;
|
|
10
|
+
readonly paused: boolean;
|
|
11
|
+
readonly retired: boolean;
|
|
12
|
+
readonly generation: number;
|
|
13
|
+
readonly nextAt: number | null;
|
|
14
|
+
readonly fireCount: number;
|
|
15
|
+
readonly lastFiredRunId: string | null;
|
|
16
|
+
readonly pendingTriggers: readonly string[];
|
|
17
|
+
readonly done: boolean;
|
|
18
|
+
readonly invalid: string | null;
|
|
19
|
+
readonly rejection: string | null;
|
|
20
|
+
readonly misfire: ScheduleMisfire;
|
|
21
|
+
readonly overlap: ScheduleOverlap;
|
|
22
|
+
readonly definitionName: string;
|
|
23
|
+
readonly definitionVersion: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function createScheduleThread(definitions: DefinitionStore<ScheduleDefinition>, minIntervalMs: number): ThreadDefinition<ScheduleState>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Option, Schema } from 'effect';
|
|
2
|
+
import { complete, emit, fail, invoke, stringifyJson, } from '@swirls/looms/core';
|
|
3
|
+
import { ScheduleStartSchema } from './definitions.js';
|
|
4
|
+
import { fireEffect, fireInputOf, planEffect, planInputOf } from './effects.js';
|
|
5
|
+
import { ScheduleUpdatedPayloadSchema } from './events.js';
|
|
6
|
+
import { scheduleModule } from './scope.js';
|
|
7
|
+
import { specRejection } from './specs.js';
|
|
8
|
+
const PENDING_TRIGGER_CAP = 32;
|
|
9
|
+
const decodeStart = Schema.decodeUnknownOption(ScheduleStartSchema);
|
|
10
|
+
const decodeUpdate = Schema.decodeUnknownOption(ScheduleUpdatedPayloadSchema);
|
|
11
|
+
export function createScheduleThread(definitions, minIntervalMs) {
|
|
12
|
+
return scheduleModule.thread({
|
|
13
|
+
kind: 'schedule',
|
|
14
|
+
input: ScheduleStartSchema,
|
|
15
|
+
forgetsCompletedEffects: true,
|
|
16
|
+
initialState: (ctx) => {
|
|
17
|
+
const definition = definitions.get(ctx.definitionName, ctx.definitionVersion);
|
|
18
|
+
const base = {
|
|
19
|
+
input: null,
|
|
20
|
+
endAt: null,
|
|
21
|
+
maxFires: null,
|
|
22
|
+
startAt: null,
|
|
23
|
+
paused: false,
|
|
24
|
+
retired: false,
|
|
25
|
+
generation: 0,
|
|
26
|
+
nextAt: null,
|
|
27
|
+
fireCount: 0,
|
|
28
|
+
lastFiredRunId: null,
|
|
29
|
+
pendingTriggers: [],
|
|
30
|
+
done: false,
|
|
31
|
+
rejection: null,
|
|
32
|
+
misfire: definition?.misfire ?? 'fire-once',
|
|
33
|
+
overlap: definition?.overlap ?? 'allow',
|
|
34
|
+
definitionName: ctx.definitionName,
|
|
35
|
+
definitionVersion: ctx.definitionVersion,
|
|
36
|
+
};
|
|
37
|
+
if (!definition) {
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
spec: null,
|
|
41
|
+
invalid: `Unknown schedule definition "${ctx.definitionName}@${ctx.definitionVersion}"`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const start = Option.getOrUndefined(decodeStart(ctx.input));
|
|
45
|
+
const config = start && start !== null ? start : {};
|
|
46
|
+
const spec = config.spec ?? definition.spec;
|
|
47
|
+
const invalid = specRejection(spec, minIntervalMs) ?? boundsError(config);
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
spec: invalid ? null : spec,
|
|
51
|
+
input: config.input ?? null,
|
|
52
|
+
endAt: config.endAt ?? null,
|
|
53
|
+
maxFires: config.maxFires ?? null,
|
|
54
|
+
startAt: config.startAt ?? null,
|
|
55
|
+
invalid: invalid ?? null,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
step(state, event) {
|
|
59
|
+
switch (event.type) {
|
|
60
|
+
case 'schedule.planned': {
|
|
61
|
+
if (event.payload.generation !== state.generation) {
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
if (event.payload.nextAt === null) {
|
|
65
|
+
return { ...state, spec: event.payload.spec, nextAt: null, done: true };
|
|
66
|
+
}
|
|
67
|
+
return { ...state, spec: event.payload.spec, nextAt: event.payload.nextAt };
|
|
68
|
+
}
|
|
69
|
+
case 'schedule.fired': {
|
|
70
|
+
const pending = dropTrigger(state.pendingTriggers, event.payload.triggerId);
|
|
71
|
+
const fireCount = state.fireCount + 1;
|
|
72
|
+
if (event.payload.manual) {
|
|
73
|
+
const done = state.maxFires !== null && fireCount >= state.maxFires;
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
fireCount,
|
|
77
|
+
lastFiredRunId: event.payload.runId,
|
|
78
|
+
nextAt: done ? null : state.nextAt,
|
|
79
|
+
done: state.done || done,
|
|
80
|
+
pendingTriggers: pending,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (event.payload.generation !== state.generation) {
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
const done = event.payload.nextAt === null;
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
fireCount,
|
|
90
|
+
lastFiredRunId: event.payload.runId,
|
|
91
|
+
nextAt: event.payload.nextAt,
|
|
92
|
+
done,
|
|
93
|
+
pendingTriggers: pending,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
case 'schedule.skipped': {
|
|
97
|
+
const pending = dropTrigger(state.pendingTriggers, event.payload.triggerId);
|
|
98
|
+
if (event.payload.manual) {
|
|
99
|
+
return { ...state, pendingTriggers: pending };
|
|
100
|
+
}
|
|
101
|
+
if (event.payload.generation !== state.generation) {
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
const done = event.payload.nextAt === null;
|
|
105
|
+
return {
|
|
106
|
+
...state,
|
|
107
|
+
nextAt: event.payload.nextAt,
|
|
108
|
+
done,
|
|
109
|
+
pendingTriggers: pending,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
case 'schedule.updated':
|
|
113
|
+
return applyUpdate(state, event.payload, definitions, minIntervalMs);
|
|
114
|
+
case 'schedule.paused':
|
|
115
|
+
return { ...state, paused: true, nextAt: null };
|
|
116
|
+
case 'schedule.resumed':
|
|
117
|
+
if (!state.paused && !state.retired) {
|
|
118
|
+
return state;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
paused: false,
|
|
123
|
+
retired: false,
|
|
124
|
+
nextAt: null,
|
|
125
|
+
generation: state.generation + 1,
|
|
126
|
+
rejection: null,
|
|
127
|
+
};
|
|
128
|
+
case 'schedule.retired':
|
|
129
|
+
return {
|
|
130
|
+
...state,
|
|
131
|
+
retired: true,
|
|
132
|
+
paused: true,
|
|
133
|
+
nextAt: null,
|
|
134
|
+
pendingTriggers: [],
|
|
135
|
+
};
|
|
136
|
+
case 'schedule.triggered': {
|
|
137
|
+
const pending = state.pendingTriggers.length < PENDING_TRIGGER_CAP
|
|
138
|
+
? [...state.pendingTriggers, event.id]
|
|
139
|
+
: state.pendingTriggers;
|
|
140
|
+
return { ...state, pendingTriggers: pending };
|
|
141
|
+
}
|
|
142
|
+
case 'schedule.rejected':
|
|
143
|
+
return { ...state, rejection: null };
|
|
144
|
+
default:
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
effects(state, ctx) {
|
|
149
|
+
if (state.invalid && state.fireCount === 0 && state.nextAt === null) {
|
|
150
|
+
return [fail(state.invalid)];
|
|
151
|
+
}
|
|
152
|
+
const spec = state.spec;
|
|
153
|
+
if (state.done || !spec) {
|
|
154
|
+
return [complete({ fireCount: state.fireCount })];
|
|
155
|
+
}
|
|
156
|
+
const armed = { ...state, spec };
|
|
157
|
+
const effects = [];
|
|
158
|
+
if (state.rejection) {
|
|
159
|
+
effects.push(emit({
|
|
160
|
+
type: 'schedule.rejected',
|
|
161
|
+
payload: { reason: state.rejection },
|
|
162
|
+
threadId: ctx.threadId,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
const triggerId = state.pendingTriggers[0];
|
|
166
|
+
if (!state.retired && triggerId !== undefined) {
|
|
167
|
+
effects.push(invoke(fireEffect, fireInputOf(armed, state.nextAt ?? 0, true, triggerId), `manual:${String(triggerId)}`));
|
|
168
|
+
}
|
|
169
|
+
if (!state.paused && !state.retired) {
|
|
170
|
+
if (state.nextAt === null) {
|
|
171
|
+
effects.push(invoke(planEffect, planInputOf(armed), `plan:${state.generation}`));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const nextAt = state.nextAt;
|
|
175
|
+
effects.push(invoke(fireEffect, fireInputOf(armed, nextAt, false), `fire:${String(state.generation)}:${String(state.fireCount)}:${String(nextAt)}`, { at: nextAt }));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return effects;
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function applyUpdate(state, payload, definitions, minIntervalMs) {
|
|
183
|
+
const decoded = decodeUpdate(payload);
|
|
184
|
+
if (Option.isNone(decoded)) {
|
|
185
|
+
return { ...state, rejection: 'schedule update payload is invalid' };
|
|
186
|
+
}
|
|
187
|
+
const patch = decoded.value;
|
|
188
|
+
const definitionName = patch.definitionName ?? state.definitionName;
|
|
189
|
+
const definitionVersion = patch.definitionVersion ?? state.definitionVersion;
|
|
190
|
+
const definition = definitions.get(definitionName, definitionVersion);
|
|
191
|
+
if ((patch.definitionName !== undefined || patch.definitionVersion !== undefined) &&
|
|
192
|
+
!definition) {
|
|
193
|
+
return {
|
|
194
|
+
...state,
|
|
195
|
+
rejection: `Unknown schedule definition "${definitionName}@${definitionVersion}"`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (patch.spec) {
|
|
199
|
+
const rejection = specRejection(patch.spec, minIntervalMs);
|
|
200
|
+
if (rejection) {
|
|
201
|
+
return { ...state, rejection };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const spec = patch.spec ?? state.spec;
|
|
205
|
+
if (!spec) {
|
|
206
|
+
return { ...state, rejection: 'schedule has no spec' };
|
|
207
|
+
}
|
|
208
|
+
const input = patch.input === undefined ? state.input : patch.input;
|
|
209
|
+
const endAt = patch.endAt === undefined ? state.endAt : patch.endAt;
|
|
210
|
+
const maxFires = patch.maxFires === undefined ? state.maxFires : patch.maxFires;
|
|
211
|
+
const bounds = boundsError({ endAt, maxFires, startAt: state.startAt });
|
|
212
|
+
if (bounds) {
|
|
213
|
+
return { ...state, rejection: bounds };
|
|
214
|
+
}
|
|
215
|
+
const misfire = definition ? definition.misfire : state.misfire;
|
|
216
|
+
const overlap = definition ? definition.overlap : state.overlap;
|
|
217
|
+
if (stringifyJson(spec) === stringifyJson(state.spec) &&
|
|
218
|
+
stringifyJson(input) === stringifyJson(state.input) &&
|
|
219
|
+
endAt === state.endAt &&
|
|
220
|
+
maxFires === state.maxFires &&
|
|
221
|
+
definitionName === state.definitionName &&
|
|
222
|
+
definitionVersion === state.definitionVersion &&
|
|
223
|
+
misfire === state.misfire &&
|
|
224
|
+
overlap === state.overlap) {
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
...state,
|
|
229
|
+
spec,
|
|
230
|
+
input,
|
|
231
|
+
endAt,
|
|
232
|
+
maxFires,
|
|
233
|
+
definitionName,
|
|
234
|
+
definitionVersion,
|
|
235
|
+
misfire,
|
|
236
|
+
overlap,
|
|
237
|
+
generation: state.generation + 1,
|
|
238
|
+
nextAt: null,
|
|
239
|
+
rejection: null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function boundsError(config) {
|
|
243
|
+
if (config.maxFires != null && (!Number.isInteger(config.maxFires) || config.maxFires < 1)) {
|
|
244
|
+
return 'maxFires must be an integer of at least 1';
|
|
245
|
+
}
|
|
246
|
+
if (config.startAt != null && !Number.isFinite(config.startAt)) {
|
|
247
|
+
return 'startAt must be a finite epoch timestamp';
|
|
248
|
+
}
|
|
249
|
+
if (config.endAt != null && !Number.isFinite(config.endAt)) {
|
|
250
|
+
return 'endAt must be a finite epoch timestamp';
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
function dropTrigger(pending, triggerId) {
|
|
255
|
+
if (triggerId === undefined) {
|
|
256
|
+
return pending;
|
|
257
|
+
}
|
|
258
|
+
return pending.filter((id) => id !== triggerId);
|
|
259
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type EventInput, type JsonValue } from '@swirls/looms/core';
|
|
2
|
+
/**
|
|
3
|
+
* A small production-shaped module used to record and replay the upgrade-safety corpus.
|
|
4
|
+
* Keep its event and effect contracts stable; add a new type when meaning changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const corpusDefinition: {
|
|
7
|
+
kind: string;
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const corpusChargeV1 = "payments.charge";
|
|
12
|
+
export declare const corpusChargeV2 = "payments.charge@v2";
|
|
13
|
+
export declare function corpusModule(options?: {
|
|
14
|
+
readonly charge?: (input: JsonValue) => ReadonlyArray<EventInput>;
|
|
15
|
+
}): import("@swirls/looms/core").RuntimeModule<"corpus", import("@swirls/looms/core").EventCatalog<"corpus", {
|
|
16
|
+
charged: import("@swirls/looms/core").CatalogEventSpec<{
|
|
17
|
+
amount: number;
|
|
18
|
+
version: string;
|
|
19
|
+
}>;
|
|
20
|
+
approved: import("@swirls/looms/core").CatalogEventSpec<{
|
|
21
|
+
decision: 'approve' | 'reject';
|
|
22
|
+
}>;
|
|
23
|
+
}>, {
|
|
24
|
+
charge: import("@swirls/looms/core").EffectDefinition<JsonValue, never, EventInput>;
|
|
25
|
+
chargeV2: import("@swirls/looms/core").EffectDefinition<JsonValue, never, EventInput>;
|
|
26
|
+
}, {
|
|
27
|
+
'corpus.checkout': import("@swirls/looms/core").ThreadDefinition<{
|
|
28
|
+
readonly amount: number;
|
|
29
|
+
readonly charged: boolean;
|
|
30
|
+
readonly decision: "approve" | "reject" | null;
|
|
31
|
+
}>;
|
|
32
|
+
}, {
|
|
33
|
+
readonly [key: string]: import("@swirls/looms/core").ProjectionDefinition<unknown>;
|
|
34
|
+
}, import("effect/Context").Service.Any>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Option, Schema } from 'effect';
|
|
2
|
+
import { complete, defineEffect, defineEventCatalog, defineRuntimeModule, defineThread, fail, invoke, payload, wait, } from '@swirls/looms/core';
|
|
3
|
+
/**
|
|
4
|
+
* A small production-shaped module used to record and replay the upgrade-safety corpus.
|
|
5
|
+
* Keep its event and effect contracts stable; add a new type when meaning changes.
|
|
6
|
+
*/
|
|
7
|
+
export const corpusDefinition = {
|
|
8
|
+
kind: 'corpus.checkout',
|
|
9
|
+
name: 'checkout',
|
|
10
|
+
version: 'v1',
|
|
11
|
+
};
|
|
12
|
+
export const corpusChargeV1 = 'payments.charge';
|
|
13
|
+
export const corpusChargeV2 = 'payments.charge@v2';
|
|
14
|
+
const DecisionSchema = Schema.Union([Schema.Literal('approve'), Schema.Literal('reject')]);
|
|
15
|
+
const ApprovedPayloadSchema = Schema.Struct({ decision: DecisionSchema });
|
|
16
|
+
const CheckoutInputSchema = Schema.Finite;
|
|
17
|
+
const decodeApproved = Schema.decodeUnknownOption(ApprovedPayloadSchema);
|
|
18
|
+
const decodeCheckoutInput = Schema.decodeUnknownOption(CheckoutInputSchema);
|
|
19
|
+
export function corpusModule(options) {
|
|
20
|
+
const charge = defineEffect({
|
|
21
|
+
type: corpusChargeV1,
|
|
22
|
+
retry: { maxAttempts: 3, backoffMs: 1, scheduleToStartTimeoutMs: 60_000 },
|
|
23
|
+
execute: options?.charge,
|
|
24
|
+
});
|
|
25
|
+
const chargeV2 = defineEffect({
|
|
26
|
+
type: corpusChargeV2,
|
|
27
|
+
retry: { maxAttempts: 1, scheduleToStartTimeoutMs: 60_000 },
|
|
28
|
+
execute: options?.charge,
|
|
29
|
+
});
|
|
30
|
+
return defineRuntimeModule({
|
|
31
|
+
namespace: 'corpus',
|
|
32
|
+
protocolVersion: '1.0.0',
|
|
33
|
+
events: defineEventCatalog('corpus', {
|
|
34
|
+
charged: payload(),
|
|
35
|
+
approved: payload(),
|
|
36
|
+
}),
|
|
37
|
+
effects: { charge, chargeV2 },
|
|
38
|
+
threads: {
|
|
39
|
+
'corpus.checkout': defineThread({
|
|
40
|
+
kind: 'corpus.checkout',
|
|
41
|
+
shape: Schema.Struct({
|
|
42
|
+
amount: Schema.Finite,
|
|
43
|
+
charged: Schema.Boolean,
|
|
44
|
+
decision: Schema.NullOr(DecisionSchema),
|
|
45
|
+
}),
|
|
46
|
+
initialState: (ctx) => {
|
|
47
|
+
const decision = null;
|
|
48
|
+
return {
|
|
49
|
+
amount: Option.getOrElse(decodeCheckoutInput(ctx.input), () => 0),
|
|
50
|
+
charged: false,
|
|
51
|
+
decision,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
step: (state, event) => {
|
|
55
|
+
if (event.type === 'corpus.approved') {
|
|
56
|
+
return Option.match(decodeApproved(event.payload), {
|
|
57
|
+
onNone: () => state,
|
|
58
|
+
onSome: ({ decision }) => ({ ...state, decision }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (event.type === 'corpus.charged') {
|
|
62
|
+
return { ...state, charged: true };
|
|
63
|
+
}
|
|
64
|
+
return state;
|
|
65
|
+
},
|
|
66
|
+
effects: (state) => {
|
|
67
|
+
if (state.decision === 'reject') {
|
|
68
|
+
return [fail('rejected')];
|
|
69
|
+
}
|
|
70
|
+
if (state.decision === null) {
|
|
71
|
+
return [
|
|
72
|
+
wait({
|
|
73
|
+
waitId: 'approval',
|
|
74
|
+
on: { type: 'corpus.approved' },
|
|
75
|
+
}),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (!state.charged) {
|
|
79
|
+
return [invoke(charge, { amount: state.amount }, 'charge')];
|
|
80
|
+
}
|
|
81
|
+
return [complete({ amount: state.amount, ok: true })];
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
definitions: [corpusDefinition],
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface TestRuntime {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function createTestRuntime(modules: readonly AnyRuntimeModule[]): Promise<TestRuntime>;
|
|
10
10
|
export declare function assertReplayDeterministic(test: TestRuntime, runId: string): Promise<void>;
|
|
11
|
+
export { assertCorpusEntry, assertReplayDeterministicEvents, corpusRegistry, readCorpusEntry, type CorpusEntry, } from './replay-corpus';
|
|
12
|
+
export { corpusChargeV1, corpusChargeV2, corpusDefinition, corpusModule } from './corpus-module';
|
|
11
13
|
export declare function moduleConformance(module: AnyRuntimeModule): string[];
|
|
12
14
|
export type EventStoreFaultOperation = 'append' | 'read' | 'tail' | 'bounds' | 'trim' | 'listRuns';
|
|
13
15
|
export interface EventStoreFaultContext {
|
package/dist/testing/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, Predicate } from 'effect';
|
|
2
|
-
import { composeModules, EventStoreTag,
|
|
2
|
+
import { composeModules, EventStoreTag, makeMemoryEventStore, ModuleCompositionError, EventStoreError, snapshotStoreOf, withSnapshotStore, } from '@swirls/looms/core';
|
|
3
3
|
import { createRuntime } from '@swirls/looms/runtime';
|
|
4
|
+
import { assertReplayDeterministicEvents } from './replay-corpus.js';
|
|
4
5
|
function namedEffects(effects) {
|
|
5
6
|
const listed = [];
|
|
6
7
|
for (const key of Object.keys(effects)) {
|
|
@@ -27,13 +28,11 @@ export function createTestRuntime(modules) {
|
|
|
27
28
|
}
|
|
28
29
|
export function assertReplayDeterministic(test, runId) {
|
|
29
30
|
return test.run(test.runtime.getEvents(runId)).then((events) => {
|
|
30
|
-
|
|
31
|
-
const second = foldRun(events, test.runtime.registry, { runId });
|
|
32
|
-
if (stringifyJson(first) !== stringifyJson(second)) {
|
|
33
|
-
throw new Error(`Replay is not deterministic for run ${runId}`);
|
|
34
|
-
}
|
|
31
|
+
assertReplayDeterministicEvents(events, test.runtime.registry, runId);
|
|
35
32
|
});
|
|
36
33
|
}
|
|
34
|
+
export { assertCorpusEntry, assertReplayDeterministicEvents, corpusRegistry, readCorpusEntry, } from './replay-corpus.js';
|
|
35
|
+
export { corpusChargeV1, corpusChargeV2, corpusDefinition, corpusModule } from './corpus-module.js';
|
|
37
36
|
export function moduleConformance(module) {
|
|
38
37
|
const errors = [];
|
|
39
38
|
if (!module.namespace) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AnyRuntimeModule, type EventEnvelope, type FoldRegistry, type JsonValue, type RunState } from '@swirls/looms/core';
|
|
2
|
+
export type CorpusEntry = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly runId: string;
|
|
5
|
+
readonly events: ReadonlyArray<EventEnvelope>;
|
|
6
|
+
readonly stateHash: string;
|
|
7
|
+
};
|
|
8
|
+
/** Modules every corpus fixture was recorded against. Keep them stable across releases. */
|
|
9
|
+
export declare function corpusRegistry(modules?: ReadonlyArray<AnyRuntimeModule>): FoldRegistry;
|
|
10
|
+
/**
|
|
11
|
+
* Fold the same log twice and require a byte-identical {@link RunState}. Use this in CI against a
|
|
12
|
+
* recorded corpus so a fold or protocol change that breaks replay fails before release.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertReplayDeterministicEvents(events: ReadonlyArray<EventEnvelope>, registry: FoldRegistry, runId: string): RunState;
|
|
15
|
+
/** Parse one recorded corpus JSON document. */
|
|
16
|
+
export declare function readCorpusEntry(raw: JsonValue): CorpusEntry;
|
|
17
|
+
/**
|
|
18
|
+
* Replay one recorded log: fold must be deterministic and must match the hash taken when the
|
|
19
|
+
* fixture was written. A changed hash means the fold or a domain reducer moved under a recorded
|
|
20
|
+
* production-shaped log.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertCorpusEntry(entry: CorpusEntry, registry?: FoldRegistry): RunState;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import { composeModules, EventEnvelopeSchema, foldRun, fromWireEvent, hashRunState, stringifyJson, } from '@swirls/looms/core';
|
|
3
|
+
import { corpusModule } from './corpus-module.js';
|
|
4
|
+
const CorpusEntrySchema = Schema.Struct({
|
|
5
|
+
name: Schema.String,
|
|
6
|
+
runId: Schema.String,
|
|
7
|
+
events: Schema.Array(EventEnvelopeSchema),
|
|
8
|
+
/** Hash of the folded state when the log was recorded. */
|
|
9
|
+
stateHash: Schema.String,
|
|
10
|
+
});
|
|
11
|
+
const decodeEntry = Schema.decodeUnknownSync(CorpusEntrySchema);
|
|
12
|
+
/** Modules every corpus fixture was recorded against. Keep them stable across releases. */
|
|
13
|
+
export function corpusRegistry(modules = [corpusModule()]) {
|
|
14
|
+
return composeModules(modules);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fold the same log twice and require a byte-identical {@link RunState}. Use this in CI against a
|
|
18
|
+
* recorded corpus so a fold or protocol change that breaks replay fails before release.
|
|
19
|
+
*/
|
|
20
|
+
export function assertReplayDeterministicEvents(events, registry, runId) {
|
|
21
|
+
const first = foldRun(events, registry, { runId });
|
|
22
|
+
const second = foldRun(events, registry, { runId });
|
|
23
|
+
if (stringifyJson(first) !== stringifyJson(second)) {
|
|
24
|
+
throw new Error(`Replay is not deterministic for run ${runId}`);
|
|
25
|
+
}
|
|
26
|
+
return first;
|
|
27
|
+
}
|
|
28
|
+
/** Parse one recorded corpus JSON document. */
|
|
29
|
+
export function readCorpusEntry(raw) {
|
|
30
|
+
const entry = decodeEntry(raw);
|
|
31
|
+
return {
|
|
32
|
+
name: entry.name,
|
|
33
|
+
runId: entry.runId,
|
|
34
|
+
stateHash: entry.stateHash,
|
|
35
|
+
events: entry.events.map(fromWireEvent),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Replay one recorded log: fold must be deterministic and must match the hash taken when the
|
|
40
|
+
* fixture was written. A changed hash means the fold or a domain reducer moved under a recorded
|
|
41
|
+
* production-shaped log.
|
|
42
|
+
*/
|
|
43
|
+
export function assertCorpusEntry(entry, registry = corpusRegistry()) {
|
|
44
|
+
const state = assertReplayDeterministicEvents(entry.events, registry, entry.runId);
|
|
45
|
+
const actual = hashRunState(state);
|
|
46
|
+
if (actual !== entry.stateHash) {
|
|
47
|
+
throw new Error(`Corpus "${entry.name}" folded to ${actual}, expected ${entry.stateHash}. Re-record only after confirming the change is intentional.`);
|
|
48
|
+
}
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
@@ -6,5 +6,5 @@ declare const WorkflowDefinitionsTag_base: Context.ServiceClass<WorkflowDefiniti
|
|
|
6
6
|
export declare class WorkflowDefinitionsTag extends WorkflowDefinitionsTag_base {
|
|
7
7
|
}
|
|
8
8
|
export declare function makeWorkflowDefinitionStore(definitions: ReadonlyArray<WorkflowDefinition>): WorkflowDefinitionStore;
|
|
9
|
-
export declare const WorkflowDefinitionsLive: (definitions:
|
|
9
|
+
export declare const WorkflowDefinitionsLive: (definitions: WorkflowDefinitionStore) => Layer.Layer<WorkflowDefinitionsTag, never, never>;
|
|
10
10
|
export {};
|
|
@@ -5,4 +5,4 @@ export class WorkflowDefinitionsTag extends Context.Service()('looms/WorkflowDef
|
|
|
5
5
|
export function makeWorkflowDefinitionStore(definitions) {
|
|
6
6
|
return makeDefinitionStore('workflow', definitions);
|
|
7
7
|
}
|
|
8
|
-
export const WorkflowDefinitionsLive = (definitions) => Layer.succeed(WorkflowDefinitionsTag,
|
|
8
|
+
export const WorkflowDefinitionsLive = (definitions) => Layer.succeed(WorkflowDefinitionsTag, definitions);
|
|
@@ -118,6 +118,16 @@ export interface WorkflowDefinition<TName extends string = string, TInput = Json
|
|
|
118
118
|
readonly inputLimitBytes?: number;
|
|
119
119
|
readonly outputLimitBytes?: number;
|
|
120
120
|
readonly checkpointLimitBytes?: number;
|
|
121
|
+
/**
|
|
122
|
+
* `thread` keeps spawned, mapped, and looped children in this run.
|
|
123
|
+
* `run` launches each child as its own run.
|
|
124
|
+
*/
|
|
125
|
+
readonly childExecution?: 'thread' | 'run';
|
|
126
|
+
/**
|
|
127
|
+
* When this run is cancelled, `terminate` cancels child runs immediately and `wait` cancels
|
|
128
|
+
* this run only after those children have finished cancelling. Ignored when children are threads.
|
|
129
|
+
*/
|
|
130
|
+
readonly childClose?: 'terminate' | 'wait';
|
|
121
131
|
output?(ctx: {
|
|
122
132
|
input: TInput;
|
|
123
133
|
results: {
|
|
@@ -125,6 +135,10 @@ export interface WorkflowDefinition<TName extends string = string, TInput = Json
|
|
|
125
135
|
};
|
|
126
136
|
}): TOutput;
|
|
127
137
|
}
|
|
138
|
+
export interface WorkflowChildMode {
|
|
139
|
+
readonly execution: 'thread' | 'run';
|
|
140
|
+
readonly close: 'terminate' | 'wait';
|
|
141
|
+
}
|
|
128
142
|
export type AnyWorkflowDefinition = WorkflowDefinition;
|
|
129
143
|
export declare function defineWorkflow<TName extends string, TInput = JsonValue, TOutput extends JsonValue = JsonValue>(def: Omit<WorkflowDefinition<TName, TInput, TOutput>, 'kind' | 'version'> & {
|
|
130
144
|
readonly version?: string;
|