effect-cf 0.4.0 → 0.6.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/src/D1.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { D1Client } from "@effect/sql-d1";
2
+ import { Data, Effect, Layer } from "effect";
3
+
4
+ import * as Binding from "./Binding";
5
+
6
+ const TypeId = "effect-cf/D1" as const;
7
+
8
+ /** Error raised when a D1 operation fails. */
9
+ export class D1OperationError extends Data.TaggedError("D1OperationError")<{
10
+ readonly binding: string;
11
+ readonly operation: string;
12
+ readonly cause: unknown;
13
+ }> {}
14
+
15
+ /** Typed D1 binding definition. */
16
+ export interface D1Definition {
17
+ /** Binding name as configured in `wrangler.jsonc`. */
18
+ readonly binding: string;
19
+ }
20
+
21
+ /** Options forwarded to `@effect/sql-d1` when building a SQL client layer. */
22
+ export type D1SqlLayerOptions = Omit<D1Client.D1ClientConfig, "db">;
23
+
24
+ declare const D1ServiceTypeId: unique symbol;
25
+
26
+ /** Nominal service marker for D1 services created with {@link make}. */
27
+ export interface D1Service<Id extends string> {
28
+ readonly [D1ServiceTypeId]: {
29
+ readonly id: Id;
30
+ };
31
+ }
32
+
33
+ const isD1Database = (value: unknown): value is D1Database => {
34
+ if (typeof value !== "object" || value === null) {
35
+ return false;
36
+ }
37
+
38
+ const resource = value as Record<string, unknown>;
39
+
40
+ return (
41
+ typeof resource.prepare === "function" &&
42
+ typeof resource.batch === "function" &&
43
+ typeof resource.exec === "function"
44
+ );
45
+ };
46
+
47
+ const d1Error = (binding: string, operation: string, cause: unknown) =>
48
+ new D1OperationError({ binding, operation, cause });
49
+
50
+ const tryD1Promise = <A>(
51
+ binding: string,
52
+ operation: string,
53
+ evaluate: () => Promise<A>,
54
+ ): Effect.Effect<A, D1OperationError> =>
55
+ Effect.tryPromise({
56
+ try: evaluate,
57
+ catch: (cause) => d1Error(binding, operation, cause),
58
+ });
59
+
60
+ const tryD1Sync = <A>(
61
+ binding: string,
62
+ operation: string,
63
+ evaluate: () => A,
64
+ ): Effect.Effect<A, D1OperationError> =>
65
+ Effect.try({
66
+ try: evaluate,
67
+ catch: (cause) => d1Error(binding, operation, cause),
68
+ });
69
+
70
+ /**
71
+ * Creates a typed D1 service tag plus Effect helpers.
72
+ */
73
+ export const make = <Id extends string>(id: Id, definition: D1Definition) =>
74
+ Service<D1Service<Id>>()(id, definition);
75
+
76
+ /**
77
+ * Builds a D1 service around a Cloudflare D1 database binding.
78
+ *
79
+ * The returned service exposes the raw `D1Database` binding, small native D1
80
+ * helpers, and `sqlLayer(...)` for providing `effect/unstable/sql` via
81
+ * `@effect/sql-d1`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * class TodoDatabase extends D1.Service<TodoDatabase>()("TodoDatabase", {
86
+ * binding: "TODO_DB",
87
+ * }) {}
88
+ *
89
+ * const SqlLive = TodoDatabase.sqlLayer();
90
+ * ```
91
+ */
92
+ export const Service =
93
+ <Self>() =>
94
+ <Id extends string>(id: Id, definition: D1Definition) => {
95
+ const tag = Binding.Service<Self>()(id, definition.binding, isD1Database);
96
+
97
+ const sqlLayer = (options?: D1SqlLayerOptions) =>
98
+ Layer.unwrap(
99
+ Effect.gen(function* () {
100
+ const db = yield* tag;
101
+ return D1Client.layer({ ...options, db });
102
+ }),
103
+ ).pipe(Layer.provide(tag.layer));
104
+
105
+ const prepare = Effect.fnUntraced(function* (query: string) {
106
+ const db = yield* tag;
107
+ return yield* tryD1Sync(definition.binding, "prepare", () => db.prepare(query));
108
+ });
109
+
110
+ const batch = Effect.fnUntraced(function* <T = unknown>(
111
+ statements: ReadonlyArray<D1PreparedStatement>,
112
+ ) {
113
+ const db = yield* tag;
114
+ return yield* tryD1Promise(definition.binding, "batch", () => db.batch<T>([...statements]));
115
+ });
116
+
117
+ const exec = Effect.fnUntraced(function* (query: string) {
118
+ const db = yield* tag;
119
+ return yield* tryD1Promise(definition.binding, "exec", () => db.exec(query));
120
+ });
121
+
122
+ const unsafeRaw = Effect.fnUntraced(function* () {
123
+ return yield* tag;
124
+ });
125
+
126
+ return Object.assign(tag, {
127
+ [TypeId]: TypeId,
128
+ definition,
129
+ sqlLayer,
130
+ prepare,
131
+ batch,
132
+ exec,
133
+ unsafeRaw,
134
+ });
135
+ };
@@ -1,4 +1,4 @@
1
- import { Data, Effect } from "effect";
1
+ import { Data, Effect, type Scope } from "effect";
2
2
 
3
3
  import * as Binding from "./Binding";
4
4
  import * as CloudflareRpc from "./Rpc";
@@ -110,21 +110,21 @@ type StubMethodSuccess<Api, Method extends keyof Api> = RpcInvocation.AsyncMetho
110
110
  Method
111
111
  >;
112
112
 
113
- type StubCall<Api extends object> = <Method extends StubMethodKey<Api>>(
113
+ type StubCall<R, Api extends object> = <Method extends StubMethodKey<Api>>(
114
114
  stub: DurableObjectStubClient<Api>,
115
115
  method: Method,
116
116
  ...args: StubMethodArgs<Api, Method>
117
- ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError>;
117
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError, R>;
118
118
 
119
- type DurableObjectDirectClient<Self, Api extends object> = {
119
+ type DurableObjectDirectClient<R, Api extends object> = {
120
120
  readonly fetch: (
121
121
  input: RequestInfo | URL,
122
122
  init?: RequestInit,
123
- ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, Self>;
123
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, R>;
124
124
  } & {
125
125
  readonly [Method in StubMethodKey<Api>]: (
126
126
  ...args: StubMethodArgs<Api, Method>
127
- ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError, Self>;
127
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError, R>;
128
128
  };
129
129
 
130
130
  type DefinitionNamespaceDirectMethods<
@@ -141,46 +141,89 @@ type DefinitionNamespaceDirectMethods<
141
141
  };
142
142
 
143
143
  type DefinitionDurableObjectDirectClient<
144
- Self,
144
+ R,
145
145
  Definition extends DurableObjectDefinition.Definition.Any,
146
146
  > = {
147
147
  readonly fetch: (
148
148
  input: RequestInfo | URL,
149
149
  init?: RequestInit,
150
- ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, Self>;
150
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, R>;
151
151
  } & {
152
152
  readonly [Method in RpcDefinition.Definition.MethodNames<Definition>]: (
153
153
  ...args: RpcDefinition.Method.Args<Definition["methods"][Method]>
154
154
  ) => Effect.Effect<
155
155
  RpcDefinition.Method.Success<Definition["methods"][Method]>,
156
156
  DurableObjectRpcError,
157
- Self
157
+ R
158
158
  >;
159
159
  };
160
160
 
161
161
  type DefinitionNamespaceDirectClientMethods<
162
- Self,
162
+ R,
163
163
  Definition extends DurableObjectDefinition.Definition.Any,
164
164
  > = {
165
165
  readonly byName: (
166
166
  name: string,
167
167
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
168
- ) => DefinitionDurableObjectDirectClient<Self, Definition>;
168
+ ) => DefinitionDurableObjectDirectClient<R, Definition>;
169
169
  readonly byId: (
170
170
  id: globalThis.DurableObjectId,
171
171
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
172
- ) => DefinitionDurableObjectDirectClient<Self, Definition>;
172
+ ) => DefinitionDurableObjectDirectClient<R, Definition>;
173
173
  };
174
174
 
175
175
  type DirectMethods<
176
- Self,
176
+ R,
177
177
  Api extends object,
178
178
  Definition,
179
179
  > = Definition extends DurableObjectDefinition.Definition.Any
180
180
  ? DefinitionNamespaceDirectMethods<Api, Definition> &
181
- DefinitionNamespaceDirectClientMethods<Self, Definition>
181
+ DefinitionNamespaceDirectClientMethods<R, Definition>
182
182
  : {};
183
183
 
184
+ export type DurableObjectNamespaceEffectClient<
185
+ Api extends object,
186
+ Definition extends DurableObjectDefinition.Definition.Any | undefined = undefined,
187
+ > = DirectMethods<never, Api, Definition> & {
188
+ readonly newUniqueId: (
189
+ options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions,
190
+ ) => Effect.Effect<globalThis.DurableObjectId>;
191
+ readonly idFromName: (name: string) => Effect.Effect<globalThis.DurableObjectId>;
192
+ readonly idFromString: (id: string) => Effect.Effect<globalThis.DurableObjectId>;
193
+ readonly get: (
194
+ id: globalThis.DurableObjectId,
195
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
196
+ ) => Effect.Effect<DurableObjectStubClient<Api>>;
197
+ readonly getByName: (
198
+ name: string,
199
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
200
+ ) => Effect.Effect<DurableObjectStubClient<Api>>;
201
+ readonly jurisdiction: (
202
+ jurisdiction: globalThis.DurableObjectJurisdiction,
203
+ ) => Effect.Effect<DurableObjectNamespaceClient<Api>>;
204
+ readonly fetch: (
205
+ stub: DurableObjectStubClient<Api>,
206
+ input: RequestInfo | URL,
207
+ init?: RequestInit,
208
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError>;
209
+ readonly rpc: <Method extends StubMethodKey<Api>>(
210
+ stub: DurableObjectStubClient<Api>,
211
+ method: Method,
212
+ ...args: StubMethodArgs<Api, Method>
213
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError>;
214
+ readonly call: <Method extends StubMethodKey<Api>>(
215
+ stub: DurableObjectStubClient<Api>,
216
+ method: Method,
217
+ ...args: StubMethodArgs<Api, Method>
218
+ ) => Effect.Effect<StubMethodSuccess<Api, Method>, DurableObjectRpcError>;
219
+ readonly scopedCall: <Method extends StubMethodKey<Api>>(
220
+ stub: DurableObjectStubClient<Api>,
221
+ method: Method,
222
+ ...args: StubMethodArgs<Api, Method>
223
+ ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope>;
224
+ readonly unsafeRaw: Effect.Effect<DurableObjectNamespaceClient<Api>>;
225
+ };
226
+
184
227
  /**
185
228
  * Creates a typed Effect service for a Durable Object namespace binding.
186
229
  *
@@ -230,25 +273,170 @@ export const Service =
230
273
  hasFunction(value, "newUniqueId") &&
231
274
  hasFunction(value, "jurisdiction");
232
275
 
233
- const tag = Binding.Service<Self>()(id, definition.binding, (value): value is NamespaceClient =>
234
- isDurableObjectNamespaceClient(value),
276
+ const makeClient = (
277
+ namespace: NamespaceClient,
278
+ ): DurableObjectNamespaceEffectClient<NamespaceApi, Definition> => {
279
+ const newUniqueId = (options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions) =>
280
+ Effect.sync(() => namespace.newUniqueId(options));
281
+
282
+ const idFromName = (name: string) => Effect.sync(() => namespace.idFromName(name));
283
+
284
+ const idFromString = (id: string) => Effect.sync(() => namespace.idFromString(id));
285
+
286
+ const get = (
287
+ id: globalThis.DurableObjectId,
288
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
289
+ ) => Effect.sync(() => namespace.get(id, options));
290
+
291
+ const getByName = (
292
+ name: string,
293
+ options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
294
+ ) => Effect.sync(() => namespace.getByName(name, options));
295
+
296
+ const jurisdiction = (value: globalThis.DurableObjectJurisdiction) =>
297
+ Effect.sync(() => namespace.jurisdiction(value));
298
+
299
+ const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
300
+ Effect.tryPromise({
301
+ try: () => stub.fetch(input, init),
302
+ catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
303
+ });
304
+
305
+ const rpc = <Method extends StubMethodKey<NamespaceApi>>(
306
+ stub: StubClient,
307
+ method: Method,
308
+ ...args: StubMethodArgs<NamespaceApi, Method>
309
+ ) =>
310
+ Effect.gen(function* () {
311
+ const methodName = String(method);
312
+ const encodedArgs =
313
+ definition.definition === undefined
314
+ ? args
315
+ : yield* RpcDefinition.encodeArgs(
316
+ definition.definition,
317
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
318
+ args as never,
319
+ ).pipe(
320
+ Effect.mapError(
321
+ (cause) =>
322
+ new DurableObjectRpcError({
323
+ binding: definition.binding,
324
+ method: methodName,
325
+ cause,
326
+ }),
327
+ ),
328
+ );
329
+
330
+ return yield* RpcInvocation.invokeRpcMethod(
331
+ stub,
332
+ method,
333
+ encodedArgs as StubMethodArgs<NamespaceApi, Method>,
334
+ (cause) =>
335
+ new DurableObjectRpcError({
336
+ binding: definition.binding,
337
+ method: methodName,
338
+ cause,
339
+ }),
340
+ );
341
+ });
342
+
343
+ const call = <Method extends StubMethodKey<NamespaceApi>>(
344
+ stub: StubClient,
345
+ method: Method,
346
+ ...args: StubMethodArgs<NamespaceApi, Method>
347
+ ) =>
348
+ Effect.gen(function* () {
349
+ const methodName = String(method);
350
+ const value = yield* CloudflareRpc.resolve(yield* rpc(stub, method, ...args)).pipe(
351
+ Effect.mapError(
352
+ (cause) =>
353
+ new DurableObjectRpcError({
354
+ binding: definition.binding,
355
+ method: methodName,
356
+ cause,
357
+ }),
358
+ ),
359
+ );
360
+
361
+ if (definition.definition === undefined) {
362
+ return value as StubMethodSuccess<NamespaceApi, Method>;
363
+ }
364
+
365
+ const decoded = yield* RpcDefinition.decodeSuccess(
366
+ definition.definition,
367
+ methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
368
+ value,
369
+ ).pipe(
370
+ Effect.mapError(
371
+ (cause) =>
372
+ new DurableObjectRpcError({
373
+ binding: definition.binding,
374
+ method: methodName,
375
+ cause,
376
+ }),
377
+ ),
378
+ );
379
+
380
+ return decoded as StubMethodSuccess<NamespaceApi, Method>;
381
+ });
382
+
383
+ const scopedCall = <Method extends StubMethodKey<NamespaceApi>>(
384
+ stub: StubClient,
385
+ method: Method,
386
+ ...args: StubMethodArgs<NamespaceApi, Method>
387
+ ) =>
388
+ Effect.gen(function* () {
389
+ const result = yield* rpc(stub, method, ...args);
390
+ return yield* CloudflareRpc.scoped(result);
391
+ });
392
+
393
+ const directMethods = makeDirectMethods<never, NamespaceApi, Definition>(
394
+ definition.definition,
395
+ {
396
+ call,
397
+ fetch,
398
+ get,
399
+ getByName,
400
+ },
401
+ );
402
+
403
+ return Object.assign(directMethods, {
404
+ newUniqueId,
405
+ idFromName,
406
+ idFromString,
407
+ get,
408
+ getByName,
409
+ jurisdiction,
410
+ fetch,
411
+ rpc,
412
+ call,
413
+ scopedCall,
414
+ unsafeRaw: Effect.succeed(namespace),
415
+ }) as DurableObjectNamespaceEffectClient<NamespaceApi, Definition>;
416
+ };
417
+
418
+ const tag = Binding.Service<Self>()(
419
+ id,
420
+ definition.binding,
421
+ (value): value is NamespaceClient => isDurableObjectNamespaceClient(value),
422
+ makeClient,
235
423
  );
236
424
 
237
425
  const newUniqueId = Effect.fnUntraced(function* (
238
426
  options?: globalThis.DurableObjectNamespaceNewUniqueIdOptions,
239
427
  ) {
240
428
  const namespace = yield* tag;
241
- return namespace.newUniqueId(options);
429
+ return yield* namespace.newUniqueId(options);
242
430
  });
243
431
 
244
432
  const idFromName = Effect.fnUntraced(function* (name: string) {
245
433
  const namespace = yield* tag;
246
- return namespace.idFromName(name);
434
+ return yield* namespace.idFromName(name);
247
435
  });
248
436
 
249
437
  const idFromString = Effect.fnUntraced(function* (id: string) {
250
438
  const namespace = yield* tag;
251
- return namespace.idFromString(id);
439
+ return yield* namespace.idFromString(id);
252
440
  });
253
441
 
254
442
  const get = Effect.fnUntraced(function* (
@@ -256,7 +444,7 @@ export const Service =
256
444
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
257
445
  ) {
258
446
  const namespace = yield* tag;
259
- return namespace.get(id, options);
447
+ return yield* namespace.get(id, options);
260
448
  });
261
449
 
262
450
  const getByName = Effect.fnUntraced(function* (
@@ -264,12 +452,12 @@ export const Service =
264
452
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
265
453
  ) {
266
454
  const namespace = yield* tag;
267
- return namespace.getByName(name, options);
455
+ return yield* namespace.getByName(name, options);
268
456
  });
269
457
 
270
458
  const jurisdiction = Effect.fnUntraced(function* (value: globalThis.DurableObjectJurisdiction) {
271
459
  const namespace = yield* tag;
272
- return namespace.jurisdiction(value);
460
+ return yield* namespace.jurisdiction(value);
273
461
  });
274
462
 
275
463
  const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
@@ -366,6 +554,11 @@ export const Service =
366
554
  return yield* CloudflareRpc.scoped(result);
367
555
  });
368
556
 
557
+ const unsafeRaw = Effect.fnUntraced(function* () {
558
+ const namespace = yield* tag;
559
+ return yield* namespace.unsafeRaw;
560
+ });
561
+
369
562
  const directMethods = makeDirectMethods<Self, NamespaceApi, Definition>(definition.definition, {
370
563
  call,
371
564
  fetch,
@@ -386,6 +579,7 @@ export const Service =
386
579
  rpc,
387
580
  call,
388
581
  scopedCall,
582
+ unsafeRaw,
389
583
  }) as typeof tag &
390
584
  DirectMethods<Self, NamespaceApi, Definition> & {
391
585
  readonly [TypeId]: typeof TypeId;
@@ -400,32 +594,33 @@ export const Service =
400
594
  readonly rpc: typeof rpc;
401
595
  readonly call: typeof call;
402
596
  readonly scopedCall: typeof scopedCall;
597
+ readonly unsafeRaw: typeof unsafeRaw;
403
598
  };
404
599
  };
405
600
 
406
601
  const makeDirectMethods = <
407
- Self,
602
+ R,
408
603
  Api extends object,
409
604
  Definition extends DurableObjectDefinition.Definition.Any | undefined,
410
605
  >(
411
606
  rpcDefinition: Definition | undefined,
412
607
  helpers: {
413
- readonly call: StubCall<Api>;
608
+ readonly call: StubCall<R, Api>;
414
609
  readonly fetch: (
415
610
  stub: DurableObjectStubClient<Api>,
416
611
  input: RequestInfo | URL,
417
612
  init?: RequestInit,
418
- ) => Effect.Effect<globalThis.Response, DurableObjectFetchError>;
613
+ ) => Effect.Effect<globalThis.Response, DurableObjectFetchError, R>;
419
614
  readonly get: (
420
615
  id: globalThis.DurableObjectId,
421
616
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
422
- ) => Effect.Effect<DurableObjectStubClient<Api>, never, Self>;
617
+ ) => Effect.Effect<DurableObjectStubClient<Api>, never, R>;
423
618
  readonly getByName: (
424
619
  name: string,
425
620
  options?: globalThis.DurableObjectNamespaceGetDurableObjectOptions,
426
- ) => Effect.Effect<DurableObjectStubClient<Api>, never, Self>;
621
+ ) => Effect.Effect<DurableObjectStubClient<Api>, never, R>;
427
622
  },
428
- ): DirectMethods<Self, Api, Definition> => {
623
+ ): DirectMethods<R, Api, Definition> => {
429
624
  const methods = {} as Record<string, unknown>;
430
625
 
431
626
  if (rpcDefinition !== undefined) {
@@ -441,8 +636,8 @@ const makeDirectMethods = <
441
636
  }
442
637
 
443
638
  const makeClient = (
444
- getStub: () => Effect.Effect<DurableObjectStubClient<Api>, never, Self>,
445
- ): DurableObjectDirectClient<Self, Api> => {
639
+ getStub: () => Effect.Effect<DurableObjectStubClient<Api>, never, R>,
640
+ ): DurableObjectDirectClient<R, Api> => {
446
641
  const client = {
447
642
  fetch: (input: RequestInfo | URL, init?: RequestInit) =>
448
643
  Effect.gen(function* () {
@@ -465,7 +660,7 @@ const makeDirectMethods = <
465
660
  });
466
661
  }
467
662
 
468
- return client as DurableObjectDirectClient<Self, Api>;
663
+ return client as DurableObjectDirectClient<R, Api>;
469
664
  };
470
665
 
471
666
  methods.byName = (
@@ -479,5 +674,5 @@ const makeDirectMethods = <
479
674
  ) => makeClient(() => helpers.get(id, options));
480
675
  }
481
676
 
482
- return methods as DirectMethods<Self, Api, Definition>;
677
+ return methods as DirectMethods<R, Api, Definition>;
483
678
  };