effect-cf 0.5.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.
@@ -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
  };
package/src/Kv.ts CHANGED
@@ -84,6 +84,28 @@ export interface KvService<Id extends string, Key, Value, EncodedValue> {
84
84
  };
85
85
  }
86
86
 
87
+ export interface KvClient<Key, Value, EncodedValue> {
88
+ readonly put: (
89
+ key: Key,
90
+ value: Value,
91
+ options?: KvPutOptions,
92
+ ) => Effect.Effect<void, KvOperationError | S.SchemaError>;
93
+ readonly get: (key: Key) => Effect.Effect<Option.Option<Value>, KvOperationError | S.SchemaError>;
94
+ readonly getWithMetadata: <Metadata>(
95
+ key: Key,
96
+ metadataSchema: S.Codec<Metadata, unknown>,
97
+ ) => Effect.Effect<
98
+ Option.Option<KvWithMetadata<Value, Metadata>>,
99
+ KvOperationError | S.SchemaError
100
+ >;
101
+ readonly list: <Metadata = unknown>(
102
+ options?: KvListOptions<Metadata>,
103
+ ) => Effect.Effect<KvListResult<Key, Metadata>, KvOperationError | S.SchemaError>;
104
+ readonly remove: (key: Key) => Effect.Effect<void, KvOperationError | S.SchemaError>;
105
+ readonly unsafeRaw: Effect.Effect<KVNamespace>;
106
+ readonly definition: KvDefinition<Key, Value, EncodedValue>;
107
+ }
108
+
87
109
  declare const BindingServiceTypeId: unique symbol;
88
110
 
89
111
  /** Nominal service marker for KV bindings created from a reusable {@link Definition}. */
@@ -208,46 +230,123 @@ export const Service =
208
230
  id: Id,
209
231
  definition: KvDefinition<Key, Value, EncodedValue>,
210
232
  ) => {
211
- const tag = Binding.Service<Self>()(id, definition.binding, (value): value is KVNamespace => {
212
- if (typeof value !== "object" || value === null) {
213
- return false;
214
- }
215
-
216
- const resource = value as Record<string, unknown>;
217
-
218
- return (
219
- typeof resource.get === "function" &&
220
- typeof resource.put === "function" &&
221
- typeof resource.delete === "function" &&
222
- typeof resource.getWithMetadata === "function" &&
223
- typeof resource.list === "function"
224
- );
225
- });
226
-
227
233
  const encodeKey = S.encodeEffect(definition.key);
228
234
  const decodeKey = S.decodeUnknownEffect(definition.key);
229
235
  const encodeValue = S.encodeEffect(S.fromJsonString(S.toCodecJson(definition.value)));
230
236
  const decodeValue = S.decodeUnknownEffect(S.fromJsonString(S.toCodecJson(definition.value)));
231
237
 
238
+ const makeClient = (kv: KVNamespace): KvClient<Key, Value, EncodedValue> => ({
239
+ definition,
240
+ put: Effect.fnUntraced(function* (key: Key, value: Value, options?: KvPutOptions) {
241
+ const keyEncoded = yield* encodeKey(key);
242
+ const valueEncoded = yield* encodeValue(value);
243
+ yield* tryKvPromise(definition.binding, "put", () =>
244
+ kv.put(keyEncoded, valueEncoded, options),
245
+ );
246
+ }),
247
+ get: Effect.fnUntraced(function* (key: Key) {
248
+ const keyEncoded = yield* encodeKey(key);
249
+ const valueEncoded = yield* tryKvPromise(definition.binding, "get", () =>
250
+ kv.get(keyEncoded),
251
+ );
252
+
253
+ if (valueEncoded === null) {
254
+ return Option.none();
255
+ }
256
+
257
+ return yield* decodeValue(valueEncoded).pipe(Effect.map(Option.some));
258
+ }),
259
+ getWithMetadata: Effect.fnUntraced(function* <Metadata>(
260
+ key: Key,
261
+ metadataSchema: S.Codec<Metadata, unknown>,
262
+ ) {
263
+ const keyEncoded = yield* encodeKey(key);
264
+ const result = yield* tryKvPromise(definition.binding, "getWithMetadata", () =>
265
+ kv.getWithMetadata<Metadata>(keyEncoded),
266
+ );
267
+
268
+ if (result.value === null) {
269
+ return Option.none();
270
+ }
271
+
272
+ const value = yield* decodeValue(result.value);
273
+ const metadata =
274
+ result.metadata === null
275
+ ? Option.none<Metadata>()
276
+ : Option.some(yield* S.decodeUnknownEffect(metadataSchema)(result.metadata));
277
+
278
+ return Option.some({
279
+ value,
280
+ metadata,
281
+ cacheStatus: maybeString(result.cacheStatus),
282
+ });
283
+ }),
284
+ list: Effect.fnUntraced(function* <Metadata = unknown>(options?: KvListOptions<Metadata>) {
285
+ const { metadataSchema, ...kvOptions } = options ?? {};
286
+ const result = yield* tryKvPromise(definition.binding, "list", () =>
287
+ kv.list<Metadata>(kvOptions),
288
+ );
289
+ const keys: Array<KvListKey<Key, Metadata>> = [];
290
+
291
+ for (const key of result.keys) {
292
+ const decodedName = yield* decodeKey(key.name);
293
+ const decodedMetadata =
294
+ key.metadata === undefined
295
+ ? Option.none<Metadata>()
296
+ : metadataSchema === undefined
297
+ ? Option.some(key.metadata as Metadata)
298
+ : Option.some(yield* S.decodeUnknownEffect(metadataSchema)(key.metadata));
299
+
300
+ keys.push({
301
+ name: decodedName,
302
+ expiration: maybeNumber(key.expiration),
303
+ metadata: decodedMetadata,
304
+ });
305
+ }
306
+
307
+ return {
308
+ keys,
309
+ listComplete: result.list_complete,
310
+ cursor: maybeString("cursor" in result ? result.cursor : undefined),
311
+ cacheStatus: maybeString(result.cacheStatus),
312
+ };
313
+ }),
314
+ remove: Effect.fnUntraced(function* (key: Key) {
315
+ const keyEncoded = yield* encodeKey(key);
316
+ yield* tryKvPromise(definition.binding, "delete", () => kv.delete(keyEncoded));
317
+ }),
318
+ unsafeRaw: Effect.succeed(kv),
319
+ });
320
+
321
+ const tag = Binding.Service<Self>()(
322
+ id,
323
+ definition.binding,
324
+ (value): value is KVNamespace => {
325
+ if (typeof value !== "object" || value === null) {
326
+ return false;
327
+ }
328
+
329
+ const resource = value as Record<string, unknown>;
330
+
331
+ return (
332
+ typeof resource.get === "function" &&
333
+ typeof resource.put === "function" &&
334
+ typeof resource.delete === "function" &&
335
+ typeof resource.getWithMetadata === "function" &&
336
+ typeof resource.list === "function"
337
+ );
338
+ },
339
+ makeClient,
340
+ );
341
+
232
342
  const put = Effect.fnUntraced(function* (key: Key, value: Value, options?: KvPutOptions) {
233
343
  const kv = yield* tag;
234
- const keyEncoded = yield* encodeKey(key);
235
- const valueEncoded = yield* encodeValue(value);
236
- yield* tryKvPromise(definition.binding, "put", () =>
237
- kv.put(keyEncoded, valueEncoded, options),
238
- );
344
+ yield* kv.put(key, value, options);
239
345
  });
240
346
 
241
347
  const get = Effect.fnUntraced(function* (key: Key) {
242
348
  const kv = yield* tag;
243
- const keyEncoded = yield* encodeKey(key);
244
- const valueEncoded = yield* tryKvPromise(definition.binding, "get", () => kv.get(keyEncoded));
245
-
246
- if (valueEncoded === null) {
247
- return Option.none();
248
- }
249
-
250
- return yield* decodeValue(valueEncoded).pipe(Effect.map(Option.some));
349
+ return yield* kv.get(key);
251
350
  });
252
351
 
253
352
  const getWithMetadata = Effect.fnUntraced(function* <Metadata>(
@@ -255,70 +354,24 @@ export const Service =
255
354
  metadataSchema: S.Codec<Metadata, unknown>,
256
355
  ) {
257
356
  const kv = yield* tag;
258
- const keyEncoded = yield* encodeKey(key);
259
- const result = yield* tryKvPromise(definition.binding, "getWithMetadata", () =>
260
- kv.getWithMetadata<Metadata>(keyEncoded),
261
- );
262
-
263
- if (result.value === null) {
264
- return Option.none();
265
- }
266
-
267
- const value = yield* decodeValue(result.value);
268
- const metadata =
269
- result.metadata === null
270
- ? Option.none<Metadata>()
271
- : Option.some(yield* S.decodeUnknownEffect(metadataSchema)(result.metadata));
272
-
273
- return Option.some({
274
- value,
275
- metadata,
276
- cacheStatus: maybeString(result.cacheStatus),
277
- });
357
+ return yield* kv.getWithMetadata(key, metadataSchema);
278
358
  });
279
359
 
280
360
  const list = Effect.fnUntraced(function* <Metadata = unknown>(
281
361
  options?: KvListOptions<Metadata>,
282
362
  ) {
283
363
  const kv = yield* tag;
284
- const { metadataSchema, ...kvOptions } = options ?? {};
285
- const result = yield* tryKvPromise(definition.binding, "list", () =>
286
- kv.list<Metadata>(kvOptions),
287
- );
288
- const keys: Array<KvListKey<Key, Metadata>> = [];
289
-
290
- for (const key of result.keys) {
291
- const decodedName = yield* decodeKey(key.name);
292
- const decodedMetadata =
293
- key.metadata === undefined
294
- ? Option.none<Metadata>()
295
- : metadataSchema === undefined
296
- ? Option.some(key.metadata as Metadata)
297
- : Option.some(yield* S.decodeUnknownEffect(metadataSchema)(key.metadata));
298
-
299
- keys.push({
300
- name: decodedName,
301
- expiration: maybeNumber(key.expiration),
302
- metadata: decodedMetadata,
303
- });
304
- }
305
-
306
- return {
307
- keys,
308
- listComplete: result.list_complete,
309
- cursor: maybeString("cursor" in result ? result.cursor : undefined),
310
- cacheStatus: maybeString(result.cacheStatus),
311
- };
364
+ return yield* kv.list(options);
312
365
  });
313
366
 
314
367
  const remove = Effect.fnUntraced(function* (key: Key) {
315
368
  const kv = yield* tag;
316
- const keyEncoded = yield* encodeKey(key);
317
- yield* tryKvPromise(definition.binding, "delete", () => kv.delete(keyEncoded));
369
+ yield* kv.remove(key);
318
370
  });
319
371
 
320
372
  const unsafeRaw = Effect.fnUntraced(function* () {
321
- return yield* tag;
373
+ const kv = yield* tag;
374
+ return yield* kv.unsafeRaw;
322
375
  });
323
376
 
324
377
  return Object.assign(tag, {