effect-cf 0.34.0 → 0.36.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,11 @@
27
27
  "import": "./dist/HyperdrivePg.mjs",
28
28
  "default": "./dist/HyperdrivePg.mjs"
29
29
  },
30
+ "./mcp": {
31
+ "types": "./dist/Mcp.d.mts",
32
+ "import": "./dist/Mcp.mjs",
33
+ "default": "./dist/Mcp.mjs"
34
+ },
30
35
  "./computer-artifacts": {
31
36
  "types": "./dist/ComputerArtifacts.d.mts",
32
37
  "import": "./dist/ComputerArtifacts.mjs",
@@ -75,6 +80,8 @@
75
80
  "@effect/sql-pg": "catalog:",
76
81
  "@effect/sql-sqlite-do": "catalog:",
77
82
  "@effect/vitest": "catalog:",
83
+ "@modelcontextprotocol/client": "^2.0.0",
84
+ "@modelcontextprotocol/server": "^2.0.0",
78
85
  "@platformatic/vfs": "^0.4.0",
79
86
  "effect": "catalog:",
80
87
  "esbuild": "^0.28.1",
@@ -89,6 +96,7 @@
89
96
  "@effect/sql-d1": "^4.0.0-rc.110",
90
97
  "@effect/sql-pg": "^4.0.0-rc.110",
91
98
  "@effect/sql-sqlite-do": "^4.0.0-rc.110",
99
+ "@modelcontextprotocol/server": "^2.0.0",
92
100
  "effect": "^4.0.0-rc.110"
93
101
  },
94
102
  "peerDependenciesMeta": {
@@ -106,6 +114,9 @@
106
114
  },
107
115
  "@effect/sql-pg": {
108
116
  "optional": true
117
+ },
118
+ "@modelcontextprotocol/server": {
119
+ "optional": true
109
120
  }
110
121
  }
111
122
  }
@@ -572,7 +572,14 @@ const executeQueryRequest = (
572
572
  options?: AnalyticsEngineQueryOptions,
573
573
  ) =>
574
574
  Effect.gen(function* () {
575
- const response = yield* httpClient.execute(queryRequest(definition, sql, options)).pipe(
575
+ const request = yield* Effect.try({
576
+ try: () => queryRequest(definition, sql, options),
577
+ catch: (cause) =>
578
+ analyticsEngineQueryError(definition, "query", "Invalid Analytics Engine SQL API URL", {
579
+ cause,
580
+ }),
581
+ });
582
+ const response = yield* httpClient.execute(request).pipe(
576
583
  Effect.mapError((cause) =>
577
584
  analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", {
578
585
  cause,
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Worker fetch/RPC and Durable Object alarm/RPC handlers schedule a best-effort
3
- * flusher. It settles within two seconds; queues and other lifecycles must
4
- * flush explicitly. Failures are ignored so telemetry cannot fail user events.
2
+ * Worker fetch/queue/RPC and Durable Object alarm/RPC handlers schedule a
3
+ * best-effort flusher. It settles within two seconds; other lifecycles must
4
+ * flush explicitly. Scheduled flush failures cannot fail user events. Exporter
5
+ * layer finalizers run separately and follow their configured shutdown timeout.
5
6
  */
6
7
  import { ConfigProvider, Effect, Layer, Option } from "effect";
7
8
  import type * as Tracer from "effect/Tracer";
@@ -207,6 +208,8 @@ export const layerProtobuf = (
207
208
  *
208
209
  * Provide this layer at runtime scope for long-lived metrics aggregation, or at
209
210
  * handler scope when traces/logs should flush as the Cloudflare event finishes.
211
+ * A handler-scoped exporter can delay scope closure while its own finalizer
212
+ * flushes; the two-second background flush limit does not bound layer shutdown.
210
213
  */
211
214
  export const layerWorker = (options: WorkerLayerOptions = {}): Layer.Layer<OtlpExporter.Flusher> =>
212
215
  makeLayer(options, {
@@ -100,6 +100,17 @@ export class WorkspaceAssetsError extends Schema.TaggedError<WorkspaceAssetsErro
100
100
  },
101
101
  ) {}
102
102
 
103
+ /** Schema-serializable error while acquiring the Computer workspace client. */
104
+ export class WorkspaceAcquireError extends Schema.TaggedError<WorkspaceAcquireError>()(
105
+ "WorkspaceAcquireError",
106
+ {
107
+ operation: Schema.String,
108
+ message: Schema.String,
109
+ code: Schema.optionalKey(Schema.String),
110
+ cause: Schema.optionalKey(Schema.Defect({ includeStack: true })),
111
+ },
112
+ ) {}
113
+
103
114
  export type WorkspaceReadFileOptions = Omit<CloudflareReadFileOptions, "encoding">;
104
115
  export type ReadTextFileOptions = WorkspaceReadFileOptions;
105
116
  export type ListDirectoryOptions = CloudflareReaddirOptions;
@@ -469,6 +480,21 @@ const execError = (operation: string, cause: unknown) => {
469
480
  return WorkspaceExecError.make(properties);
470
481
  };
471
482
 
483
+ const acquireError = (operation: string, cause: unknown) => {
484
+ const properties: WorkspaceOperationErrorProperties = {
485
+ operation,
486
+ message: errorMessage(cause),
487
+ cause,
488
+ };
489
+ const code = errorCode(cause);
490
+
491
+ if (code !== undefined) {
492
+ properties.code = code;
493
+ }
494
+
495
+ return WorkspaceAcquireError.make(properties);
496
+ };
497
+
472
498
  const assetsError = (operation: string, path: string, cause: unknown) => {
473
499
  const properties: WorkspaceFsErrorProperties = {
474
500
  operation,
@@ -887,9 +913,6 @@ const wrapRun = <Encoding extends WorkspaceExecEncoding>(
887
913
  return { id: handle.id, backend: handle.backend, events, result, kill };
888
914
  };
889
915
 
890
- const releaseRun = (handle: CloudflareWorkspaceRuntimeExecHandle<"utf8" | undefined>) =>
891
- Effect.sync(() => handle[Symbol.dispose]());
892
-
893
916
  const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntime => {
894
917
  const execImplementation = Effect.fn("ComputerWorkspace.runtime.exec", {
895
918
  attributes: { family: "runtime", operation: "exec" },
@@ -905,7 +928,7 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
905
928
  : undefined;
906
929
 
907
930
  yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: options?.id });
908
- const handle = yield* Effect.acquireRelease(
931
+ const handle = yield* Effect.acquireDisposable(
909
932
  Effect.tryPromise({
910
933
  try: () => {
911
934
  if (Predicate.isString(source)) {
@@ -926,7 +949,6 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
926
949
  },
927
950
  catch: (cause) => execError("exec", cause),
928
951
  }),
929
- releaseRun,
930
952
  );
931
953
 
932
954
  // SAFETY: runtimeExecOptions carries Encoding to Cloudflare's corresponding handle overload.
@@ -945,12 +967,11 @@ const makeRuntime = (client: CloudflareWorkspaceClient): ComputerWorkspaceRuntim
945
967
  options?: WorkspaceGetExecOptions<Encoding>,
946
968
  ) {
947
969
  yield* Effect.annotateCurrentSpan({ backend: options?.backend, runId: id });
948
- const handle = yield* Effect.acquireRelease(
970
+ const handle = yield* Effect.acquireDisposable(
949
971
  Effect.tryPromise({
950
972
  try: () => client.runtime.getExec(id, runtimeGetOptions(options)),
951
973
  catch: (cause) => execError("getExec", cause),
952
974
  }),
953
- releaseRun,
954
975
  );
955
976
 
956
977
  // SAFETY: runtimeGetOptions carries Encoding to Cloudflare's corresponding handle overload.
@@ -1169,25 +1190,31 @@ export class ComputerWorkspace extends Context.Service<
1169
1190
  * Acquires the single workspace registered by `withComputerWorkspace` and
1170
1191
  * releases its RPC client when the layer scope closes.
1171
1192
  */
1172
- static readonly layer: Layer.Layer<ComputerWorkspace, never, DurableObjectState> = Layer.effect(
1173
- ComputerWorkspace,
1174
- Effect.gen(function* () {
1175
- const state = yield* DurableObjectState;
1176
- const host = HostRegistry.lookup(state.raw);
1177
-
1178
- if (host === undefined) {
1179
- return yield* Effect.die(
1180
- "ComputerWorkspace.layer requires a Durable Object class built with withComputerWorkspace",
1193
+ static readonly layer: Layer.Layer<ComputerWorkspace, WorkspaceAcquireError, DurableObjectState> =
1194
+ Layer.effect(
1195
+ ComputerWorkspace,
1196
+ Effect.gen(function* () {
1197
+ const state = yield* DurableObjectState;
1198
+ const host = HostRegistry.lookup(state.raw);
1199
+
1200
+ if (host === undefined) {
1201
+ return yield* Effect.die(
1202
+ "ComputerWorkspace.layer requires a Durable Object class built with withComputerWorkspace",
1203
+ );
1204
+ }
1205
+
1206
+ const computer = yield* Effect.tryPromise({
1207
+ try: () => import("@cloudflare/computer"),
1208
+ catch: (cause) => acquireError("import", cause),
1209
+ });
1210
+ const client = yield* Effect.acquireDisposable(
1211
+ Effect.tryPromise({
1212
+ try: () => computer.getWorkspace(host),
1213
+ catch: (cause) => acquireError("getWorkspace", cause),
1214
+ }),
1181
1215
  );
1182
- }
1183
-
1184
- const computer = yield* Effect.promise(() => import("@cloudflare/computer"));
1185
- const client = yield* Effect.acquireRelease(
1186
- Effect.promise(() => computer.getWorkspace(host)),
1187
- (workspace) => Effect.sync(() => workspace[Symbol.dispose]()),
1188
- );
1189
1216
 
1190
- return ComputerWorkspace.of(fromWorkspaceClient(client));
1191
- }),
1192
- );
1217
+ return ComputerWorkspace.of(fromWorkspaceClient(client));
1218
+ }),
1219
+ );
1193
1220
  }
@@ -5,20 +5,13 @@ import { NativeRequest } from "./Worker";
5
5
  import { WorkerEnvironment, type WorkerEnv } from "./Environment";
6
6
  import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
7
7
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
8
+ import * as RpcDefinition from "./RpcDefinition";
8
9
  import * as Entrypoint from "./internal/Entrypoint";
9
10
  import * as Runtime from "./internal/Runtime";
10
11
  import * as Telemetry from "./internal/Telemetry";
11
12
  import type { ReceiverOptions, RpcInvocationInfo } from "./RpcTracing";
12
13
 
13
- const reservedMethodNames = new Set<string>([
14
- "constructor",
15
- "dup",
16
- "fetch",
17
- "alarm",
18
- "webSocketMessage",
19
- "webSocketClose",
20
- "webSocketError",
21
- ]);
14
+ const reservedMethodNames: ReadonlySet<string> = RpcDefinition.reservedMethodNames;
22
15
 
23
16
  type RuntimeContext<ROut> = DurableObjectState | WorkerEnvironment | ROut;
24
17
 
@@ -167,6 +160,12 @@ export type DurableObjectClass<Rpc extends DurableObjectRpc<ROut>, ROut> = new (
167
160
  ): Promise<A>;
168
161
  };
169
162
 
163
+ /**
164
+ * Builds a Durable Object whose base `layer` lives for the in-memory instance.
165
+ * Cloudflare does not expose an eviction or shutdown hook, so finalizers in
166
+ * that layer are not guaranteed to run. Put resources that require timely
167
+ * release in `eventLayer`, whose scope closes after each handled event.
168
+ */
170
169
  export const make = <
171
170
  ROut,
172
171
  LayerError,
@@ -250,7 +250,7 @@ export type DurableObjectNamespaceEffectClient<
250
250
  stub: DurableObjectStubClient<Api>,
251
251
  method: Method,
252
252
  ...args: StubMethodArgs<Api, Method>
253
- ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope>;
253
+ ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, DurableObjectRpcError, Scope.Scope>;
254
254
  /**
255
255
  * Exposes the underlying native Durable Object namespace binding.
256
256
  *
@@ -300,7 +300,11 @@ export type DurableObjectNamespaceStaticClient<
300
300
  stub: DurableObjectStubClient<Api>,
301
301
  method: Method,
302
302
  ...args: StubMethodArgs<Api, Method>
303
- ) => Effect.Effect<Awaited<StubMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
303
+ ) => Effect.Effect<
304
+ Awaited<StubMethodSuccess<Api, Method>>,
305
+ DurableObjectRpcError,
306
+ Scope.Scope | R
307
+ >;
304
308
  readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
305
309
  };
306
310
 
@@ -351,7 +355,22 @@ export const makeClient = <
351
355
 
352
356
  const fetch = (stub: StubClient, input: RequestInfo | URL, init?: RequestInit) =>
353
357
  Effect.tryPromise({
354
- try: () => stub.fetch(input, init),
358
+ try: (signal) => {
359
+ const callerSignal =
360
+ init?.signal !== undefined
361
+ ? init.signal
362
+ : input instanceof Request
363
+ ? input.signal
364
+ : undefined;
365
+
366
+ return stub.fetch(input, {
367
+ ...init,
368
+ signal:
369
+ callerSignal === null || callerSignal === undefined
370
+ ? signal
371
+ : AbortSignal.any([callerSignal, signal]),
372
+ });
373
+ },
355
374
  catch: (cause) => new DurableObjectFetchError({ binding: definition.binding, cause }),
356
375
  });
357
376
 
@@ -456,10 +475,19 @@ export const makeClient = <
456
475
  stub: StubClient,
457
476
  method: Method,
458
477
  ...args: StubMethodArgs<Api, Method>
459
- ): Effect.fn.Return<StubMethodSuccess<Api, Method>, unknown, Scope.Scope> {
478
+ ): Effect.fn.Return<StubMethodSuccess<Api, Method>, DurableObjectRpcError, Scope.Scope> {
460
479
  const methodName = String(method);
461
480
  const result = yield* rpc(stub, method, ...args);
462
- const value = yield* CloudflareRpc.scoped(result);
481
+ const value = yield* CloudflareRpc.scoped(result).pipe(
482
+ Effect.mapError(
483
+ (cause) =>
484
+ new DurableObjectRpcError({
485
+ binding: definition.binding,
486
+ method: methodName,
487
+ cause,
488
+ }),
489
+ ),
490
+ );
463
491
 
464
492
  return yield* decodeSuccess<Method>(methodName, value);
465
493
  },
@@ -230,7 +230,6 @@ export const layer = (
230
230
  const disconnects = yield* Queue.make<number>();
231
231
  const connectionsBySocket = new Map<WebSocket, RpcConnection>();
232
232
  const connectionsById = new Map<number, RpcConnection>();
233
- const clientIds = new Set<number>();
234
233
  const resetSockets = new WeakSet<WebSocket>();
235
234
  const declarationsById = new Map<string, AnyResumableStreamDeclaration>();
236
235
  const declarationsByTag = new Map<string, AnyResumableStreamDeclaration>();
@@ -268,21 +267,6 @@ export const layer = (
268
267
  }
269
268
 
270
269
  const reserveClientId = (socket: DurableWebSocket): AttachmentMetadata => {
271
- const attachment = readAttachment(socket.raw, attachmentKey);
272
-
273
- if (attachment._tag === "Invalid") {
274
- throw new Error("Invalid Durable Object RPC websocket attachment metadata");
275
- }
276
-
277
- if (attachment._tag === "Valid") {
278
- const metadata = attachment.metadata;
279
-
280
- nextClientId = Math.max(nextClientId, metadata.clientId + 1);
281
- writeAttachment(socket.raw, attachmentKey, metadata);
282
-
283
- return metadata;
284
- }
285
-
286
270
  const id = nextClientId++;
287
271
  const created: LegacyAttachmentMetadata = {
288
272
  version: legacyAttachmentVersion,
@@ -377,7 +361,6 @@ export const layer = (
377
361
 
378
362
  connectionsBySocket.set(socket.raw, connection);
379
363
  connectionsById.set(connection.id, connection);
380
- clientIds.add(connection.id);
381
364
  nextClientId = Math.max(nextClientId, connection.id + 1);
382
365
 
383
366
  return connection;
@@ -434,7 +417,6 @@ export const layer = (
434
417
 
435
418
  connectionsBySocket.delete(socket.raw);
436
419
  connectionsById.delete(connection.id);
437
- clientIds.delete(connection.id);
438
420
  activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
439
421
  connection.requestIds.clear();
440
422
  connection.nonResumableRequestIds.clear();
@@ -847,7 +829,7 @@ export const layer = (
847
829
 
848
830
  connection?.socket.raw.close();
849
831
  }),
850
- clientIds: Effect.sync(() => clientIds),
832
+ clientIds: Effect.sync(() => new Set(connectionsById.keys())),
851
833
  initialMessage: Effect.succeedNone,
852
834
  supportsAck: true,
853
835
  supportsTransferables: false,
@@ -1141,17 +1123,10 @@ const writeAttachment = (socket: WebSocket, key: string, metadata: AttachmentMet
1141
1123
  }
1142
1124
 
1143
1125
  const attachment = Predicate.isObject(current) ? current : {};
1144
- const currentMetadata =
1145
- Predicate.hasProperty(attachment, key) && Predicate.isObject(attachment[key])
1146
- ? attachment[key]
1147
- : {};
1148
1126
 
1149
1127
  socket.serializeAttachment({
1150
1128
  ...attachment,
1151
- [key]: {
1152
- ...currentMetadata,
1153
- ...metadata,
1154
- },
1129
+ [key]: metadata,
1155
1130
  });
1156
1131
  };
1157
1132
 
@@ -3,6 +3,7 @@ import { Cause, Context, Effect, Exit } from "effect";
3
3
  import { fromDurableObjectStorage, type DurableObjectStorage } from "./DurableObjectStorage";
4
4
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
5
5
  import type { WorkerContextWaitUntilOptions } from "./Worker";
6
+ import { runNativeCallback } from "./internal/NativeCallback";
6
7
  import { makeWaitUntilScheduler } from "./internal/WorkerContext";
7
8
 
8
9
  /**
@@ -87,24 +88,21 @@ export const fromDurableObjectState = (
87
88
  storage: fromDurableObjectStorage(state.storage),
88
89
  waitUntil,
89
90
  blockConcurrencyWhile: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
90
- Effect.context<R>().pipe(
91
- Effect.flatMap((context) =>
92
- Effect.promise(() =>
93
- state.blockConcurrencyWhile(() =>
94
- runPromiseExitPreservingTypedFailures(context, effect),
95
- ),
96
- ),
97
- ),
98
- Effect.flatten,
99
- ),
91
+ runNativeCallback<A, E, R, Exit.Exit<A, E>>((run) =>
92
+ state.blockConcurrencyWhile(() => runExitPreservingTypedFailures(run, effect)),
93
+ ).pipe(Effect.orDie, Effect.flatten),
100
94
  blockConcurrencyWhileOrReset: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
101
- Effect.context<R>().pipe(
102
- Effect.flatMap((context) =>
103
- Effect.promise(() =>
104
- state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect)),
105
- ),
106
- ),
107
- ),
95
+ runNativeCallback<A, E, R, A>((run) =>
96
+ state.blockConcurrencyWhile(async () => {
97
+ const exit = await run(effect);
98
+
99
+ if (Exit.isFailure(exit)) {
100
+ throw Cause.squash(exit.cause);
101
+ }
102
+
103
+ return exit.value;
104
+ }),
105
+ ).pipe(Effect.orDie),
108
106
  acceptWebSocket: (ws: DurableWebSocket, tags?: Array<string>) =>
109
107
  Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
110
108
  getWebSockets: (tag?: string) =>
@@ -125,11 +123,11 @@ export const fromDurableObjectState = (
125
123
  };
126
124
  };
127
125
 
128
- const runPromiseExitPreservingTypedFailures = async <A, E, R>(
129
- context: Context.Context<R>,
126
+ const runExitPreservingTypedFailures = async <A, E, R>(
127
+ run: (effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>,
130
128
  effect: Effect.Effect<A, E, R>,
131
129
  ): Promise<Exit.Exit<A, E>> => {
132
- const exit = await Effect.runPromiseExitWith(context)(effect);
130
+ const exit = await run(effect);
133
131
 
134
132
  if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) {
135
133
  throw Cause.squash(exit.cause);
@@ -1,6 +1,19 @@
1
- import { Clock, Data, Duration, Effect, Exit, Option, Predicate, Schema as S } from "effect";
1
+ import {
2
+ Cause,
3
+ Clock,
4
+ Data,
5
+ Duration,
6
+ Effect,
7
+ Exit,
8
+ Fiber,
9
+ Option,
10
+ Predicate,
11
+ Result,
12
+ Schema as S,
13
+ } from "effect";
2
14
 
3
15
  import * as ErrorMessage from "./internal/ErrorMessage";
16
+ import { runNativeCallback } from "./internal/NativeCallback";
4
17
 
5
18
  export type SqlStorageValue = globalThis.SqlStorageValue;
6
19
 
@@ -331,6 +344,7 @@ export const fromDurableObjectStorage = (
331
344
  // failed Exit is thrown to abort it. Keep a typed reference to that
332
345
  // Exit rather than recovering its type from the untyped catch binding.
333
346
  let aborted: Exit.Exit<A, E> | undefined;
347
+ let escapedFiber: Fiber.Fiber<unknown, unknown> | undefined;
334
348
 
335
349
  try {
336
350
  return Effect.succeed(
@@ -340,55 +354,59 @@ export const fromDurableObjectStorage = (
340
354
  if (Exit.isSuccess(exit)) {
341
355
  return exit.value;
342
356
  }
357
+ const defect = Cause.findDefect(exit.cause);
358
+
359
+ if (Result.isSuccess(defect) && Cause.isAsyncFiberError(defect.success)) {
360
+ escapedFiber = defect.success.fiber;
361
+ escapedFiber.interruptUnsafe();
362
+ }
343
363
  aborted = exit;
344
364
 
345
365
  throw exit;
346
366
  }),
347
367
  );
348
368
  } catch (cause) {
349
- if (aborted !== undefined && cause === aborted && Exit.isFailure(aborted)) {
350
- return Effect.failCause(aborted.cause);
369
+ const failure: Effect.Effect<never, E | StorageOperationError> =
370
+ aborted !== undefined && cause === aborted && Exit.isFailure(aborted)
371
+ ? Effect.failCause(aborted.cause)
372
+ : Effect.fail(storageError("transactionSync", cause));
373
+ const fiber = escapedFiber;
374
+
375
+ if (fiber !== undefined) {
376
+ return Fiber.interrupt(fiber).pipe(Effect.andThen(failure));
351
377
  }
352
378
 
353
- return Effect.fail(storageError("transactionSync", cause));
379
+ return failure;
354
380
  }
355
381
  }),
356
382
  ),
357
383
  ),
358
384
  transaction: <A, E, R>(closure: (txn: DurableObjectTransaction) => Effect.Effect<A, E, R>) =>
359
- Effect.context<R>().pipe(
360
- Effect.flatMap((context) =>
361
- Effect.callback<A, E | StorageOperationError>((resume) => {
362
- // See `transactionSync`: the thrown Exit aborts Cloudflare's
363
- // transaction, and holding it here keeps its error type.
364
- let aborted: Exit.Exit<A, E> | undefined;
385
+ Effect.suspend(() => {
386
+ // See `transactionSync`: the thrown Exit aborts Cloudflare's
387
+ // transaction, and holding it here keeps its error type.
388
+ let aborted: Exit.Exit<A, E> | undefined;
365
389
 
366
- void storage
367
- .transaction(async (txn) => {
368
- const exit = await Effect.runPromiseExitWith(context)(
369
- closure(fromDurableObjectTransaction(txn)),
370
- );
371
-
372
- if (Exit.isSuccess(exit)) {
373
- return exit.value;
374
- }
375
- aborted = exit;
376
-
377
- throw exit;
378
- })
379
- .then(
380
- (value) => resume(Effect.succeed(value)),
381
- (cause) => {
382
- if (aborted !== undefined && cause === aborted && Exit.isFailure(aborted)) {
383
- resume(Effect.failCause(aborted.cause));
384
- } else {
385
- resume(Effect.fail(storageError("transaction", cause)));
386
- }
387
- },
388
- );
390
+ return runNativeCallback<A, E, R, A>((run) =>
391
+ storage.transaction(async (txn) => {
392
+ const exit = await run(Effect.suspend(() => closure(fromDurableObjectTransaction(txn))));
393
+
394
+ if (Exit.isSuccess(exit)) {
395
+ return exit.value;
396
+ }
397
+ aborted = exit;
398
+
399
+ throw exit;
389
400
  }),
390
- ),
391
- ),
401
+ ).pipe(
402
+ Effect.catch(
403
+ (cause): Effect.Effect<never, E | StorageOperationError> =>
404
+ aborted !== undefined && cause === aborted && Exit.isFailure(aborted)
405
+ ? Effect.failCause(aborted.cause)
406
+ : Effect.fail(storageError("transaction", cause)),
407
+ ),
408
+ );
409
+ }),
392
410
  sync: () => tryStoragePromise("sync", () => storage.sync()),
393
411
  getCurrentBookmark: () =>
394
412
  tryStoragePromise("getCurrentBookmark", () => storage.getCurrentBookmark()),
@@ -38,53 +38,37 @@ export interface DurableWebSocket<Attachment = unknown> {
38
38
  send(data: DurableWebSocketSendData): Effect.Effect<void, DurableWebSocketSendError>;
39
39
  close(code?: number, reason?: string): Effect.Effect<void, DurableWebSocketCloseError>;
40
40
  /** Serializes hibernation attachment metadata onto the socket. */
41
- serializeAttachment<A = Attachment>(
41
+ serializeAttachment<A extends Attachment = Attachment>(
42
42
  value: A,
43
43
  ): Effect.Effect<void, DurableWebSocketAttachmentError>;
44
44
  /** Deserializes hibernation attachment metadata from the socket. */
45
45
  readonly deserializeAttachment: Effect.Effect<unknown, DurableWebSocketAttachmentError>;
46
46
  }
47
47
 
48
- const wrappers = new WeakMap<WebSocket, DurableWebSocket<unknown>>();
49
-
50
48
  export const fromWebSocket = <Attachment = unknown>(
51
49
  raw: WebSocket,
52
- ): DurableWebSocket<Attachment> => {
53
- const existing = wrappers.get(raw);
54
-
55
- if (existing !== undefined) {
56
- // SAFETY: Attachment is phantom; a wrapper's runtime behavior is independent of that type.
57
- return existing as DurableWebSocket<Attachment>;
58
- }
59
-
60
- const socket: DurableWebSocket<unknown> = {
61
- raw,
62
- send: (data) =>
63
- Effect.try({
64
- try: () => raw.send(data),
65
- catch: (cause) => new DurableWebSocketSendError({ cause }),
66
- }),
67
- close: (code, reason) =>
68
- Effect.try({
69
- try: () => raw.close(code, reason),
70
- catch: (cause) => new DurableWebSocketCloseError({ cause }),
71
- }),
72
- serializeAttachment: (value) =>
73
- Effect.try({
74
- try: () => raw.serializeAttachment(value),
75
- catch: (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
76
- }),
77
- deserializeAttachment: Effect.try({
78
- try: () => raw.deserializeAttachment(),
79
- catch: (cause) => new DurableWebSocketAttachmentError({ operation: "deserialize", cause }),
50
+ ): DurableWebSocket<Attachment> => ({
51
+ raw,
52
+ send: (data) =>
53
+ Effect.try({
54
+ try: () => raw.send(data),
55
+ catch: (cause) => new DurableWebSocketSendError({ cause }),
80
56
  }),
81
- };
82
-
83
- wrappers.set(raw, socket);
84
-
85
- // SAFETY: Attachment is phantom; the newly created wrapper supports values through its generic serializer.
86
- return socket as DurableWebSocket<Attachment>;
87
- };
57
+ close: (code, reason) =>
58
+ Effect.try({
59
+ try: () => raw.close(code, reason),
60
+ catch: (cause) => new DurableWebSocketCloseError({ cause }),
61
+ }),
62
+ serializeAttachment: <A extends Attachment = Attachment>(value: A) =>
63
+ Effect.try({
64
+ try: () => raw.serializeAttachment(value),
65
+ catch: (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
66
+ }),
67
+ deserializeAttachment: Effect.try({
68
+ try: () => raw.deserializeAttachment(),
69
+ catch: (cause) => new DurableWebSocketAttachmentError({ operation: "deserialize", cause }),
70
+ }),
71
+ });
88
72
 
89
73
  export interface AcceptUpgradeOptions<Attachment = unknown> {
90
74
  readonly tags?: ReadonlyArray<string> | undefined;
@@ -165,15 +149,13 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
165
149
  S.Codec.Encoded<AttachmentSchema>
166
150
  > => {
167
151
  type Attachment = S.Schema.Type<AttachmentSchema>;
168
- type Encoded = S.Codec.Encoded<AttachmentSchema>;
169
152
 
170
153
  const serialize = (socket: DurableWebSocket<unknown>, value: Attachment) =>
171
154
  S.encodeEffect(schema)(value).pipe(
172
155
  Effect.mapError(
173
156
  (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
174
157
  ),
175
- // SAFETY: S.encodeEffect returns this codec's declared Encoded representation.
176
- Effect.flatMap((encoded) => socket.serializeAttachment(encoded as Encoded)),
158
+ Effect.flatMap((encoded) => socket.serializeAttachment(encoded)),
177
159
  );
178
160
 
179
161
  const deserialize = (socket: DurableWebSocket<unknown>) =>