effect-cf 0.34.0 → 0.35.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.
@@ -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>) =>
@@ -124,7 +124,7 @@ export const decodeWireError = (cause: WireValue): WireValue => {
124
124
  }
125
125
  };
126
126
 
127
- export const reservedMethodNames = new Set([
127
+ const reservedMethodNameValues = [
128
128
  "constructor",
129
129
  "fetch",
130
130
  "connect",
@@ -137,21 +137,11 @@ export const reservedMethodNames = new Set([
137
137
  "dispose",
138
138
  "serialize",
139
139
  "deserialize",
140
- ]);
140
+ ] as const;
141
+
142
+ export const reservedMethodNames = new Set<string>(reservedMethodNameValues);
141
143
 
142
- export type ReservedMethodName =
143
- | "constructor"
144
- | "fetch"
145
- | "connect"
146
- | "alarm"
147
- | "webSocketMessage"
148
- | "webSocketClose"
149
- | "webSocketError"
150
- | "then"
151
- | "dup"
152
- | "dispose"
153
- | "serialize"
154
- | "deserialize";
144
+ export type ReservedMethodName = (typeof reservedMethodNameValues)[number];
155
145
 
156
146
  export type ServiceFreeSchema = S.Codec<any, any, never, never>;
157
147
 
@@ -145,7 +145,11 @@ export type ServiceBindingEffectClient<
145
145
  readonly scopedCall: <Method extends ServiceMethodKey<Api>>(
146
146
  method: Method,
147
147
  ...args: ServiceMethodArgs<Api, Method>
148
- ) => Effect.Effect<Awaited<ServiceMethodSuccess<Api, Method>>, unknown, Scope.Scope>;
148
+ ) => Effect.Effect<
149
+ Awaited<ServiceMethodSuccess<Api, Method>>,
150
+ ServiceBindingRpcError,
151
+ Scope.Scope
152
+ >;
149
153
  };
150
154
 
151
155
  export type ServiceBindingStaticClient<
@@ -168,7 +172,11 @@ export type ServiceBindingStaticClient<
168
172
  readonly scopedCall: <Method extends ServiceMethodKey<Api>>(
169
173
  method: Method,
170
174
  ...args: ServiceMethodArgs<Api, Method>
171
- ) => Effect.Effect<Awaited<ServiceMethodSuccess<Api, Method>>, unknown, Scope.Scope | R>;
175
+ ) => Effect.Effect<
176
+ Awaited<ServiceMethodSuccess<Api, Method>>,
177
+ ServiceBindingRpcError,
178
+ Scope.Scope | R
179
+ >;
172
180
  };
173
181
 
174
182
  type ServiceBindingValue = Schema.Schema.Type<typeof Schema.Unknown>;
@@ -195,7 +203,22 @@ export const makeClient = <
195
203
  init?: RequestInit,
196
204
  ) {
197
205
  return yield* Effect.tryPromise({
198
- try: () => service.fetch(input, init),
206
+ try: (signal) => {
207
+ const callerSignal =
208
+ init?.signal !== undefined
209
+ ? init.signal
210
+ : input instanceof Request
211
+ ? input.signal
212
+ : undefined;
213
+
214
+ return service.fetch(input, {
215
+ ...init,
216
+ signal:
217
+ callerSignal === null || callerSignal === undefined
218
+ ? signal
219
+ : AbortSignal.any([callerSignal, signal]),
220
+ });
221
+ },
199
222
  catch: (cause) => new ServiceBindingFetchError({ binding: definition.binding, cause }),
200
223
  });
201
224
  });
@@ -298,10 +321,19 @@ export const makeClient = <
298
321
  function* <Method extends ServiceMethodKey<Api>>(
299
322
  method: Method,
300
323
  ...args: ServiceMethodArgs<Api, Method>
301
- ): Effect.fn.Return<ServiceMethodSuccess<Api, Method>, unknown, Scope.Scope> {
324
+ ): Effect.fn.Return<ServiceMethodSuccess<Api, Method>, ServiceBindingRpcError, Scope.Scope> {
302
325
  const methodName = String(method);
303
326
  const result = yield* rpc(method, ...args);
304
- const value = yield* CloudflareRpc.scoped(result);
327
+ const value = yield* CloudflareRpc.scoped(result).pipe(
328
+ Effect.mapError(
329
+ (cause) =>
330
+ new ServiceBindingRpcError({
331
+ binding: definition.binding,
332
+ method: methodName,
333
+ cause,
334
+ }),
335
+ ),
336
+ );
305
337
 
306
338
  return yield* decodeSuccess<Method>(methodName, value);
307
339
  },
package/src/Vitest.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  } from "./DurableObjectState";
34
34
  import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
35
35
  import type { WorkflowInstanceStatusName } from "./WorkflowBinding";
36
+ import { runNativeCallback } from "./internal/NativeCallback";
36
37
 
37
38
  const currentEnv: WorkerEnv = env;
38
39
 
@@ -207,15 +208,20 @@ export const runInDurableObject = Effect.fn("effect-cf/vitest/runInDurableObject
207
208
  stub: globalThis.DurableObjectStub<Object>,
208
209
  use: (instance: Object, state: DurableObjectStateService) => Effect.Effect<A, E, R>,
209
210
  ): Effect.fn.Return<A, E, Exclude<R, DurableObjectState>> {
210
- const context = yield* Effect.context<Exclude<R, DurableObjectState>>();
211
- const exit = yield* Effect.promise(() =>
212
- runInDurableObjectPromise(stub, (instance, nativeState) => {
213
- const state = fromDurableObjectState(nativeState);
214
- const effect = Effect.provideService(use(instance, state), DurableObjectState, state);
215
-
216
- return Effect.runPromiseExitWith(context)(effect);
217
- }),
218
- );
211
+ const exit = yield* runNativeCallback<A, E, Exclude<R, DurableObjectState>, Exit.Exit<A, E>>(
212
+ (run) =>
213
+ runInDurableObjectPromise(stub, (instance, nativeState) => {
214
+ const state = fromDurableObjectState(nativeState);
215
+
216
+ return run(
217
+ Effect.provideService(
218
+ Effect.suspend(() => use(instance, state)),
219
+ DurableObjectState,
220
+ state,
221
+ ),
222
+ );
223
+ }),
224
+ ).pipe(Effect.orDie);
219
225
 
220
226
  return yield* exit;
221
227
  });
@@ -394,15 +400,12 @@ const runWorkflowModifier = Effect.fnUntraced(function* <A, E, R>(
394
400
  register: (use: (modifier: NativeWorkflowInstanceModifier) => Promise<void>) => Promise<any>,
395
401
  use: (modifier: WorkflowInstanceModifier) => Effect.Effect<A, E, R>,
396
402
  ): Effect.fn.Return<A, E, R> {
397
- const context = yield* Effect.context<R>();
398
- const exit = yield* Effect.promise(async () => {
403
+ const exit = yield* runNativeCallback<A, E, R, Exit.Exit<A, E>>(async (run) => {
399
404
  let callbackExit: Exit.Exit<A, E> | undefined;
400
405
 
401
406
  try {
402
407
  await register(async (modifier) => {
403
- callbackExit = await Effect.runPromiseExitWith(context)(
404
- use(wrapWorkflowModifier(modifier)),
405
- );
408
+ callbackExit = await run(Effect.suspend(() => use(wrapWorkflowModifier(modifier))));
406
409
 
407
410
  if (Exit.isFailure(callbackExit)) {
408
411
  throw callbackExit;
@@ -421,7 +424,7 @@ const runWorkflowModifier = Effect.fnUntraced(function* <A, E, R>(
421
424
  }
422
425
 
423
426
  return callbackExit;
424
- });
427
+ }).pipe(Effect.orDie);
425
428
 
426
429
  return yield* exit;
427
430
  });