effect-cf 0.15.0 → 0.17.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.
@@ -0,0 +1,337 @@
1
+ import { Context, Data, Effect, Schema, type Layer } from "effect";
2
+
3
+ import * as Binding from "./Binding";
4
+ import type { WorkerEnvironment } from "./Environment";
5
+
6
+ const expectedContainerNamespace = "Container namespace binding with getByName()";
7
+
8
+ /** Signal accepted by Cloudflare Container instances. */
9
+ export type ContainerSignal = "SIGKILL" | "SIGINT" | "SIGTERM";
10
+
11
+ /** Per-instance Container startup configuration. */
12
+ export interface ContainerStartOptions {
13
+ readonly envVars?: Record<string, string>;
14
+ readonly entrypoint?: Array<string>;
15
+ readonly enableInternet?: boolean;
16
+ readonly labels?: Record<string, string>;
17
+ }
18
+
19
+ /** Serializable retry settings used while starting a Container. */
20
+ export interface ContainerWaitOptions {
21
+ readonly portToCheck: number;
22
+ readonly retries?: number;
23
+ readonly waitInterval?: number;
24
+ }
25
+
26
+ /** Serializable timeout settings used while waiting for Container ports. */
27
+ export interface ContainerReadinessOptions {
28
+ readonly instanceGetTimeoutMS?: number;
29
+ readonly portReadyTimeoutMS?: number;
30
+ readonly waitInterval?: number;
31
+ }
32
+
33
+ /** Options for starting a Container and waiting for its ports. */
34
+ export interface ContainerStartAndWaitForPortsOptions {
35
+ readonly startOptions?: ContainerStartOptions;
36
+ readonly ports?: number | Array<number>;
37
+ readonly cancellationOptions?: ContainerReadinessOptions;
38
+ }
39
+
40
+ /** Serializable state reported by a Cloudflare Container instance. */
41
+ export const ContainerState = Schema.Union([
42
+ Schema.Struct({
43
+ lastChange: Schema.Finite,
44
+ status: Schema.Literals(["running", "stopping", "stopped", "healthy"]),
45
+ }),
46
+ Schema.Struct({
47
+ lastChange: Schema.Finite,
48
+ status: Schema.Literal("stopped_with_code"),
49
+ exitCode: Schema.optionalKey(Schema.Int),
50
+ }),
51
+ ]);
52
+ export type ContainerState = typeof ContainerState.Type;
53
+
54
+ /**
55
+ * Native Container stub shape exposed by a Container namespace binding.
56
+ *
57
+ * This is structural so using Container bindings does not make
58
+ * `@cloudflare/containers` a runtime dependency of effect-cf.
59
+ */
60
+ export interface ContainerStub {
61
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
62
+ getState(): Promise<ContainerState>;
63
+ start(options?: ContainerStartOptions, waitOptions?: ContainerWaitOptions): Promise<void>;
64
+ startAndWaitForPorts(options?: ContainerStartAndWaitForPortsOptions): Promise<void>;
65
+ stop(signal?: ContainerSignal): Promise<void>;
66
+ destroy(): Promise<void>;
67
+ }
68
+
69
+ /** Native Container namespace binding shape. */
70
+ export interface ContainerNamespaceResource {
71
+ getByName(
72
+ name: string,
73
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
74
+ ): ContainerStub;
75
+ }
76
+
77
+ /** Container namespace binding metadata. */
78
+ export interface ContainerNamespaceDefinition {
79
+ /** Binding name as configured in `wrangler.jsonc`. */
80
+ readonly binding: string;
81
+ }
82
+
83
+ /** Failure raised when invoking a named Container operation. */
84
+ export class ContainerOperationError extends Data.TaggedError("ContainerOperationError")<{
85
+ readonly binding: string;
86
+ readonly instance: string;
87
+ readonly operation: string;
88
+ readonly cause: unknown;
89
+ }> {}
90
+
91
+ /** Effect-wrapped client for one named Container instance. */
92
+ export interface ContainerInstanceClient {
93
+ readonly unsafeRaw: Effect.Effect<ContainerStub, ContainerOperationError>;
94
+ readonly state: Effect.Effect<ContainerState, ContainerOperationError>;
95
+ readonly fetch: (
96
+ input: RequestInfo | URL,
97
+ init?: RequestInit,
98
+ ) => Effect.Effect<Response, ContainerOperationError>;
99
+ readonly start: (
100
+ options?: ContainerStartOptions,
101
+ waitOptions?: ContainerWaitOptions,
102
+ ) => Effect.Effect<void, ContainerOperationError>;
103
+ readonly startAndWaitForPorts: (
104
+ options?: ContainerStartAndWaitForPortsOptions,
105
+ ) => Effect.Effect<void, ContainerOperationError>;
106
+ readonly stop: (signal?: ContainerSignal) => Effect.Effect<void, ContainerOperationError>;
107
+ readonly destroy: Effect.Effect<void, ContainerOperationError>;
108
+ }
109
+
110
+ /** Effect client for a Container namespace binding. */
111
+ export interface ContainerNamespaceClient {
112
+ readonly definition: ContainerNamespaceDefinition;
113
+ readonly getByName: (
114
+ name: string,
115
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
116
+ ) => Effect.Effect<ContainerInstanceClient, ContainerOperationError>;
117
+ readonly byName: (
118
+ name: string,
119
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
120
+ ) => ContainerInstanceClient;
121
+ readonly unsafeRaw: Effect.Effect<ContainerNamespaceResource>;
122
+ }
123
+
124
+ export type ContainerNamespaceStaticClient<R> = {
125
+ readonly getByName: (
126
+ name: string,
127
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
128
+ ) => Effect.Effect<ContainerInstanceClient, ContainerOperationError, R>;
129
+ readonly byName: (
130
+ name: string,
131
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
132
+ ) => {
133
+ readonly state: Effect.Effect<ContainerState, ContainerOperationError, R>;
134
+ readonly fetch: (
135
+ input: RequestInfo | URL,
136
+ init?: RequestInit,
137
+ ) => Effect.Effect<Response, ContainerOperationError, R>;
138
+ readonly start: (
139
+ options?: ContainerStartOptions,
140
+ waitOptions?: ContainerWaitOptions,
141
+ ) => Effect.Effect<void, ContainerOperationError, R>;
142
+ readonly startAndWaitForPorts: (
143
+ options?: ContainerStartAndWaitForPortsOptions,
144
+ ) => Effect.Effect<void, ContainerOperationError, R>;
145
+ readonly stop: (signal?: ContainerSignal) => Effect.Effect<void, ContainerOperationError, R>;
146
+ readonly destroy: Effect.Effect<void, ContainerOperationError, R>;
147
+ readonly unsafeRaw: Effect.Effect<ContainerStub, ContainerOperationError, R>;
148
+ };
149
+ readonly unsafeRaw: () => Effect.Effect<ContainerNamespaceResource, never, R>;
150
+ };
151
+
152
+ export type LayerOptions = {
153
+ readonly binding: string;
154
+ };
155
+
156
+ export interface TagClass<Self, Id extends string>
157
+ extends
158
+ Context.ServiceClass<Self, Id, ContainerNamespaceClient>,
159
+ ContainerNamespaceStaticClient<Self> {
160
+ readonly id: Id;
161
+ readonly layer: (
162
+ options: LayerOptions,
163
+ ) => Layer.Layer<
164
+ Self,
165
+ Binding.BindingNotFoundError | Binding.BindingValidationError,
166
+ WorkerEnvironment
167
+ >;
168
+ }
169
+
170
+ const tryContainerPromise = <A>(
171
+ definition: ContainerNamespaceDefinition,
172
+ instance: string,
173
+ operation: string,
174
+ evaluate: () => Promise<A>,
175
+ ): Effect.Effect<A, ContainerOperationError> =>
176
+ Effect.tryPromise({
177
+ try: evaluate,
178
+ catch: (cause) =>
179
+ new ContainerOperationError({
180
+ binding: definition.binding,
181
+ instance,
182
+ operation,
183
+ cause,
184
+ }),
185
+ });
186
+
187
+ const makeInstanceClient = (
188
+ definition: ContainerNamespaceDefinition,
189
+ instance: string,
190
+ stub: ContainerStub,
191
+ ): ContainerInstanceClient => ({
192
+ unsafeRaw: Effect.succeed(stub),
193
+ state: tryContainerPromise(definition, instance, "state", () => stub.getState()).pipe(
194
+ Effect.flatMap(Schema.decodeUnknownEffect(ContainerState)),
195
+ Effect.mapError((cause) =>
196
+ cause instanceof ContainerOperationError
197
+ ? cause
198
+ : new ContainerOperationError({
199
+ binding: definition.binding,
200
+ instance,
201
+ operation: "state",
202
+ cause,
203
+ }),
204
+ ),
205
+ ),
206
+ fetch: (input, init) =>
207
+ tryContainerPromise(definition, instance, "fetch", () => stub.fetch(input, init)),
208
+ start: (options, waitOptions) =>
209
+ tryContainerPromise(definition, instance, "start", () => stub.start(options, waitOptions)),
210
+ startAndWaitForPorts: (options) =>
211
+ tryContainerPromise(definition, instance, "startAndWaitForPorts", () =>
212
+ stub.startAndWaitForPorts(options),
213
+ ),
214
+ stop: (signal) => tryContainerPromise(definition, instance, "stop", () => stub.stop(signal)),
215
+ destroy: tryContainerPromise(definition, instance, "destroy", () => stub.destroy()),
216
+ });
217
+
218
+ export const isContainerNamespaceResource = (value: unknown): value is ContainerNamespaceResource =>
219
+ typeof value === "object" &&
220
+ value !== null &&
221
+ typeof Reflect.get(value, "getByName") === "function";
222
+
223
+ export const makeClient =
224
+ (definition: ContainerNamespaceDefinition) =>
225
+ (namespace: ContainerNamespaceResource): ContainerNamespaceClient => {
226
+ const getByName = (
227
+ name: string,
228
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
229
+ ) =>
230
+ Effect.try({
231
+ try: () => makeInstanceClient(definition, name, namespace.getByName(name, options)),
232
+ catch: (cause) =>
233
+ new ContainerOperationError({
234
+ binding: definition.binding,
235
+ instance: name,
236
+ operation: "getByName",
237
+ cause,
238
+ }),
239
+ });
240
+
241
+ const byName = (
242
+ name: string,
243
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
244
+ ): ContainerInstanceClient => {
245
+ const instance = getByName(name, options);
246
+ return {
247
+ unsafeRaw: Effect.flatMap(instance, (client) => client.unsafeRaw),
248
+ state: Effect.flatMap(instance, (client) => client.state),
249
+ fetch: (input, init) => Effect.flatMap(instance, (client) => client.fetch(input, init)),
250
+ start: (startOptions, waitOptions) =>
251
+ Effect.flatMap(instance, (client) => client.start(startOptions, waitOptions)),
252
+ startAndWaitForPorts: (startOptions) =>
253
+ Effect.flatMap(instance, (client) => client.startAndWaitForPorts(startOptions)),
254
+ stop: (signal) => Effect.flatMap(instance, (client) => client.stop(signal)),
255
+ destroy: Effect.flatMap(instance, (client) => client.destroy),
256
+ };
257
+ };
258
+
259
+ return {
260
+ definition,
261
+ getByName,
262
+ byName,
263
+ unsafeRaw: Effect.succeed(namespace),
264
+ };
265
+ };
266
+
267
+ export const layer = <Self>(
268
+ tag: Context.Service<Self, ContainerNamespaceClient>,
269
+ definition: ContainerNamespaceDefinition,
270
+ ) =>
271
+ Binding.layer(tag, definition.binding, isContainerNamespaceResource, makeClient(definition), {
272
+ expected: expectedContainerNamespace,
273
+ });
274
+
275
+ export const make = <Id extends string>(id: Id) => Tag<ContainerNamespaceService<Id>>()<Id>(id);
276
+
277
+ declare const ContainerNamespaceServiceTypeId: unique symbol;
278
+
279
+ /** Nominal service marker for Container namespaces created with {@link make}. */
280
+ export interface ContainerNamespaceService<Id extends string> {
281
+ readonly [ContainerNamespaceServiceTypeId]: {
282
+ readonly id: Id;
283
+ };
284
+ }
285
+
286
+ /** Creates a typed Effect service for a Cloudflare Container namespace. */
287
+ export const Tag =
288
+ <Self>() =>
289
+ <Id extends string>(id: Id) => {
290
+ const tag = Context.Service<Self, ContainerNamespaceClient>()(id);
291
+ const makeLayer = (definition: LayerOptions) => layer(tag, definition);
292
+
293
+ const getByName = (
294
+ name: string,
295
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
296
+ ) =>
297
+ Effect.gen(function* () {
298
+ const namespace = yield* tag;
299
+ return yield* namespace.getByName(name, options);
300
+ });
301
+
302
+ const byName = (
303
+ name: string,
304
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
305
+ ) => ({
306
+ state: Effect.flatMap(getByName(name, options), (instance) => instance.state),
307
+ fetch: (input: RequestInfo | URL, init?: RequestInit) =>
308
+ Effect.flatMap(getByName(name, options), (instance) => instance.fetch(input, init)),
309
+ start: (startOptions?: ContainerStartOptions, waitOptions?: ContainerWaitOptions) =>
310
+ Effect.flatMap(getByName(name, options), (instance) =>
311
+ instance.start(startOptions, waitOptions),
312
+ ),
313
+ startAndWaitForPorts: (startOptions?: ContainerStartAndWaitForPortsOptions) =>
314
+ Effect.flatMap(getByName(name, options), (instance) =>
315
+ instance.startAndWaitForPorts(startOptions),
316
+ ),
317
+ stop: (signal?: ContainerSignal) =>
318
+ Effect.flatMap(getByName(name, options), (instance) => instance.stop(signal)),
319
+ destroy: Effect.flatMap(getByName(name, options), (instance) => instance.destroy),
320
+ unsafeRaw: Effect.flatMap(getByName(name, options), (instance) => instance.unsafeRaw),
321
+ });
322
+
323
+ const unsafeRaw = Effect.fn(function* () {
324
+ const namespace = yield* tag;
325
+ return yield* namespace.unsafeRaw;
326
+ });
327
+
328
+ return Object.assign(tag, {
329
+ id,
330
+ layer: makeLayer,
331
+ getByName,
332
+ byName,
333
+ unsafeRaw,
334
+ }) as TagClass<Self, Id>;
335
+ };
336
+
337
+ export const ContainerNamespace = Tag;
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export * as AiGateway from "./AiGateway";
2
+ export * as AnalyticsEngine from "./AnalyticsEngine";
2
3
  export * as Binding from "./Binding";
3
4
  export * as BrowserRendering from "./BrowserRendering";
4
5
  export * as CloudflareOtlp from "./CloudflareOtlp";
6
+ export * as ContainerNamespace from "./ContainerNamespace";
5
7
  export * as D1 from "./D1";
6
8
  export * as DurableObject from "./DurableObject";
7
9
  export * as DurableObjectAlarm from "./DurableObjectAlarm";