effect-cf 0.0.4 → 0.2.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 +21 -0
- package/LICENSE +21 -0
- package/README.md +163 -46
- package/dist/index.d.mts +1234 -309
- package/dist/index.mjs +944 -126
- package/package.json +3 -2
- package/src/DurableObject.ts +26 -2
- package/src/DurableObjectAlarm.ts +832 -0
- package/src/DurableObjectRpcWebSocket.ts +21 -17
- package/src/DurableObjectState.ts +12 -10
- package/src/DurableObjectWebSocket.ts +215 -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 +7 -0
- package/src/internal/WorkerContext.ts +83 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Effect, Schema as S, type Layer, type Scope } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { WorkerEnvironment } from "./Environment";
|
|
4
|
+
import * as QueueEntrypoint from "./Queue";
|
|
5
|
+
import * as QueueBinding from "./QueueBinding";
|
|
6
|
+
import type { ExecutionContext, WorkerContext } from "./Worker";
|
|
7
|
+
import * as WorkerEntrypoint from "./Worker";
|
|
8
|
+
import type * as RpcDefinition from "./RpcDefinition";
|
|
9
|
+
|
|
10
|
+
export interface Definition<
|
|
11
|
+
Id extends string = string,
|
|
12
|
+
Message extends RpcDefinition.ServiceFreeSchema = RpcDefinition.ServiceFreeSchema,
|
|
13
|
+
> {
|
|
14
|
+
readonly id: Id;
|
|
15
|
+
readonly message: Message;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export namespace Definition {
|
|
19
|
+
export type Any = Definition<string, RpcDefinition.ServiceFreeSchema>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const BindingServiceTypeId: unique symbol;
|
|
23
|
+
|
|
24
|
+
export interface BindingService<Id extends string, Self extends Definition.Any> {
|
|
25
|
+
readonly [BindingServiceTypeId]: {
|
|
26
|
+
readonly id: Id;
|
|
27
|
+
readonly definition: Self;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Handler<ROut, Self extends Definition.Any> = (
|
|
32
|
+
batch: QueueEntrypoint.QueueBatch<S.Schema.Type<Self["message"]>>,
|
|
33
|
+
) => Effect.Effect<
|
|
34
|
+
void,
|
|
35
|
+
unknown,
|
|
36
|
+
ExecutionContext | WorkerEntrypoint.WorkerContext | WorkerEnvironment | Scope.Scope | ROut
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export interface Options<ROut, Self extends Definition.Any> extends Omit<
|
|
40
|
+
WorkerEntrypoint.WorkerOptions<ROut, Record<never, never>>,
|
|
41
|
+
"queue" | "rpc"
|
|
42
|
+
> {
|
|
43
|
+
readonly queue: Handler<ROut, Self>;
|
|
44
|
+
readonly rpc?: never;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const makeDefinition = <Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
|
|
48
|
+
id: Id,
|
|
49
|
+
definition: { readonly message: Message },
|
|
50
|
+
) => {
|
|
51
|
+
type SelfDefinition = Definition<Id, Message>;
|
|
52
|
+
const queueDefinition: SelfDefinition = {
|
|
53
|
+
id,
|
|
54
|
+
message: definition.message,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return Object.assign(queueDefinition, {
|
|
58
|
+
make: <ROut, LayerError>(
|
|
59
|
+
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
60
|
+
options: Options<ROut, SelfDefinition>,
|
|
61
|
+
) =>
|
|
62
|
+
WorkerEntrypoint.make(layer, {
|
|
63
|
+
...options,
|
|
64
|
+
queue: wrapHandler(queueDefinition, options.queue),
|
|
65
|
+
}),
|
|
66
|
+
Binding:
|
|
67
|
+
<Self>() =>
|
|
68
|
+
<BindingId extends string>(
|
|
69
|
+
bindingId: BindingId,
|
|
70
|
+
binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
|
|
71
|
+
) =>
|
|
72
|
+
QueueBinding.Service<Self>()<BindingId, Message>(bindingId, {
|
|
73
|
+
...binding,
|
|
74
|
+
message: definition.message,
|
|
75
|
+
}),
|
|
76
|
+
binding: <BindingId extends string>(
|
|
77
|
+
bindingId: BindingId,
|
|
78
|
+
binding: Omit<QueueBinding.QueueBindingDefinition<Message>, "message">,
|
|
79
|
+
) =>
|
|
80
|
+
QueueBinding.Service<BindingService<BindingId, SelfDefinition>>()<BindingId, Message>(
|
|
81
|
+
bindingId,
|
|
82
|
+
{
|
|
83
|
+
...binding,
|
|
84
|
+
message: definition.message,
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const make = makeDefinition;
|
|
91
|
+
|
|
92
|
+
export const Tag =
|
|
93
|
+
<_Self>() =>
|
|
94
|
+
<Id extends string, Message extends RpcDefinition.ServiceFreeSchema>(
|
|
95
|
+
id: Id,
|
|
96
|
+
definition: { readonly message: Message },
|
|
97
|
+
) => {
|
|
98
|
+
const queueDefinition = makeDefinition(id, definition);
|
|
99
|
+
|
|
100
|
+
abstract class QueueDefinitionClass {
|
|
101
|
+
static readonly id = queueDefinition.id;
|
|
102
|
+
static readonly message = queueDefinition.message;
|
|
103
|
+
static readonly make = queueDefinition.make;
|
|
104
|
+
static readonly Binding = queueDefinition.Binding;
|
|
105
|
+
static readonly binding = queueDefinition.binding;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return QueueDefinitionClass as (abstract new () => object) & typeof queueDefinition;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const Queue = Tag;
|
|
112
|
+
|
|
113
|
+
const wrapHandler = <ROut, const Self extends Definition.Any>(
|
|
114
|
+
definition: Self,
|
|
115
|
+
handler: Handler<ROut, Self>,
|
|
116
|
+
): QueueEntrypoint.QueueHandler<ROut> => {
|
|
117
|
+
const decodeBody = S.decodeUnknownEffect(definition.message);
|
|
118
|
+
|
|
119
|
+
return (batch) =>
|
|
120
|
+
Effect.gen(function* () {
|
|
121
|
+
const decoded = yield* QueueEntrypoint.decodeBatch(batch.raw, decodeBody);
|
|
122
|
+
yield* handler(decoded);
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const implement = <ROut, const Self extends Definition.Any>(
|
|
127
|
+
_definition: Self,
|
|
128
|
+
handler: Handler<ROut, Self>,
|
|
129
|
+
): Handler<ROut, Self> => handler;
|
package/src/Worker.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { WorkerEntrypoint as CloudflareWorkerEntrypoint } from "cloudflare:workers";
|
|
2
|
-
import {
|
|
3
|
-
Cause,
|
|
4
|
-
ConfigProvider,
|
|
5
|
-
Context,
|
|
6
|
-
Effect,
|
|
7
|
-
Exit,
|
|
8
|
-
Layer,
|
|
9
|
-
ManagedRuntime,
|
|
10
|
-
type Scope,
|
|
11
|
-
} from "effect";
|
|
2
|
+
import { Cause, ConfigProvider, Context, Effect, Layer, ManagedRuntime, type Scope } from "effect";
|
|
12
3
|
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
13
4
|
|
|
14
5
|
import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
|
|
6
|
+
import { fromMessage, fromMessageBatch, type QueueHandler } from "./Queue";
|
|
15
7
|
import type * as RpcDefinition from "./RpcDefinition";
|
|
16
8
|
import * as WorkerDefinition from "./WorkerDefinition";
|
|
17
9
|
import * as Entrypoint from "./internal/Entrypoint";
|
|
10
|
+
import { fromExecutionContext, type RunWaitUntilEffect } from "./internal/WorkerContext";
|
|
18
11
|
|
|
19
12
|
export class ExecutionContext extends Context.Service<
|
|
20
13
|
ExecutionContext,
|
|
@@ -22,6 +15,7 @@ export class ExecutionContext extends Context.Service<
|
|
|
22
15
|
>()("effect-cf/ExecutionContext") {}
|
|
23
16
|
|
|
24
17
|
export interface WorkerContextWaitUntilOptions<E, R> {
|
|
18
|
+
readonly mode?: "observe" | "propagate";
|
|
25
19
|
readonly onFailure?: (cause: Cause.Cause<E>) => Effect.Effect<void, never, R>;
|
|
26
20
|
}
|
|
27
21
|
|
|
@@ -31,6 +25,10 @@ export interface WorkerContextService {
|
|
|
31
25
|
effect: Effect.Effect<A, E, R>,
|
|
32
26
|
options?: WorkerContextWaitUntilOptions<E, R2>,
|
|
33
27
|
): Effect.Effect<void, never, R | R2>;
|
|
28
|
+
waitUntilPropagating<A, E, R, R2 = never>(
|
|
29
|
+
effect: Effect.Effect<A, E, R>,
|
|
30
|
+
options?: Omit<WorkerContextWaitUntilOptions<E, R2>, "mode">,
|
|
31
|
+
): Effect.Effect<void, never, R | R2>;
|
|
34
32
|
readonly passThroughOnException: Effect.Effect<void>;
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -38,60 +36,6 @@ export class WorkerContext extends Context.Service<WorkerContext, WorkerContextS
|
|
|
38
36
|
"effect-cf/WorkerContext",
|
|
39
37
|
) {}
|
|
40
38
|
|
|
41
|
-
type RunWaitUntilEffect = <A, E>(
|
|
42
|
-
effect: Effect.Effect<A, E, never>,
|
|
43
|
-
) => Promise<Exit.Exit<A, unknown>>;
|
|
44
|
-
|
|
45
|
-
const fromExecutionContext = (
|
|
46
|
-
ctx: globalThis.ExecutionContext,
|
|
47
|
-
runPromiseExit: RunWaitUntilEffect,
|
|
48
|
-
): WorkerContextService => ({
|
|
49
|
-
raw: ctx,
|
|
50
|
-
waitUntil: <A, E, R, R2 = never>(
|
|
51
|
-
effect: Effect.Effect<A, E, R>,
|
|
52
|
-
options?: WorkerContextWaitUntilOptions<E, R2>,
|
|
53
|
-
) =>
|
|
54
|
-
Effect.context<R | R2>().pipe(
|
|
55
|
-
Effect.flatMap((context) =>
|
|
56
|
-
Effect.sync(() => {
|
|
57
|
-
const observed = Effect.exit(effect).pipe(
|
|
58
|
-
Effect.flatMap((exit) => {
|
|
59
|
-
if (Exit.isSuccess(exit)) {
|
|
60
|
-
return Effect.void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const handleFailure =
|
|
64
|
-
options?.onFailure?.(exit.cause) ??
|
|
65
|
-
Effect.logError("WorkerContext.waitUntil failed", Cause.pretty(exit.cause));
|
|
66
|
-
|
|
67
|
-
return handleFailure.pipe(
|
|
68
|
-
Effect.catchCause((handlerCause) =>
|
|
69
|
-
Effect.logError(
|
|
70
|
-
"WorkerContext.waitUntil failure handler failed",
|
|
71
|
-
Cause.pretty(exit.cause),
|
|
72
|
-
Cause.pretty(handlerCause),
|
|
73
|
-
),
|
|
74
|
-
),
|
|
75
|
-
);
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
ctx.waitUntil(
|
|
80
|
-
runPromiseExit(Effect.scoped(Effect.provideContext(observed, context))).then((exit) => {
|
|
81
|
-
if (Exit.isFailure(exit)) {
|
|
82
|
-
console.error(
|
|
83
|
-
"WorkerContext.waitUntil failure handler failed",
|
|
84
|
-
Cause.pretty(exit.cause),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
}),
|
|
90
|
-
),
|
|
91
|
-
),
|
|
92
|
-
passThroughOnException: Effect.sync(() => ctx.passThroughOnException()),
|
|
93
|
-
});
|
|
94
|
-
|
|
95
39
|
export class NativeRequest extends Context.Service<NativeRequest, Request>()(
|
|
96
40
|
"effect-cf/NativeRequest",
|
|
97
41
|
) {}
|
|
@@ -103,6 +47,12 @@ export type ReservedMethodName =
|
|
|
103
47
|
| RpcDefinition.ReservedMethodName
|
|
104
48
|
| "fetch"
|
|
105
49
|
| "connect"
|
|
50
|
+
| "queue"
|
|
51
|
+
| "scheduled"
|
|
52
|
+
| "tail"
|
|
53
|
+
| "tailStream"
|
|
54
|
+
| "test"
|
|
55
|
+
| "trace"
|
|
106
56
|
| "alarm"
|
|
107
57
|
| "webSocketMessage"
|
|
108
58
|
| "webSocketClose"
|
|
@@ -113,6 +63,12 @@ const reservedMethodNames = new Set<string>([
|
|
|
113
63
|
"dup",
|
|
114
64
|
"fetch",
|
|
115
65
|
"connect",
|
|
66
|
+
"queue",
|
|
67
|
+
"scheduled",
|
|
68
|
+
"tail",
|
|
69
|
+
"tailStream",
|
|
70
|
+
"test",
|
|
71
|
+
"trace",
|
|
116
72
|
"alarm",
|
|
117
73
|
"webSocketMessage",
|
|
118
74
|
"webSocketClose",
|
|
@@ -169,6 +125,7 @@ export type RpcHandlers<ROut, Api> = {
|
|
|
169
125
|
|
|
170
126
|
export interface WorkerOptions<ROut, Rpc extends WorkerRpc<ROut>> {
|
|
171
127
|
readonly fetch?: Effect.Effect<WorkerFetchSuccess, unknown, WorkerFetchContext<ROut>>;
|
|
128
|
+
readonly queue?: QueueHandler<ROut>;
|
|
172
129
|
readonly rpc?: Rpc;
|
|
173
130
|
}
|
|
174
131
|
|
|
@@ -181,6 +138,7 @@ export type WorkerClass<Rpc extends WorkerRpc<ROut>, ROut> = new (
|
|
|
181
138
|
env: WorkerEnv,
|
|
182
139
|
) => CloudflareWorkerEntrypoint<WorkerEnv> & {
|
|
183
140
|
fetch(request: Request): Promise<Response>;
|
|
141
|
+
queue(batch: globalThis.MessageBatch): Promise<void>;
|
|
184
142
|
} & WorkerRpcShape<Rpc, ROut>;
|
|
185
143
|
|
|
186
144
|
export interface FetchHandler<Env extends WorkerEnv = WorkerEnv> {
|
|
@@ -211,10 +169,29 @@ const renderFetchSuccess = <E, R>(
|
|
|
211
169
|
),
|
|
212
170
|
);
|
|
213
171
|
|
|
214
|
-
|
|
172
|
+
const isWorkerOptions = <ROut, Rpc extends WorkerRpc<ROut>>(
|
|
173
|
+
options: WorkerOptions<ROut, Rpc> | WorkerHandler<ROut>,
|
|
174
|
+
): options is WorkerOptions<ROut, Rpc> =>
|
|
175
|
+
typeof options === "object" &&
|
|
176
|
+
options !== null &&
|
|
177
|
+
("fetch" in options || "queue" in options || "rpc" in options);
|
|
178
|
+
|
|
179
|
+
export function make<ROut, LayerError>(
|
|
180
|
+
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
181
|
+
fetch: WorkerHandler<ROut>,
|
|
182
|
+
): WorkerClass<Record<never, never>, ROut>;
|
|
183
|
+
export function make<ROut, LayerError, const Rpc extends WorkerRpc<ROut> = Record<never, never>>(
|
|
215
184
|
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
216
185
|
options: WorkerOptions<ROut, Rpc>,
|
|
217
|
-
): WorkerClass<Rpc, ROut
|
|
186
|
+
): WorkerClass<Rpc, ROut>;
|
|
187
|
+
export function make<ROut, LayerError, const Rpc extends WorkerRpc<ROut> = Record<never, never>>(
|
|
188
|
+
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
|
189
|
+
optionsOrFetch: WorkerOptions<ROut, Rpc> | WorkerHandler<ROut>,
|
|
190
|
+
): WorkerClass<Rpc, ROut> {
|
|
191
|
+
const options = isWorkerOptions(optionsOrFetch)
|
|
192
|
+
? optionsOrFetch
|
|
193
|
+
: ({ fetch: optionsOrFetch } as WorkerOptions<ROut, Rpc>);
|
|
194
|
+
|
|
218
195
|
class EffectWorker extends CloudflareWorkerEntrypoint<WorkerEnv> {
|
|
219
196
|
readonly runtime: ManagedRuntime.ManagedRuntime<RuntimeContext<ROut>, LayerError>;
|
|
220
197
|
|
|
@@ -222,7 +199,7 @@ export const make = <ROut, LayerError, const Rpc extends WorkerRpc<ROut> = Recor
|
|
|
222
199
|
super(ctx, env);
|
|
223
200
|
|
|
224
201
|
let runWaitUntilEffect: RunWaitUntilEffect = () =>
|
|
225
|
-
Promise.
|
|
202
|
+
Promise.reject(new Error("WorkerContext runtime is not initialized"));
|
|
226
203
|
|
|
227
204
|
const services = Layer.mergeAll(
|
|
228
205
|
Layer.succeed(ExecutionContext, ctx),
|
|
@@ -269,6 +246,18 @@ export const make = <ROut, LayerError, const Rpc extends WorkerRpc<ROut> = Recor
|
|
|
269
246
|
>,
|
|
270
247
|
);
|
|
271
248
|
}
|
|
249
|
+
|
|
250
|
+
queue(batch: globalThis.MessageBatch): Promise<void> {
|
|
251
|
+
const queueHandler = options.queue;
|
|
252
|
+
|
|
253
|
+
if (queueHandler === undefined) {
|
|
254
|
+
return Promise.resolve();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const messages = batch.messages.map((message) => fromMessage(message, message.body));
|
|
258
|
+
|
|
259
|
+
return this[RunSymbol](queueHandler(fromMessageBatch(batch, messages)));
|
|
260
|
+
}
|
|
272
261
|
}
|
|
273
262
|
|
|
274
263
|
Entrypoint.defineEntrypointRpcMethods<EffectWorker>(
|
|
@@ -280,7 +269,7 @@ export const make = <ROut, LayerError, const Rpc extends WorkerRpc<ROut> = Recor
|
|
|
280
269
|
);
|
|
281
270
|
|
|
282
271
|
return Entrypoint.assumeEntrypointClass<WorkerClass<Rpc, ROut>>(EffectWorker);
|
|
283
|
-
}
|
|
272
|
+
}
|
|
284
273
|
|
|
285
274
|
export const makeFetchHandler = <ROut, LayerError, Env extends WorkerEnv = WorkerEnv>(
|
|
286
275
|
layer: Layer.Layer<ROut, LayerError, ExecutionContext | WorkerContext | WorkerEnvironment>,
|
package/src/WorkerDefinition.ts
CHANGED
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>;
|