effect-cf 0.3.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/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
2
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Layer, ManagedRuntime, Option, Queue, Schema } from "effect";
3
+ import { D1Client } from "@effect/sql-d1";
3
4
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint } from "cloudflare:workers";
4
5
  import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
5
6
  import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
@@ -28,11 +29,11 @@ let WorkerConfig;
28
29
  var Binding_exports = /* @__PURE__ */ __exportAll({
29
30
  BindingNotFoundError: () => BindingNotFoundError,
30
31
  BindingValidationError: () => BindingValidationError,
31
- Service: () => Service$5,
32
- TypeId: () => TypeId$5
32
+ Service: () => Service$6,
33
+ TypeId: () => TypeId$6
33
34
  });
34
35
  /** Internal type id marker used by binding helper services. */
35
- const TypeId$5 = "effect-cf/Binding";
36
+ const TypeId$6 = "effect-cf/Binding";
36
37
  /** Error raised when a configured binding does not exist on `env`. */
37
38
  var BindingNotFoundError = class extends Data.TaggedError("BindingNotFoundError") {};
38
39
  /** Error raised when a binding exists but does not match the expected shape. */
@@ -48,18 +49,100 @@ const getBinding = (env, binding, isResource) => Effect.gen(function* () {
48
49
  /**
49
50
  * Creates a Context tag + layer for reading and validating a Cloudflare binding.
50
51
  */
51
- const Service$5 = () => (id, binding, isResource) => {
52
+ const Service$6 = () => (id, binding, isResource) => {
52
53
  const tag = Context.Service()(id);
53
54
  const layer = Layer.effect(tag, Effect.gen(function* () {
54
55
  return yield* getBinding(yield* WorkerEnvironment, binding, isResource);
55
56
  }));
56
57
  return Object.assign(tag, {
57
- [TypeId$5]: TypeId$5,
58
+ [TypeId$6]: TypeId$6,
58
59
  binding,
59
60
  layer
60
61
  });
61
62
  };
62
63
  //#endregion
64
+ //#region src/D1.ts
65
+ var D1_exports = /* @__PURE__ */ __exportAll({
66
+ D1OperationError: () => D1OperationError,
67
+ Service: () => Service$5,
68
+ make: () => make$11
69
+ });
70
+ const TypeId$5 = "effect-cf/D1";
71
+ /** Error raised when a D1 operation fails. */
72
+ var D1OperationError = class extends Data.TaggedError("D1OperationError") {};
73
+ const isD1Database = (value) => {
74
+ if (typeof value !== "object" || value === null) return false;
75
+ const resource = value;
76
+ return typeof resource.prepare === "function" && typeof resource.batch === "function" && typeof resource.exec === "function";
77
+ };
78
+ const d1Error = (binding, operation, cause) => new D1OperationError({
79
+ binding,
80
+ operation,
81
+ cause
82
+ });
83
+ const tryD1Promise = (binding, operation, evaluate) => Effect.tryPromise({
84
+ try: evaluate,
85
+ catch: (cause) => d1Error(binding, operation, cause)
86
+ });
87
+ const tryD1Sync = (binding, operation, evaluate) => Effect.try({
88
+ try: evaluate,
89
+ catch: (cause) => d1Error(binding, operation, cause)
90
+ });
91
+ /**
92
+ * Creates a typed D1 service tag plus Effect helpers.
93
+ */
94
+ const make$11 = (id, definition) => Service$5()(id, definition);
95
+ /**
96
+ * Builds a D1 service around a Cloudflare D1 database binding.
97
+ *
98
+ * The returned service exposes the raw `D1Database` binding, small native D1
99
+ * helpers, and `sqlLayer(...)` for providing `effect/unstable/sql` via
100
+ * `@effect/sql-d1`.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * class TodoDatabase extends D1.Service<TodoDatabase>()("TodoDatabase", {
105
+ * binding: "TODO_DB",
106
+ * }) {}
107
+ *
108
+ * const SqlLive = TodoDatabase.sqlLayer();
109
+ * ```
110
+ */
111
+ const Service$5 = () => (id, definition) => {
112
+ const tag = Service$6()(id, definition.binding, isD1Database);
113
+ const sqlLayer = (options) => Layer.unwrap(Effect.gen(function* () {
114
+ const db = yield* tag;
115
+ return D1Client.layer({
116
+ ...options,
117
+ db
118
+ });
119
+ })).pipe(Layer.provide(tag.layer));
120
+ const prepare = Effect.fnUntraced(function* (query) {
121
+ const db = yield* tag;
122
+ return yield* tryD1Sync(definition.binding, "prepare", () => db.prepare(query));
123
+ });
124
+ const batch = Effect.fnUntraced(function* (statements) {
125
+ const db = yield* tag;
126
+ return yield* tryD1Promise(definition.binding, "batch", () => db.batch([...statements]));
127
+ });
128
+ const exec = Effect.fnUntraced(function* (query) {
129
+ const db = yield* tag;
130
+ return yield* tryD1Promise(definition.binding, "exec", () => db.exec(query));
131
+ });
132
+ const unsafeRaw = Effect.fnUntraced(function* () {
133
+ return yield* tag;
134
+ });
135
+ return Object.assign(tag, {
136
+ [TypeId$5]: TypeId$5,
137
+ definition,
138
+ sqlLayer,
139
+ prepare,
140
+ batch,
141
+ exec,
142
+ unsafeRaw
143
+ });
144
+ };
145
+ //#endregion
63
146
  //#region src/QueueBinding.ts
64
147
  var QueueBinding_exports = /* @__PURE__ */ __exportAll({
65
148
  QueueOperationError: () => QueueOperationError,
@@ -88,7 +171,7 @@ const make$10 = (id, definition) => Service$4()(id, definition);
88
171
  * Builds an Effect service around a Cloudflare Queue producer binding.
89
172
  */
90
173
  const Service$4 = () => (id, definition) => {
91
- const tag = Service$5()(id, definition.binding, (value) => isQueue(value));
174
+ const tag = Service$6()(id, definition.binding, (value) => isQueue(value));
92
175
  const encodeMessage = Schema.encodeEffect(definition.message);
93
176
  const send = Effect.fnUntraced(function* (message, options) {
94
177
  const queue = yield* tag;
@@ -120,11 +203,11 @@ const Service$4 = () => (id, definition) => {
120
203
  //#region src/QueueDefinition.ts
121
204
  var QueueDefinition_exports = /* @__PURE__ */ __exportAll({
122
205
  Queue: () => Queue$1,
123
- Tag: () => Tag$7,
206
+ Tag: () => Tag$8,
124
207
  implement: () => implement$7,
125
208
  make: () => make$9
126
209
  });
127
- const makeDefinition$3 = (id, definition) => {
210
+ const makeDefinition$4 = (id, definition) => {
128
211
  const queueDefinition = {
129
212
  id,
130
213
  message: definition.message
@@ -144,9 +227,9 @@ const makeDefinition$3 = (id, definition) => {
144
227
  })
145
228
  });
146
229
  };
147
- const make$9 = makeDefinition$3;
148
- const Tag$7 = () => (id, definition) => {
149
- const queueDefinition = makeDefinition$3(id, definition);
230
+ const make$9 = makeDefinition$4;
231
+ const Tag$8 = () => (id, definition) => {
232
+ const queueDefinition = makeDefinition$4(id, definition);
150
233
  class QueueDefinitionClass {
151
234
  static id = queueDefinition.id;
152
235
  static message = queueDefinition.message;
@@ -156,7 +239,7 @@ const Tag$7 = () => (id, definition) => {
156
239
  }
157
240
  return QueueDefinitionClass;
158
241
  };
159
- const Queue$1 = Tag$7;
242
+ const Queue$1 = Tag$8;
160
243
  const wrapHandler$1 = (definition, handler) => {
161
244
  const decodeBody = Schema.decodeUnknownEffect(definition.message);
162
245
  return (batch) => Effect.gen(function* () {
@@ -168,7 +251,7 @@ const implement$7 = (_definition, handler) => handler;
168
251
  //#region src/Queue.ts
169
252
  var Queue_exports = /* @__PURE__ */ __exportAll({
170
253
  QueueMessageDecodeError: () => QueueMessageDecodeError,
171
- Tag: () => Tag$6,
254
+ Tag: () => Tag$7,
172
255
  decodeBatch: () => decodeBatch,
173
256
  fromMessage: () => fromMessage,
174
257
  fromMessageBatch: () => fromMessageBatch,
@@ -206,7 +289,7 @@ const decodeBatch = (batch, decodeBody) => Effect.gen(function* () {
206
289
  }
207
290
  return fromMessageBatch(batch, messages);
208
291
  });
209
- const Tag$6 = Tag$7;
292
+ const Tag$7 = Tag$8;
210
293
  const implement$6 = implement$7;
211
294
  //#endregion
212
295
  //#region src/RpcDefinition.ts
@@ -421,7 +504,7 @@ var ServiceBindingRpcError = class extends Data.TaggedError("ServiceBindingRpcEr
421
504
  * ```
422
505
  */
423
506
  const Service$3 = () => (id, definition) => {
424
- const tag = Service$5()(id, definition.binding, (value) => typeof value === "object" && value !== null && "fetch" in value);
507
+ const tag = Service$6()(id, definition.binding, (value) => typeof value === "object" && value !== null && "fetch" in value);
425
508
  const fetch = (input, init) => Effect.gen(function* () {
426
509
  const service = yield* tag;
427
510
  return yield* Effect.tryPromise({
@@ -480,7 +563,7 @@ const makeDirectMethods$1 = (rpcDefinition, call) => {
480
563
  //#endregion
481
564
  //#region src/WorkerDefinition.ts
482
565
  var WorkerDefinition_exports = /* @__PURE__ */ __exportAll({
483
- Tag: () => Tag$5,
566
+ Tag: () => Tag$6,
484
567
  Worker: () => Worker,
485
568
  implement: () => implement$5,
486
569
  make: () => make$7,
@@ -519,7 +602,7 @@ const method$3 = method$4;
519
602
  * });
520
603
  * ```
521
604
  */
522
- const makeDefinition$2 = (id, methods) => {
605
+ const makeDefinition$3 = (id, methods) => {
523
606
  assertNoReservedMethods("Worker", methods, reservedMethodNames$3);
524
607
  const definition = make$8(id, methods);
525
608
  return Object.assign(definition, {
@@ -537,9 +620,9 @@ const makeDefinition$2 = (id, methods) => {
537
620
  })
538
621
  });
539
622
  };
540
- const make$7 = makeDefinition$2;
541
- const Tag$5 = () => (id, methods) => {
542
- const definition = makeDefinition$2(id, methods);
623
+ const make$7 = makeDefinition$3;
624
+ const Tag$6 = () => (id, methods) => {
625
+ const definition = makeDefinition$3(id, methods);
543
626
  class WorkerDefinitionClass {
544
627
  static id = definition.id;
545
628
  static methods = definition.methods;
@@ -549,7 +632,7 @@ const Tag$5 = () => (id, methods) => {
549
632
  }
550
633
  return WorkerDefinitionClass;
551
634
  };
552
- const Worker = Tag$5;
635
+ const Worker = Tag$6;
553
636
  const wrapHandlers$1 = (definition, handlers) => {
554
637
  const wrapped = {};
555
638
  for (const key of Object.keys(definition.methods)) {
@@ -605,7 +688,7 @@ const fromExecutionContext = (ctx, runPromiseExit) => {
605
688
  var Worker_exports = /* @__PURE__ */ __exportAll({
606
689
  ExecutionContext: () => ExecutionContext,
607
690
  NativeRequest: () => NativeRequest,
608
- Tag: () => Tag$4,
691
+ Tag: () => Tag$5,
609
692
  WorkerContext: () => WorkerContext,
610
693
  implement: () => implement$4,
611
694
  isWebSocketUpgrade: () => isWebSocketUpgrade,
@@ -672,7 +755,7 @@ const makeFetchHandler = (layer, options) => {
672
755
  const WorkerClass = make$6(layer, options);
673
756
  return { fetch: (request, env, ctx) => Promise.resolve(new WorkerClass(ctx, env).fetch(request)) };
674
757
  };
675
- const Tag$4 = Tag$5;
758
+ const Tag$5 = Tag$6;
676
759
  const method$2 = method$3;
677
760
  const implement$4 = implement$5;
678
761
  //#endregion
@@ -1006,7 +1089,7 @@ var DurableObjectFetchError = class extends Data.TaggedError("DurableObjectFetch
1006
1089
  const Service$2 = () => (id, definition) => {
1007
1090
  const hasFunction = (value, key) => typeof Reflect.get(value, key) === "function";
1008
1091
  const isDurableObjectNamespaceClient = (value) => typeof value === "object" && value !== null && hasFunction(value, "getByName") && hasFunction(value, "get") && hasFunction(value, "idFromName") && hasFunction(value, "idFromString") && hasFunction(value, "newUniqueId") && hasFunction(value, "jurisdiction");
1009
- const tag = Service$5()(id, definition.binding, (value) => isDurableObjectNamespaceClient(value));
1092
+ const tag = Service$6()(id, definition.binding, (value) => isDurableObjectNamespaceClient(value));
1010
1093
  const newUniqueId = Effect.fnUntraced(function* (options) {
1011
1094
  return (yield* tag).newUniqueId(options);
1012
1095
  });
@@ -1106,7 +1189,7 @@ const makeDirectMethods = (rpcDefinition, helpers) => {
1106
1189
  //#region src/DurableObjectDefinition.ts
1107
1190
  var DurableObjectDefinition_exports = /* @__PURE__ */ __exportAll({
1108
1191
  DurableObject: () => DurableObject$1,
1109
- Tag: () => Tag$3,
1192
+ Tag: () => Tag$4,
1110
1193
  implement: () => implement$3,
1111
1194
  make: () => make$5,
1112
1195
  method: () => method$1
@@ -1137,7 +1220,7 @@ const method$1 = method$4;
1137
1220
  * });
1138
1221
  * ```
1139
1222
  */
1140
- const makeDefinition$1 = (id, methods) => {
1223
+ const makeDefinition$2 = (id, methods) => {
1141
1224
  assertNoReservedMethods("Durable Object", methods, reservedMethodNames$1);
1142
1225
  const definition = make$8(id, methods);
1143
1226
  return Object.assign(definition, {
@@ -1155,9 +1238,9 @@ const makeDefinition$1 = (id, methods) => {
1155
1238
  })
1156
1239
  });
1157
1240
  };
1158
- const make$5 = makeDefinition$1;
1159
- const Tag$3 = () => (id, methods) => {
1160
- const definition = makeDefinition$1(id, methods);
1241
+ const make$5 = makeDefinition$2;
1242
+ const Tag$4 = () => (id, methods) => {
1243
+ const definition = makeDefinition$2(id, methods);
1161
1244
  class DurableObjectDefinitionClass {
1162
1245
  static id = definition.id;
1163
1246
  static methods = definition.methods;
@@ -1167,7 +1250,7 @@ const Tag$3 = () => (id, methods) => {
1167
1250
  }
1168
1251
  return DurableObjectDefinitionClass;
1169
1252
  };
1170
- const DurableObject$1 = Tag$3;
1253
+ const DurableObject$1 = Tag$4;
1171
1254
  const wrapHandlers = (definition, handlers) => {
1172
1255
  const wrapped = {};
1173
1256
  for (const key of Object.keys(definition.methods)) {
@@ -1185,7 +1268,7 @@ const implement$3 = (_definition, handlers) => handlers;
1185
1268
  //#endregion
1186
1269
  //#region src/DurableObject.ts
1187
1270
  var DurableObject_exports = /* @__PURE__ */ __exportAll({
1188
- Tag: () => Tag$2,
1271
+ Tag: () => Tag$3,
1189
1272
  implement: () => implement$2,
1190
1273
  make: () => make$4,
1191
1274
  method: () => method
@@ -1242,7 +1325,7 @@ const make$4 = (layer, options = {}) => {
1242
1325
  defineEntrypointRpcMethods("Durable Object", EffectDurableObject.prototype, options.rpc, reservedMethodNames, (self, effect) => self[RunSymbol](effect));
1243
1326
  return assumeEntrypointClass(EffectDurableObject);
1244
1327
  };
1245
- const Tag$2 = Tag$3;
1328
+ const Tag$3 = Tag$4;
1246
1329
  const method = method$1;
1247
1330
  const implement$2 = implement$3;
1248
1331
  //#endregion
@@ -1698,6 +1781,7 @@ const writeAttachment = (socket, key, clientId) => {
1698
1781
  var Kv_exports = /* @__PURE__ */ __exportAll({
1699
1782
  KvOperationError: () => KvOperationError,
1700
1783
  Service: () => Service$1,
1784
+ Tag: () => Tag$2,
1701
1785
  make: () => make$3
1702
1786
  });
1703
1787
  const TypeId$1 = "effect-cf/Kv";
@@ -1718,6 +1802,36 @@ const tryKvPromise = (binding, operation, evaluate) => Effect.tryPromise({
1718
1802
  * Creates a typed KV service tag plus Effect helpers.
1719
1803
  */
1720
1804
  const make$3 = (id, definition) => Service$1()(id, definition);
1805
+ const makeDefinition$1 = (id, definition) => {
1806
+ const kvDefinition = {
1807
+ id,
1808
+ key: definition.key,
1809
+ value: definition.value
1810
+ };
1811
+ return Object.assign(kvDefinition, {
1812
+ Binding: () => (bindingId, binding) => Service$1()(bindingId, {
1813
+ ...binding,
1814
+ key: definition.key,
1815
+ value: definition.value
1816
+ }),
1817
+ binding: (bindingId, binding) => Service$1()(bindingId, {
1818
+ ...binding,
1819
+ key: definition.key,
1820
+ value: definition.value
1821
+ })
1822
+ });
1823
+ };
1824
+ const Tag$2 = () => (id, definition) => {
1825
+ const kvDefinition = makeDefinition$1(id, definition);
1826
+ class KvDefinitionClass {
1827
+ static id = kvDefinition.id;
1828
+ static key = kvDefinition.key;
1829
+ static value = kvDefinition.value;
1830
+ static Binding = kvDefinition.Binding;
1831
+ static binding = kvDefinition.binding;
1832
+ }
1833
+ return KvDefinitionClass;
1834
+ };
1721
1835
  /**
1722
1836
  * Builds a KV service around a Cloudflare KV namespace binding.
1723
1837
  *
@@ -1739,7 +1853,7 @@ const make$3 = (id, definition) => Service$1()(id, definition);
1739
1853
  * ```
1740
1854
  */
1741
1855
  const Service$1 = () => (id, definition) => {
1742
- const tag = Service$5()(id, definition.binding, (value) => {
1856
+ const tag = Service$6()(id, definition.binding, (value) => {
1743
1857
  if (typeof value !== "object" || value === null) return false;
1744
1858
  const resource = value;
1745
1859
  return typeof resource.get === "function" && typeof resource.put === "function" && typeof resource.delete === "function" && typeof resource.getWithMetadata === "function" && typeof resource.list === "function";
@@ -1841,7 +1955,7 @@ const isWorkflow = (value) => {
1841
1955
  };
1842
1956
  const make$2 = (id, definition) => Service()(id, definition);
1843
1957
  const Service = () => (id, definition) => {
1844
- const tag = Service$5()(id, definition.binding, (value) => isWorkflow(value));
1958
+ const tag = Service$6()(id, definition.binding, (value) => isWorkflow(value));
1845
1959
  const encodePayload = Schema.encodeEffect(definition.payload);
1846
1960
  const decodeResult = Schema.decodeUnknownEffect(definition.result);
1847
1961
  const wrapInstance = (raw) => {
@@ -2025,4 +2139,4 @@ const waitForEvent = (name, options) => Effect.flatMap(WorkflowStep$1.asEffect()
2025
2139
  const Tag = Tag$1;
2026
2140
  const implement = implement$1;
2027
2141
  //#endregion
2028
- export { Binding_exports as Binding, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, ServiceBinding_exports as ServiceBinding, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
2142
+ export { Binding_exports as Binding, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, ServiceBinding_exports as ServiceBinding, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@cloudflare/vitest-pool-workers": "^0.15.0",
38
38
  "@cloudflare/workers-types": "^4.20260421.1",
39
+ "@effect/sql-d1": "catalog:",
39
40
  "@effect/vitest": "catalog:",
40
41
  "effect": "catalog:",
41
42
  "typescript": "^6.0.2",
@@ -43,6 +44,7 @@
43
44
  "vitest": "catalog:"
44
45
  },
45
46
  "peerDependencies": {
47
+ "@effect/sql-d1": "^4.0.0-beta.65",
46
48
  "effect": "^4.0.0-beta.65"
47
49
  }
48
50
  }
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
+ };
package/src/Kv.ts CHANGED
@@ -52,6 +52,26 @@ export interface KvDefinition<Key, Value, EncodedValue> {
52
52
  readonly value: S.Codec<Value, EncodedValue>;
53
53
  }
54
54
 
55
+ /**
56
+ * Reusable typed KV resource definition without a concrete Cloudflare binding name.
57
+ */
58
+ export interface Definition<
59
+ Id extends string = string,
60
+ Key = unknown,
61
+ Value = unknown,
62
+ EncodedValue = unknown,
63
+ > {
64
+ readonly id: Id;
65
+ /** Codec used to encode/decode keys. */
66
+ readonly key: S.Codec<Key, string>;
67
+ /** Codec used to encode/decode values. */
68
+ readonly value: S.Codec<Value, EncodedValue>;
69
+ }
70
+
71
+ export namespace Definition {
72
+ export type Any = Definition<string, any, any, any>;
73
+ }
74
+
55
75
  declare const KvServiceTypeId: unique symbol;
56
76
 
57
77
  /** Nominal service marker for KV services created with {@link make}. */
@@ -64,6 +84,16 @@ export interface KvService<Id extends string, Key, Value, EncodedValue> {
64
84
  };
65
85
  }
66
86
 
87
+ declare const BindingServiceTypeId: unique symbol;
88
+
89
+ /** Nominal service marker for KV bindings created from a reusable {@link Definition}. */
90
+ export interface BindingService<Id extends string, Self extends Definition.Any> {
91
+ readonly [BindingServiceTypeId]: {
92
+ readonly id: Id;
93
+ readonly definition: Self;
94
+ };
95
+ }
96
+
67
97
  const maybeString = (value: string | null | undefined): Option.Option<string> =>
68
98
  value == null ? Option.none() : Option.some(value);
69
99
 
@@ -92,6 +122,66 @@ export const make = <Id extends string, Key, Value, EncodedValue>(
92
122
  ) =>
93
123
  Service<KvService<Id, Key, Value, EncodedValue>>()<Id, Key, Value, EncodedValue>(id, definition);
94
124
 
125
+ const makeDefinition = <Id extends string, Key, Value, EncodedValue>(
126
+ id: Id,
127
+ definition: { readonly key: S.Codec<Key, string>; readonly value: S.Codec<Value, EncodedValue> },
128
+ ) => {
129
+ type SelfDefinition = Definition<Id, Key, Value, EncodedValue>;
130
+ const kvDefinition: SelfDefinition = {
131
+ id,
132
+ key: definition.key,
133
+ value: definition.value,
134
+ };
135
+
136
+ return Object.assign(kvDefinition, {
137
+ Binding:
138
+ <Self>() =>
139
+ <BindingId extends string>(
140
+ bindingId: BindingId,
141
+ binding: Omit<KvDefinition<Key, Value, EncodedValue>, "key" | "value">,
142
+ ) =>
143
+ Service<Self>()<BindingId, Key, Value, EncodedValue>(bindingId, {
144
+ ...binding,
145
+ key: definition.key,
146
+ value: definition.value,
147
+ }),
148
+ binding: <BindingId extends string>(
149
+ bindingId: BindingId,
150
+ binding: Omit<KvDefinition<Key, Value, EncodedValue>, "key" | "value">,
151
+ ) =>
152
+ Service<BindingService<BindingId, SelfDefinition>>()<BindingId, Key, Value, EncodedValue>(
153
+ bindingId,
154
+ {
155
+ ...binding,
156
+ key: definition.key,
157
+ value: definition.value,
158
+ },
159
+ ),
160
+ });
161
+ };
162
+
163
+ export const Tag =
164
+ <_Self>() =>
165
+ <Id extends string, Key, Value, EncodedValue>(
166
+ id: Id,
167
+ definition: {
168
+ readonly key: S.Codec<Key, string>;
169
+ readonly value: S.Codec<Value, EncodedValue>;
170
+ },
171
+ ) => {
172
+ const kvDefinition = makeDefinition(id, definition);
173
+
174
+ abstract class KvDefinitionClass {
175
+ static readonly id = kvDefinition.id;
176
+ static readonly key = kvDefinition.key;
177
+ static readonly value = kvDefinition.value;
178
+ static readonly Binding = kvDefinition.Binding;
179
+ static readonly binding = kvDefinition.binding;
180
+ }
181
+
182
+ return KvDefinitionClass as (abstract new () => object) & typeof kvDefinition;
183
+ };
184
+
95
185
  /**
96
186
  * Builds a KV service around a Cloudflare KV namespace binding.
97
187
  *
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as Binding from "./Binding";
2
+ export * as D1 from "./D1";
2
3
  export * as DurableObject from "./DurableObject";
3
4
  export * as DurableObjectAlarm from "./DurableObjectAlarm";
4
5
  export * as DurableObjectDefinition from "./DurableObjectDefinition";