effect-cf 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 +27 -0
- package/README.md +139 -1
- package/dist/index.d.mts +904 -311
- package/dist/index.mjs +616 -127
- package/package.json +3 -2
- package/src/DurableObject.ts +9 -6
- package/src/DurableObjectRpcWebSocket.ts +21 -17
- package/src/DurableObjectState.ts +12 -10
- package/src/DurableObjectWebSocket.ts +203 -6
- package/src/Queue.ts +116 -0
- package/src/QueueBinding.ts +131 -0
- package/src/QueueDefinition.ts +129 -0
- package/src/Worker.ts +57 -68
- package/src/WorkerDefinition.ts +6 -0
- package/src/Workflow.ts +279 -0
- package/src/WorkflowBinding.ts +254 -0
- package/src/WorkflowDefinition.ts +167 -0
- package/src/index.ts +6 -0
- package/src/internal/WorkerContext.ts +83 -0
package/src/Workflow.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WorkflowEntrypoint as CloudflareWorkflowEntrypoint,
|
|
3
|
+
type WorkflowEvent as CloudflareWorkflowEvent,
|
|
4
|
+
type WorkflowSleepDuration,
|
|
5
|
+
type WorkflowStep as CloudflareWorkflowStep,
|
|
6
|
+
type WorkflowStepConfig,
|
|
7
|
+
type WorkflowStepContext as CloudflareWorkflowStepContext,
|
|
8
|
+
type WorkflowStepEvent,
|
|
9
|
+
type WorkflowTimeoutDuration,
|
|
10
|
+
} from "cloudflare:workers";
|
|
11
|
+
import { ConfigProvider, Context, Effect, Layer, ManagedRuntime, type Scope } from "effect";
|
|
12
|
+
|
|
13
|
+
import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
|
|
14
|
+
import { ExecutionContext, WorkerContext } from "./Worker";
|
|
15
|
+
import * as WorkflowDefinition from "./WorkflowDefinition";
|
|
16
|
+
import * as Entrypoint from "./internal/Entrypoint";
|
|
17
|
+
import { fromExecutionContext, type RunWaitUntilEffect } from "./internal/WorkerContext";
|
|
18
|
+
|
|
19
|
+
export interface WorkflowEventService<Payload = unknown> {
|
|
20
|
+
readonly raw: CloudflareWorkflowEvent<unknown>;
|
|
21
|
+
readonly payload: Payload;
|
|
22
|
+
readonly timestamp: Date;
|
|
23
|
+
readonly instanceId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class WorkflowEvent extends Context.Service<WorkflowEvent, WorkflowEventService>()(
|
|
27
|
+
"effect-cf/WorkflowEvent",
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
type RunWorkflowStepEffect = <A, E>(effect: Effect.Effect<A, E, never>) => Promise<A>;
|
|
31
|
+
|
|
32
|
+
export interface WorkflowStepService {
|
|
33
|
+
readonly raw: CloudflareWorkflowStep;
|
|
34
|
+
do<A, E, R>(
|
|
35
|
+
name: string,
|
|
36
|
+
effect: Effect.Effect<A, E, R>,
|
|
37
|
+
config?: WorkflowStepConfig,
|
|
38
|
+
): Effect.Effect<A, unknown, Exclude<R, WorkflowStepContext>>;
|
|
39
|
+
readonly sleep: (name: string, duration: WorkflowSleepDuration) => Effect.Effect<void, unknown>;
|
|
40
|
+
readonly sleepUntil: (name: string, timestamp: Date | number) => Effect.Effect<void, unknown>;
|
|
41
|
+
readonly waitForEvent: <Payload>(
|
|
42
|
+
name: string,
|
|
43
|
+
options: {
|
|
44
|
+
readonly type: string;
|
|
45
|
+
readonly timeout?: WorkflowTimeoutDuration | number;
|
|
46
|
+
},
|
|
47
|
+
) => Effect.Effect<WorkflowStepEvent<Payload>, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class WorkflowStep extends Context.Service<WorkflowStep, WorkflowStepService>()(
|
|
51
|
+
"effect-cf/WorkflowStep",
|
|
52
|
+
) {}
|
|
53
|
+
|
|
54
|
+
export class WorkflowStepContext extends Context.Service<
|
|
55
|
+
WorkflowStepContext,
|
|
56
|
+
CloudflareWorkflowStepContext
|
|
57
|
+
>()("effect-cf/WorkflowStepContext") {}
|
|
58
|
+
|
|
59
|
+
const fromWorkflowEvent = <Payload>(
|
|
60
|
+
event: CloudflareWorkflowEvent<Payload>,
|
|
61
|
+
): WorkflowEventService<Payload> => ({
|
|
62
|
+
raw: event as CloudflareWorkflowEvent<unknown>,
|
|
63
|
+
payload: event.payload,
|
|
64
|
+
timestamp: event.timestamp,
|
|
65
|
+
instanceId: event.instanceId,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const fromWorkflowStep = (
|
|
69
|
+
step: CloudflareWorkflowStep,
|
|
70
|
+
runPromise: RunWorkflowStepEffect,
|
|
71
|
+
): WorkflowStepService => ({
|
|
72
|
+
raw: step,
|
|
73
|
+
do: <A, E, R>(name: string, effect: Effect.Effect<A, E, R>, config?: WorkflowStepConfig) =>
|
|
74
|
+
Effect.context<Exclude<R, WorkflowStepContext>>().pipe(
|
|
75
|
+
Effect.flatMap((context) =>
|
|
76
|
+
Effect.tryPromise({
|
|
77
|
+
try: () => {
|
|
78
|
+
const run = (stepContext: CloudflareWorkflowStepContext) =>
|
|
79
|
+
runPromise(
|
|
80
|
+
Effect.scoped(
|
|
81
|
+
Effect.provideService(
|
|
82
|
+
Effect.provideContext(
|
|
83
|
+
effect as Effect.Effect<A, E, Exclude<R, WorkflowStepContext>>,
|
|
84
|
+
context,
|
|
85
|
+
),
|
|
86
|
+
WorkflowStepContext,
|
|
87
|
+
stepContext,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
const rawStep = step as {
|
|
92
|
+
do(
|
|
93
|
+
name: string,
|
|
94
|
+
callback: (context: CloudflareWorkflowStepContext) => Promise<A>,
|
|
95
|
+
): Promise<A>;
|
|
96
|
+
do(
|
|
97
|
+
name: string,
|
|
98
|
+
config: WorkflowStepConfig,
|
|
99
|
+
callback: (context: CloudflareWorkflowStepContext) => Promise<A>,
|
|
100
|
+
): Promise<A>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return config === undefined ? rawStep.do(name, run) : rawStep.do(name, config, run);
|
|
104
|
+
},
|
|
105
|
+
catch: (cause) => cause,
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
) as Effect.Effect<A, unknown, Exclude<R, WorkflowStepContext>>,
|
|
109
|
+
sleep: (name, duration) =>
|
|
110
|
+
Effect.tryPromise({
|
|
111
|
+
try: () => step.sleep(name, duration),
|
|
112
|
+
catch: (cause) => cause,
|
|
113
|
+
}),
|
|
114
|
+
sleepUntil: (name, timestamp) =>
|
|
115
|
+
Effect.tryPromise({
|
|
116
|
+
try: () => step.sleepUntil(name, timestamp),
|
|
117
|
+
catch: (cause) => cause,
|
|
118
|
+
}),
|
|
119
|
+
waitForEvent: <Payload>(
|
|
120
|
+
name: string,
|
|
121
|
+
options: {
|
|
122
|
+
readonly type: string;
|
|
123
|
+
readonly timeout?: WorkflowTimeoutDuration | number;
|
|
124
|
+
},
|
|
125
|
+
) =>
|
|
126
|
+
Effect.tryPromise({
|
|
127
|
+
try: () => step.waitForEvent(name, options) as Promise<WorkflowStepEvent<Payload>>,
|
|
128
|
+
catch: (cause) => cause,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
type RuntimeContext<ROut> = ExecutionContext | WorkerContext | WorkerEnvironment | ROut;
|
|
133
|
+
|
|
134
|
+
export type WorkflowRunContext<ROut> =
|
|
135
|
+
| RuntimeContext<ROut>
|
|
136
|
+
| WorkflowEvent
|
|
137
|
+
| WorkflowStep
|
|
138
|
+
| Scope.Scope;
|
|
139
|
+
|
|
140
|
+
export type WorkflowHandler<ROut, Payload = unknown, Result = unknown> = (
|
|
141
|
+
payload: Payload,
|
|
142
|
+
) => Effect.Effect<Result, unknown, WorkflowRunContext<ROut>>;
|
|
143
|
+
|
|
144
|
+
export interface WorkflowOptions<ROut, Payload = unknown, Result = unknown> {
|
|
145
|
+
readonly run: WorkflowHandler<ROut, Payload, Result>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type WorkflowClass<Payload, Result, _ROut> = new (
|
|
149
|
+
ctx: globalThis.ExecutionContext,
|
|
150
|
+
env: WorkerEnv,
|
|
151
|
+
) => CloudflareWorkflowEntrypoint<WorkerEnv, Payload> & {
|
|
152
|
+
run(
|
|
153
|
+
event: Readonly<CloudflareWorkflowEvent<Payload>>,
|
|
154
|
+
step: CloudflareWorkflowStep,
|
|
155
|
+
): Promise<Result>;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const make = <ROut, LayerError, Payload = unknown, Result = unknown>(
|
|
159
|
+
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
160
|
+
options: WorkflowOptions<ROut, Payload, Result>,
|
|
161
|
+
): WorkflowClass<Payload, Result, ROut> => {
|
|
162
|
+
class EffectWorkflow extends CloudflareWorkflowEntrypoint<WorkerEnv, Payload> {
|
|
163
|
+
readonly runtime: ManagedRuntime.ManagedRuntime<RuntimeContext<ROut>, LayerError>;
|
|
164
|
+
|
|
165
|
+
constructor(ctx: globalThis.ExecutionContext, env: WorkerEnv) {
|
|
166
|
+
super(ctx, env);
|
|
167
|
+
|
|
168
|
+
let runWaitUntilEffect: RunWaitUntilEffect = () =>
|
|
169
|
+
Promise.reject(new Error("WorkerContext runtime is not initialized"));
|
|
170
|
+
|
|
171
|
+
const services = Layer.mergeAll(
|
|
172
|
+
Layer.succeed(ExecutionContext, ctx),
|
|
173
|
+
ConfigProvider.layer(Effect.succeed(WorkerConfig.providerFromEnv(env))),
|
|
174
|
+
Layer.succeed(
|
|
175
|
+
WorkerContext,
|
|
176
|
+
fromExecutionContext(ctx, (effect) => runWaitUntilEffect(effect)),
|
|
177
|
+
),
|
|
178
|
+
Layer.succeed(WorkerEnvironment, env),
|
|
179
|
+
) as Layer.Layer<ExecutionContext | WorkerContext | WorkerEnvironment, never, never>;
|
|
180
|
+
|
|
181
|
+
const runtimeLayer = Entrypoint.provideEntrypointServices<
|
|
182
|
+
ROut,
|
|
183
|
+
LayerError,
|
|
184
|
+
ExecutionContext | WorkerContext | WorkerEnvironment
|
|
185
|
+
>(layer, services);
|
|
186
|
+
|
|
187
|
+
this.runtime = ManagedRuntime.make(runtimeLayer);
|
|
188
|
+
runWaitUntilEffect = <A, E>(effect: Effect.Effect<A, E, never>) =>
|
|
189
|
+
this.runtime.runPromiseExit(effect as Effect.Effect<A, E, RuntimeContext<ROut>>);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
run(
|
|
193
|
+
event: Readonly<CloudflareWorkflowEvent<Payload>>,
|
|
194
|
+
step: CloudflareWorkflowStep,
|
|
195
|
+
): Promise<Result> {
|
|
196
|
+
const workflowServices = Layer.mergeAll(
|
|
197
|
+
Layer.succeed(WorkflowEvent, fromWorkflowEvent(event)),
|
|
198
|
+
Layer.succeed(
|
|
199
|
+
WorkflowStep,
|
|
200
|
+
fromWorkflowStep(
|
|
201
|
+
step,
|
|
202
|
+
(effect) =>
|
|
203
|
+
this.runtime.runPromise(
|
|
204
|
+
effect as Effect.Effect<unknown, unknown, RuntimeContext<ROut>>,
|
|
205
|
+
) as never,
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return this.runtime.runPromise(
|
|
211
|
+
Effect.scoped(
|
|
212
|
+
options.run(event.payload).pipe(Effect.provide(workflowServices)),
|
|
213
|
+
) as Effect.Effect<Result, unknown, RuntimeContext<ROut>>,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return Entrypoint.assumeEntrypointClass<WorkflowClass<Payload, Result, ROut>>(EffectWorkflow);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const step = <A, E, R>(
|
|
222
|
+
name: string,
|
|
223
|
+
effect: Effect.Effect<A, E, R>,
|
|
224
|
+
config?: WorkflowStepConfig,
|
|
225
|
+
): Effect.Effect<A, unknown, WorkflowStep | Exclude<R, WorkflowStepContext>> =>
|
|
226
|
+
Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
|
|
227
|
+
workflowStep.do(name, effect, config),
|
|
228
|
+
) as Effect.Effect<A, unknown, WorkflowStep | Exclude<R, WorkflowStepContext>>;
|
|
229
|
+
|
|
230
|
+
export const sleep = (
|
|
231
|
+
name: string,
|
|
232
|
+
duration: WorkflowSleepDuration,
|
|
233
|
+
): Effect.Effect<void, unknown, WorkflowStep> =>
|
|
234
|
+
Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) => workflowStep.sleep(name, duration));
|
|
235
|
+
|
|
236
|
+
export const sleepUntil = (
|
|
237
|
+
name: string,
|
|
238
|
+
timestamp: Date | number,
|
|
239
|
+
): Effect.Effect<void, unknown, WorkflowStep> =>
|
|
240
|
+
Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
|
|
241
|
+
workflowStep.sleepUntil(name, timestamp),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
export const waitForEvent = <Payload>(
|
|
245
|
+
name: string,
|
|
246
|
+
options: {
|
|
247
|
+
readonly type: string;
|
|
248
|
+
readonly timeout?: WorkflowTimeoutDuration | number;
|
|
249
|
+
},
|
|
250
|
+
): Effect.Effect<WorkflowStepEvent<Payload>, unknown, WorkflowStep> =>
|
|
251
|
+
Effect.flatMap(WorkflowStep.asEffect(), (workflowStep) =>
|
|
252
|
+
workflowStep.waitForEvent<Payload>(name, options),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
export const Tag = WorkflowDefinition.Tag;
|
|
256
|
+
|
|
257
|
+
export const implement = WorkflowDefinition.implement;
|
|
258
|
+
|
|
259
|
+
export type Definition<
|
|
260
|
+
Id extends string = string,
|
|
261
|
+
Payload extends WorkflowDefinition.Definition.Any["payload"] =
|
|
262
|
+
WorkflowDefinition.Definition.Any["payload"],
|
|
263
|
+
Result extends WorkflowDefinition.Definition.Any["result"] =
|
|
264
|
+
WorkflowDefinition.Definition.Any["result"],
|
|
265
|
+
> = WorkflowDefinition.Definition<Id, Payload, Result>;
|
|
266
|
+
|
|
267
|
+
export namespace Definition {
|
|
268
|
+
export type Any = WorkflowDefinition.Definition.Any;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type Handler<
|
|
272
|
+
ROut,
|
|
273
|
+
Self extends WorkflowDefinition.Definition.Any,
|
|
274
|
+
> = WorkflowDefinition.Handler<ROut, Self>;
|
|
275
|
+
|
|
276
|
+
export type Options<
|
|
277
|
+
ROut,
|
|
278
|
+
Self extends WorkflowDefinition.Definition.Any,
|
|
279
|
+
> = WorkflowDefinition.Options<ROut, Self>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Data, Effect, Option, Schema as S } from "effect";
|
|
2
|
+
|
|
3
|
+
import * as Binding from "./Binding";
|
|
4
|
+
import type * as RpcDefinition from "./RpcDefinition";
|
|
5
|
+
|
|
6
|
+
const TypeId = "effect-cf/WorkflowBinding" as const;
|
|
7
|
+
|
|
8
|
+
export type WorkflowInstanceCreateOptions<Payload> = Omit<
|
|
9
|
+
globalThis.WorkflowInstanceCreateOptions<Payload>,
|
|
10
|
+
"params"
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
export type WorkflowInstanceCreateBatchOptions<Payload, EncodedPayload = unknown> = ReadonlyArray<
|
|
14
|
+
{ readonly payload: Payload } & WorkflowInstanceCreateOptions<EncodedPayload>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export interface WorkflowInstanceRestartOptions {
|
|
18
|
+
readonly from?: {
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
readonly count?: number;
|
|
21
|
+
readonly type?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type WorkflowInstanceStatusName = globalThis.InstanceStatus["status"];
|
|
26
|
+
|
|
27
|
+
export interface WorkflowInstanceStatus<Result> {
|
|
28
|
+
readonly status: WorkflowInstanceStatusName;
|
|
29
|
+
readonly output: Option.Option<Result>;
|
|
30
|
+
readonly error: Option.Option<{
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WorkflowInstance<Result> {
|
|
37
|
+
readonly raw: globalThis.WorkflowInstance;
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly pause: Effect.Effect<void, WorkflowOperationError>;
|
|
40
|
+
readonly resume: Effect.Effect<void, WorkflowOperationError>;
|
|
41
|
+
readonly terminate: Effect.Effect<void, WorkflowOperationError>;
|
|
42
|
+
readonly restart: (
|
|
43
|
+
options?: WorkflowInstanceRestartOptions,
|
|
44
|
+
) => Effect.Effect<void, WorkflowOperationError>;
|
|
45
|
+
readonly status: Effect.Effect<
|
|
46
|
+
WorkflowInstanceStatus<Result>,
|
|
47
|
+
WorkflowOperationError | WorkflowResultDecodeError
|
|
48
|
+
>;
|
|
49
|
+
readonly sendEvent: (event: WorkflowInstanceEvent) => Effect.Effect<void, WorkflowOperationError>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkflowInstanceEvent {
|
|
53
|
+
readonly type: string;
|
|
54
|
+
readonly payload: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkflowBindingDefinition<
|
|
58
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
59
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
60
|
+
> {
|
|
61
|
+
/** Binding name as configured in `wrangler.jsonc`. */
|
|
62
|
+
readonly binding: string;
|
|
63
|
+
/** Codec used to encode payloads passed to `Workflow.create`. */
|
|
64
|
+
readonly payload: Payload;
|
|
65
|
+
/** Codec used to decode completed workflow status output. */
|
|
66
|
+
readonly result: Result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
declare const WorkflowServiceTypeId: unique symbol;
|
|
70
|
+
|
|
71
|
+
export interface WorkflowService<
|
|
72
|
+
Id extends string,
|
|
73
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
74
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
75
|
+
> {
|
|
76
|
+
readonly [WorkflowServiceTypeId]: {
|
|
77
|
+
readonly id: Id;
|
|
78
|
+
readonly payload: S.Schema.Type<Payload>;
|
|
79
|
+
readonly encodedPayload: S.Codec.Encoded<Payload>;
|
|
80
|
+
readonly result: S.Schema.Type<Result>;
|
|
81
|
+
readonly encodedResult: S.Codec.Encoded<Result>;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class WorkflowOperationError extends Data.TaggedError("WorkflowOperationError")<{
|
|
86
|
+
readonly binding: string;
|
|
87
|
+
readonly operation: string;
|
|
88
|
+
readonly cause: unknown;
|
|
89
|
+
}> {}
|
|
90
|
+
|
|
91
|
+
export class WorkflowResultDecodeError extends Data.TaggedError("WorkflowResultDecodeError")<{
|
|
92
|
+
readonly binding: string;
|
|
93
|
+
readonly instanceId: string;
|
|
94
|
+
readonly cause: unknown;
|
|
95
|
+
}> {}
|
|
96
|
+
|
|
97
|
+
const workflowError = (binding: string, operation: string, cause: unknown) =>
|
|
98
|
+
new WorkflowOperationError({ binding, operation, cause });
|
|
99
|
+
|
|
100
|
+
const tryWorkflowPromise = <A>(
|
|
101
|
+
binding: string,
|
|
102
|
+
operation: string,
|
|
103
|
+
evaluate: () => Promise<A>,
|
|
104
|
+
): Effect.Effect<A, WorkflowOperationError> =>
|
|
105
|
+
Effect.tryPromise({
|
|
106
|
+
try: evaluate,
|
|
107
|
+
catch: (cause) => workflowError(binding, operation, cause),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const isWorkflow = <Payload>(value: unknown): value is globalThis.Workflow<Payload> => {
|
|
111
|
+
if (typeof value !== "object" || value === null) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resource = value as Record<string, unknown>;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
typeof resource.create === "function" &&
|
|
119
|
+
typeof resource.createBatch === "function" &&
|
|
120
|
+
typeof resource.get === "function"
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const make = <
|
|
125
|
+
Id extends string,
|
|
126
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
127
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
128
|
+
>(
|
|
129
|
+
id: Id,
|
|
130
|
+
definition: WorkflowBindingDefinition<Payload, Result>,
|
|
131
|
+
) => Service<WorkflowService<Id, Payload, Result>>()<Id, Payload, Result>(id, definition);
|
|
132
|
+
|
|
133
|
+
export const Service =
|
|
134
|
+
<Self>() =>
|
|
135
|
+
<
|
|
136
|
+
Id extends string,
|
|
137
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
138
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
139
|
+
>(
|
|
140
|
+
id: Id,
|
|
141
|
+
definition: WorkflowBindingDefinition<Payload, Result>,
|
|
142
|
+
) => {
|
|
143
|
+
type PayloadValue = S.Schema.Type<Payload>;
|
|
144
|
+
type EncodedPayload = S.Codec.Encoded<Payload>;
|
|
145
|
+
type ResultValue = S.Schema.Type<Result>;
|
|
146
|
+
|
|
147
|
+
const tag = Binding.Service<Self>()(
|
|
148
|
+
id,
|
|
149
|
+
definition.binding,
|
|
150
|
+
(value): value is globalThis.Workflow<EncodedPayload> => isWorkflow<EncodedPayload>(value),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const encodePayload = S.encodeEffect(definition.payload);
|
|
154
|
+
const decodeResult = S.decodeUnknownEffect(definition.result);
|
|
155
|
+
|
|
156
|
+
const wrapInstance = (raw: globalThis.WorkflowInstance): WorkflowInstance<ResultValue> => {
|
|
157
|
+
const operation = <A>(name: string, evaluate: () => Promise<A>) =>
|
|
158
|
+
tryWorkflowPromise(definition.binding, name, evaluate);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
raw,
|
|
162
|
+
id: raw.id,
|
|
163
|
+
pause: operation("pause", () => raw.pause()),
|
|
164
|
+
resume: operation("resume", () => raw.resume()),
|
|
165
|
+
terminate: operation("terminate", () => raw.terminate()),
|
|
166
|
+
restart: (options) =>
|
|
167
|
+
operation("restart", () =>
|
|
168
|
+
(raw as { restart(options?: WorkflowInstanceRestartOptions): Promise<void> }).restart(
|
|
169
|
+
options,
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
status: operation("status", () => raw.status()).pipe(
|
|
173
|
+
Effect.flatMap((status) =>
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const output =
|
|
176
|
+
status.output === undefined
|
|
177
|
+
? Option.none<ResultValue>()
|
|
178
|
+
: Option.some(
|
|
179
|
+
yield* decodeResult(status.output).pipe(
|
|
180
|
+
Effect.mapError(
|
|
181
|
+
(cause) =>
|
|
182
|
+
new WorkflowResultDecodeError({
|
|
183
|
+
binding: definition.binding,
|
|
184
|
+
instanceId: raw.id,
|
|
185
|
+
cause,
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
status: status.status,
|
|
193
|
+
output,
|
|
194
|
+
error: status.error === undefined ? Option.none() : Option.some(status.error),
|
|
195
|
+
};
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
sendEvent: (event) => operation("sendEvent", () => raw.sendEvent(event)),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const create = Effect.fnUntraced(function* (
|
|
204
|
+
payload: PayloadValue,
|
|
205
|
+
options?: WorkflowInstanceCreateOptions<EncodedPayload>,
|
|
206
|
+
) {
|
|
207
|
+
const workflow = yield* tag;
|
|
208
|
+
const encoded = yield* encodePayload(payload);
|
|
209
|
+
const raw = yield* tryWorkflowPromise(definition.binding, "create", () =>
|
|
210
|
+
workflow.create({ ...options, params: encoded }),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return wrapInstance(raw);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const createBatch = Effect.fnUntraced(function* (
|
|
217
|
+
batch: WorkflowInstanceCreateBatchOptions<PayloadValue, EncodedPayload>,
|
|
218
|
+
) {
|
|
219
|
+
const workflow = yield* tag;
|
|
220
|
+
const encodedBatch: Array<globalThis.WorkflowInstanceCreateOptions<EncodedPayload>> = [];
|
|
221
|
+
|
|
222
|
+
for (const item of batch) {
|
|
223
|
+
const { payload, ...options } = item;
|
|
224
|
+
|
|
225
|
+
encodedBatch.push({
|
|
226
|
+
...options,
|
|
227
|
+
params: yield* encodePayload(payload),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const rawInstances = yield* tryWorkflowPromise(definition.binding, "createBatch", () =>
|
|
232
|
+
workflow.createBatch(encodedBatch),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return rawInstances.map(wrapInstance);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const get = Effect.fnUntraced(function* (instanceId: string) {
|
|
239
|
+
const workflow = yield* tag;
|
|
240
|
+
const raw = yield* tryWorkflowPromise(definition.binding, "get", () =>
|
|
241
|
+
workflow.get(instanceId),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return wrapInstance(raw);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return Object.assign(tag, {
|
|
248
|
+
[TypeId]: TypeId,
|
|
249
|
+
binding: definition.binding,
|
|
250
|
+
create,
|
|
251
|
+
createBatch,
|
|
252
|
+
get,
|
|
253
|
+
});
|
|
254
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Effect, Schema as S, type Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { WorkerEnvironment } from "./Environment";
|
|
4
|
+
import type { ExecutionContext, WorkerContext } from "./Worker";
|
|
5
|
+
import type * as RpcDefinition from "./RpcDefinition";
|
|
6
|
+
import * as WorkflowBinding from "./WorkflowBinding";
|
|
7
|
+
import * as WorkflowEntrypoint from "./Workflow";
|
|
8
|
+
|
|
9
|
+
export interface Definition<
|
|
10
|
+
Id extends string = string,
|
|
11
|
+
Payload extends RpcDefinition.ServiceFreeSchema = RpcDefinition.ServiceFreeSchema,
|
|
12
|
+
Result extends RpcDefinition.ServiceFreeSchema = RpcDefinition.ServiceFreeSchema,
|
|
13
|
+
> {
|
|
14
|
+
readonly id: Id;
|
|
15
|
+
readonly payload: Payload;
|
|
16
|
+
readonly result: Result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export namespace Definition {
|
|
20
|
+
export type Any = Definition<
|
|
21
|
+
string,
|
|
22
|
+
RpcDefinition.ServiceFreeSchema,
|
|
23
|
+
RpcDefinition.ServiceFreeSchema
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare const BindingServiceTypeId: unique symbol;
|
|
28
|
+
|
|
29
|
+
export interface BindingService<Id extends string, Self extends Definition.Any> {
|
|
30
|
+
readonly [BindingServiceTypeId]: {
|
|
31
|
+
readonly id: Id;
|
|
32
|
+
readonly definition: Self;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Handler<ROut, Self extends Definition.Any> = (
|
|
37
|
+
payload: S.Schema.Type<Self["payload"]>,
|
|
38
|
+
) => Effect.Effect<
|
|
39
|
+
S.Schema.Type<Self["result"]>,
|
|
40
|
+
unknown,
|
|
41
|
+
WorkflowEntrypoint.WorkflowRunContext<ROut>
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export interface Options<ROut, Self extends Definition.Any> {
|
|
45
|
+
readonly run: Handler<ROut, Self>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const makeDefinition = <
|
|
49
|
+
Id extends string,
|
|
50
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
51
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
52
|
+
>(
|
|
53
|
+
id: Id,
|
|
54
|
+
definition: {
|
|
55
|
+
readonly payload: Payload;
|
|
56
|
+
readonly result: Result;
|
|
57
|
+
},
|
|
58
|
+
) => {
|
|
59
|
+
type SelfDefinition = Definition<Id, Payload, Result>;
|
|
60
|
+
const workflowDefinition: SelfDefinition = {
|
|
61
|
+
id,
|
|
62
|
+
payload: definition.payload,
|
|
63
|
+
result: definition.result,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return Object.assign(workflowDefinition, {
|
|
67
|
+
make: <ROut, LayerError>(
|
|
68
|
+
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
69
|
+
options: Options<ROut, SelfDefinition>,
|
|
70
|
+
) =>
|
|
71
|
+
WorkflowEntrypoint.make(layer, {
|
|
72
|
+
run: wrapHandler(workflowDefinition, options.run),
|
|
73
|
+
}),
|
|
74
|
+
Binding:
|
|
75
|
+
<Self>() =>
|
|
76
|
+
<BindingId extends string>(
|
|
77
|
+
bindingId: BindingId,
|
|
78
|
+
binding: Omit<
|
|
79
|
+
WorkflowBinding.WorkflowBindingDefinition<Payload, Result>,
|
|
80
|
+
"payload" | "result"
|
|
81
|
+
>,
|
|
82
|
+
) =>
|
|
83
|
+
WorkflowBinding.Service<Self>()<BindingId, Payload, Result>(bindingId, {
|
|
84
|
+
...binding,
|
|
85
|
+
payload: definition.payload,
|
|
86
|
+
result: definition.result,
|
|
87
|
+
}),
|
|
88
|
+
binding: <BindingId extends string>(
|
|
89
|
+
bindingId: BindingId,
|
|
90
|
+
binding: Omit<
|
|
91
|
+
WorkflowBinding.WorkflowBindingDefinition<Payload, Result>,
|
|
92
|
+
"payload" | "result"
|
|
93
|
+
>,
|
|
94
|
+
) =>
|
|
95
|
+
WorkflowBinding.Service<BindingService<BindingId, SelfDefinition>>()<
|
|
96
|
+
BindingId,
|
|
97
|
+
Payload,
|
|
98
|
+
Result
|
|
99
|
+
>(bindingId, {
|
|
100
|
+
...binding,
|
|
101
|
+
payload: definition.payload,
|
|
102
|
+
result: definition.result,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const make = makeDefinition;
|
|
108
|
+
|
|
109
|
+
export const Tag =
|
|
110
|
+
<_Self>() =>
|
|
111
|
+
<
|
|
112
|
+
Id extends string,
|
|
113
|
+
Payload extends RpcDefinition.ServiceFreeSchema,
|
|
114
|
+
Result extends RpcDefinition.ServiceFreeSchema,
|
|
115
|
+
>(
|
|
116
|
+
id: Id,
|
|
117
|
+
definition: {
|
|
118
|
+
readonly payload: Payload;
|
|
119
|
+
readonly result: Result;
|
|
120
|
+
},
|
|
121
|
+
) => {
|
|
122
|
+
const workflowDefinition = makeDefinition(id, definition);
|
|
123
|
+
|
|
124
|
+
abstract class WorkflowDefinitionClass {
|
|
125
|
+
static readonly id = workflowDefinition.id;
|
|
126
|
+
static readonly payload = workflowDefinition.payload;
|
|
127
|
+
static readonly result = workflowDefinition.result;
|
|
128
|
+
static readonly make = workflowDefinition.make;
|
|
129
|
+
static readonly Binding = workflowDefinition.Binding;
|
|
130
|
+
static readonly binding = workflowDefinition.binding;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return WorkflowDefinitionClass as (abstract new () => object) & typeof workflowDefinition;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const Workflow = Tag;
|
|
137
|
+
|
|
138
|
+
const wrapHandler = <ROut, const Self extends Definition.Any>(
|
|
139
|
+
definition: Self,
|
|
140
|
+
handler: Handler<ROut, Self>,
|
|
141
|
+
): WorkflowEntrypoint.WorkflowHandler<
|
|
142
|
+
ROut,
|
|
143
|
+
S.Codec.Encoded<Self["payload"]>,
|
|
144
|
+
S.Codec.Encoded<Self["result"]>
|
|
145
|
+
> => {
|
|
146
|
+
const decodePayload = S.decodeUnknownEffect(definition.payload);
|
|
147
|
+
const encodeResult = S.encodeEffect(definition.result);
|
|
148
|
+
|
|
149
|
+
return (payload) =>
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
const decodedPayload = yield* decodePayload(payload);
|
|
152
|
+
const event = yield* WorkflowEntrypoint.WorkflowEvent;
|
|
153
|
+
const decodedEvent = {
|
|
154
|
+
...event,
|
|
155
|
+
payload: decodedPayload,
|
|
156
|
+
} as WorkflowEntrypoint.WorkflowEventService<S.Schema.Type<Self["payload"]>>;
|
|
157
|
+
const result = yield* handler(decodedPayload as S.Schema.Type<Self["payload"]>).pipe(
|
|
158
|
+
Effect.provideService(WorkflowEntrypoint.WorkflowEvent, decodedEvent),
|
|
159
|
+
);
|
|
160
|
+
return yield* encodeResult(result as S.Schema.Type<Self["result"]>);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const implement = <ROut, const Self extends Definition.Any>(
|
|
165
|
+
_definition: Self,
|
|
166
|
+
handler: Handler<ROut, Self>,
|
|
167
|
+
): Handler<ROut, Self> => handler;
|