effect-cf 0.46.1 → 0.48.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.48.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#192](https://github.com/danieljvdm/effect-cf/pull/192) [`761c4f4`](https://github.com/danieljvdm/effect-cf/commit/761c4f411e11ae0491f61c6fe7c7697faf68e7f0) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Report absent Artifacts commits as `ArtifactsOperationError` with `NOT_FOUND` instead of succeeding with `null`. Correct the raw `ArtifactsRepoBinding.readCommit` return type to include `null`; the Effect client continues to return a commit or a typed failure.
8
+
9
+ ## 0.47.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#190](https://github.com/danieljvdm/effect-cf/pull/190) [`a3ae008`](https://github.com/danieljvdm/effect-cf/commit/a3ae008af959d08eec649042edc9309982354a0d) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add `RpcWebSocketClient.layer` and the browser-safe `effect-cf/rpc-websocket-client` entrypoint. Provide a typed client service and its WebSocket connection for an application or session without requiring callers to wrap client construction in `Effect.scoped`. The layer closes the connection when its lifetime ends.
14
+
15
+ Persist the RPC serializer's content type in Durable Object WebSocket attachments and reset restored connections whose saved content type differs from the server's, preventing incompatible frames from reaching RPC handlers after a deployment. Existing attachments without serializer metadata remain supported.
16
+
17
+ Acknowledge WebSocket close events and ignore late messages on closed connections so client scope cleanup completes the close handshake.
18
+
3
19
  ## 0.46.1
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -10,6 +10,8 @@ The repository tests against workerd `1.20260825.1` and `@cloudflare/workers-typ
10
10
 
11
11
  See the [runtime performance guide](https://github.com/danieljvdm/effect-cf/blob/main/docs/runtime-performance.md) for measured alarm batching and telemetry configuration guidance.
12
12
 
13
+ For typed WebSocket RPC clients whose connection lifetime is owned by an Effect layer, see the [WebSocket RPC guide](https://github.com/danieljvdm/effect-cf/blob/main/docs/websocket-rpc.md).
14
+
13
15
  ## Worker
14
16
 
15
17
  ```ts
@@ -145,7 +145,7 @@ interface ArtifactsRepoBinding extends Partial<ArtifactsRepoInfo> {
145
145
  readonly revokeToken: (tokenOrId: string) => Promise<boolean>;
146
146
  readonly fork: (name: RepoName, options?: ArtifactsForkOptions) => Promise<ArtifactsCreateRepoResult>;
147
147
  readonly log: (options?: ArtifactsLogOptions) => Promise<ArtifactsLogResult>;
148
- readonly readCommit: (hash: string) => Promise<ArtifactsCommit>;
148
+ readonly readCommit: (hash: string) => Promise<ArtifactsCommit | null>;
149
149
  readonly readTree: (hash: string) => Promise<ArtifactsTree>;
150
150
  }
151
151
  interface ArtifactsBinding {
@@ -105,7 +105,17 @@ const wrapRepo = (binding, repo, info) => ({
105
105
  revokeToken: Effect.fn("Artifacts.revokeToken", spanOptions(binding, "revokeToken"))((tokenOrId) => tryArtifactsPromise(binding, "revokeToken", () => repo.revokeToken(tokenOrId))),
106
106
  fork: Effect.fn("Artifacts.fork", spanOptions(binding, "fork"))((name, options) => tryArtifactsPromise(binding, "fork", () => repo.fork(name, options))),
107
107
  log: Effect.fn("Artifacts.log", spanOptions(binding, "log"))((options) => tryArtifactsPromise(binding, "log", () => repo.log(options))),
108
- readCommit: Effect.fn("Artifacts.readCommit", spanOptions(binding, "readCommit"))((hash) => tryArtifactsPromise(binding, "readCommit", () => repo.readCommit(hash))),
108
+ readCommit: Effect.fn("Artifacts.readCommit", spanOptions(binding, "readCommit"))(function* (hash) {
109
+ const commit = yield* tryArtifactsPromise(binding, "readCommit", () => repo.readCommit(hash));
110
+ if (commit === null) return yield* new ArtifactsOperationError({
111
+ binding,
112
+ operation: "readCommit",
113
+ code: "NOT_FOUND",
114
+ numericCode: artifactsErrorNumericCodes.NOT_FOUND,
115
+ cause: /* @__PURE__ */ new Error("Commit not found")
116
+ });
117
+ return commit;
118
+ }),
109
119
  readTree: Effect.fn("Artifacts.readTree", spanOptions(binding, "readTree"))((hash) => tryArtifactsPromise(binding, "readTree", () => repo.readTree(hash))),
110
120
  raw: repo
111
121
  });
@@ -40,7 +40,8 @@ const ResumableAttachmentMetadataSchema = Schema.Struct({
40
40
  clientId: Schema.Finite,
41
41
  hasPendingRequests: Schema.Boolean,
42
42
  hasNonResumableRequests: Schema.Boolean,
43
- subscriptions: Schema.Array(PersistedSubscriptionSchema)
43
+ subscriptions: Schema.Array(PersistedSubscriptionSchema),
44
+ serialization: Schema.optionalKey(Schema.String)
44
45
  });
45
46
  const decodeResumableAttachment = Schema.decodeUnknownOption(ResumableAttachmentMetadataSchema);
46
47
  var DurableObjectRpcWebSocket = class extends Context.Service()("effect-cf/DurableObjectRpcWebSocket") {};
@@ -83,7 +84,8 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
83
84
  const created = {
84
85
  version: legacyAttachmentVersion,
85
86
  clientId: id,
86
- hasPendingRequests: false
87
+ hasPendingRequests: false,
88
+ serialization: serialization.contentType
87
89
  };
88
90
  writeAttachment(socket.raw, attachmentKey, created);
89
91
  return created;
@@ -143,7 +145,8 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
143
145
  writeAttachment(connection.socket.raw, attachmentKey, {
144
146
  version: legacyAttachmentVersion,
145
147
  clientId: connection.id,
146
- hasPendingRequests: connection.requestIds.size > 0
148
+ hasPendingRequests: connection.requestIds.size > 0,
149
+ serialization: serialization.contentType
147
150
  });
148
151
  return;
149
152
  }
@@ -153,7 +156,8 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
153
156
  clientId: connection.id,
154
157
  hasPendingRequests: connection.requestIds.size > 0,
155
158
  hasNonResumableRequests: connection.nonResumableRequestIds.size > 0,
156
- subscriptions
159
+ subscriptions,
160
+ serialization: serialization.contentType
157
161
  });
158
162
  },
159
163
  catch: (cause) => new DurableWebSocketAttachmentError({
@@ -249,6 +253,10 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
249
253
  continue;
250
254
  }
251
255
  const metadata = attachment.metadata;
256
+ if (metadata.serialization !== void 0 && metadata.serialization !== serialization.contentType) {
257
+ yield* resetSocket(socket);
258
+ continue;
259
+ }
252
260
  if (metadata.version === legacyAttachmentVersion) {
253
261
  if (metadata.hasPendingRequests) yield* resetSocket(socket);
254
262
  else if (connectionsById.has(metadata.clientId)) yield* resetSocket(socket);
@@ -476,7 +484,10 @@ const layer = (options = {}) => Layer.effectContext(Effect.gen(function* () {
476
484
  const connection = register(socket);
477
485
  return Predicate.isTagged(cause, "MaxBufferSizeExceeded") ? Effect.ignore(connection.socket.close(1009, String(cause))) : send(connection, RpcMessage.ResponseDefectEncoded(encodeDefect(cause)));
478
486
  })),
479
- close: unregister,
487
+ close: (socket) => Effect.gen(function* () {
488
+ resetSockets.add(socket.raw);
489
+ yield* socket.close(1e3).pipe(Effect.ignore);
490
+ }).pipe(Effect.ensuring(unregister(socket)), Effect.uninterruptible),
480
491
  error: resetSocket,
481
492
  checkpoint
482
493
  });
@@ -506,12 +517,14 @@ const readAttachment = (socket, key) => {
506
517
  } : { _tag: "Invalid" };
507
518
  }
508
519
  const hasPendingRequests = Predicate.hasProperty(metadata, "hasPendingRequests") ? metadata.hasPendingRequests : false;
509
- if ((!Predicate.hasProperty(metadata, "version") || metadata.version === legacyAttachmentVersion) && Predicate.hasProperty(metadata, "clientId") && Predicate.isNumber(metadata.clientId) && isClientId(metadata.clientId) && Predicate.isBoolean(hasPendingRequests)) return {
520
+ const serialization = Predicate.hasProperty(metadata, "serialization") ? metadata.serialization : void 0;
521
+ if ((!Predicate.hasProperty(metadata, "version") || metadata.version === legacyAttachmentVersion) && Predicate.hasProperty(metadata, "clientId") && Predicate.isNumber(metadata.clientId) && isClientId(metadata.clientId) && Predicate.isBoolean(hasPendingRequests) && (serialization === void 0 || Predicate.isString(serialization))) return {
510
522
  _tag: "Valid",
511
523
  metadata: {
512
524
  version: legacyAttachmentVersion,
513
525
  clientId: metadata.clientId,
514
- hasPendingRequests
526
+ hasPendingRequests,
527
+ serialization
515
528
  }
516
529
  };
517
530
  return { _tag: "Invalid" };
@@ -0,0 +1,30 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Layer from "effect/Layer";
3
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
4
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
+ import * as Rpc from "effect/unstable/rpc/Rpc";
7
+ import { RpcClientError } from "effect/unstable/rpc/RpcClientError";
8
+ import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
9
+ declare namespace RpcWebSocketClient_d_exports {
10
+ export { LayerOptions, layer };
11
+ }
12
+ interface LayerOptions {
13
+ readonly socket?: Parameters<typeof Socket.layerWebSocket>[1];
14
+ readonly protocol?: Parameters<typeof RpcClient.layerProtocolSocket>[0];
15
+ readonly client?: Omit<NonNullable<Parameters<typeof RpcClient.make>[1]>, "flatten">;
16
+ /** Must match the server's serialization. Defaults to Effect RPC JSON. */
17
+ readonly serialization?: RpcSerialization.RpcSerialization["Service"] | undefined;
18
+ }
19
+ /**
20
+ * Provides a typed RPC client and its WebSocket transport for the layer's lifetime.
21
+ * Callers yield the service without supplying a Scope. Provide this layer once
22
+ * around the application or session to share the connection across calls.
23
+ *
24
+ * Provide Socket.layerWebSocketConstructorGlobal in browsers, or a platform's
25
+ * WebSocketConstructor layer elsewhere. Streaming RPCs returned as queues still
26
+ * require a consumer scope; ordinary Stream operations manage their own scopes.
27
+ */
28
+ declare const layer: <I, Rpcs extends Rpc.Any>(service: Context.Key<I, RpcClient.RpcClient<Rpcs, RpcClientError>>, group: RpcGroup.RpcGroup<Rpcs>, url: Parameters<typeof Socket.layerWebSocket>[0], options?: LayerOptions) => Layer.Layer<I, never, Socket.WebSocketConstructor | Rpc.MiddlewareClient<Rpcs>>;
29
+ //#endregion
30
+ export { LayerOptions, RpcWebSocketClient_d_exports, layer };
@@ -0,0 +1,19 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import * as Layer from "effect/Layer";
3
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
4
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
+ //#region src/RpcWebSocketClient.ts
7
+ var RpcWebSocketClient_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
8
+ /**
9
+ * Provides a typed RPC client and its WebSocket transport for the layer's lifetime.
10
+ * Callers yield the service without supplying a Scope. Provide this layer once
11
+ * around the application or session to share the connection across calls.
12
+ *
13
+ * Provide Socket.layerWebSocketConstructorGlobal in browsers, or a platform's
14
+ * WebSocketConstructor layer elsewhere. Streaming RPCs returned as queues still
15
+ * require a consumer scope; ordinary Stream operations manage their own scopes.
16
+ */
17
+ const layer = (service, group, url, options = {}) => Layer.effect(service, RpcClient.make(group, options.client)).pipe(Layer.provide(RpcClient.layerProtocolSocket(options.protocol).pipe(Layer.provide(Socket.layerWebSocket(url, options.socket)), Layer.provide(Layer.succeed(RpcSerialization.RpcSerialization, options.serialization ?? RpcSerialization.json)))));
18
+ //#endregion
19
+ export { RpcWebSocketClient_exports, layer };
package/dist/index.d.mts CHANGED
@@ -14,6 +14,7 @@ import { ServiceBinding_d_exports } from "./ServiceBinding.mjs";
14
14
  import { Worker_d_exports } from "./Worker.mjs";
15
15
  import { DurableObjectState_d_exports } from "./DurableObjectState.mjs";
16
16
  import { Hyperdrive_d_exports } from "./Hyperdrive.mjs";
17
+ import { RpcWebSocketClient_d_exports } from "./RpcWebSocketClient.mjs";
17
18
  import { ContainerNamespace_d_exports } from "./ContainerNamespace.mjs";
18
19
  import { WorkflowBinding_d_exports } from "./WorkflowBinding.mjs";
19
20
  import { WorkersAi_d_exports } from "./WorkersAi.mjs";
@@ -39,4 +40,4 @@ import { Vectorize_d_exports } from "./Vectorize.mjs";
39
40
  import { WebTransport_d_exports } from "./WebTransport.mjs";
40
41
  import { WorkflowDefinition_d_exports } from "./WorkflowDefinition.mjs";
41
42
  import { Workflow_d_exports } from "./Workflow.mjs";
42
- export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTargets_d_exports as RpcTargets, RpcTracing_d_exports as RpcTracing, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
43
+ export { AiGateway_d_exports as AiGateway, AnalyticsEngine_d_exports as AnalyticsEngine, Artifacts_d_exports as Artifacts, Binding_d_exports as Binding, BrowserRendering_d_exports as BrowserRendering, Cache_d_exports as Cache, CloudflareOtlp_d_exports as CloudflareOtlp, CloudflareTracer_d_exports as CloudflareTracer, ContainerNamespace_d_exports as ContainerNamespace, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_d_exports as DurableObjectSqlite, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Email_d_exports as Email, Environment_d_exports as Environment, Hyperdrive_d_exports as Hyperdrive, Images_d_exports as Images, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, R2_d_exports as R2, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, RpcTargets_d_exports as RpcTargets, RpcTracing_d_exports as RpcTracing, RpcWebSocketClient_d_exports as RpcWebSocketClient, ServiceBinding_d_exports as ServiceBinding, Vectorize_d_exports as Vectorize, WebTransport_d_exports as WebTransport, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, WorkersAi_d_exports as WorkersAi, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
package/dist/index.mjs CHANGED
@@ -34,9 +34,10 @@ import { Hyperdrive_exports } from "./Hyperdrive.mjs";
34
34
  import { Images_exports } from "./Images.mjs";
35
35
  import { Kv_exports } from "./Kv.mjs";
36
36
  import { R2_exports } from "./R2.mjs";
37
+ import { RpcWebSocketClient_exports } from "./RpcWebSocketClient.mjs";
37
38
  import { Vectorize_exports } from "./Vectorize.mjs";
38
39
  import { WebTransport_exports } from "./WebTransport.mjs";
39
40
  import { WorkflowBinding_exports } from "./WorkflowBinding.mjs";
40
41
  import { WorkflowDefinition_exports } from "./WorkflowDefinition.mjs";
41
42
  import { Workflow_exports } from "./Workflow.mjs";
42
- export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTargets_exports as RpcTargets, RpcTracing_exports as RpcTracing, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
43
+ export { AiGateway_exports as AiGateway, AnalyticsEngine_exports as AnalyticsEngine, Artifacts_exports as Artifacts, Binding_exports as Binding, BrowserRendering_exports as BrowserRendering, Cache_exports as Cache, CloudflareOtlp_exports as CloudflareOtlp, CloudflareTracer_exports as CloudflareTracer, ContainerNamespace_exports as ContainerNamespace, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectSqlite_exports as DurableObjectSqlite, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Email_exports as Email, Environment_exports as Environment, Hyperdrive_exports as Hyperdrive, Images_exports as Images, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, R2_exports as R2, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, RpcTargets_exports as RpcTargets, RpcTracing_exports as RpcTracing, RpcWebSocketClient_exports as RpcWebSocketClient, ServiceBinding_exports as ServiceBinding, Vectorize_exports as Vectorize, WebTransport_exports as WebTransport, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, WorkersAi_exports as WorkersAi, 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.46.1",
3
+ "version": "0.48.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -57,6 +57,11 @@
57
57
  "import": "./dist/Vitest.mjs",
58
58
  "default": "./dist/Vitest.mjs"
59
59
  },
60
+ "./rpc-websocket-client": {
61
+ "types": "./dist/RpcWebSocketClient.d.mts",
62
+ "import": "./dist/RpcWebSocketClient.mjs",
63
+ "default": "./dist/RpcWebSocketClient.mjs"
64
+ },
60
65
  "./package.json": "./package.json"
61
66
  },
62
67
  "publishConfig": {
package/src/Artifacts.ts CHANGED
@@ -222,7 +222,7 @@ export interface ArtifactsRepoBinding extends Partial<ArtifactsRepoInfo> {
222
222
  options?: ArtifactsForkOptions,
223
223
  ) => Promise<ArtifactsCreateRepoResult>;
224
224
  readonly log: (options?: ArtifactsLogOptions) => Promise<ArtifactsLogResult>;
225
- readonly readCommit: (hash: string) => Promise<ArtifactsCommit>;
225
+ readonly readCommit: (hash: string) => Promise<ArtifactsCommit | null>;
226
226
  readonly readTree: (hash: string) => Promise<ArtifactsTree>;
227
227
  }
228
228
 
@@ -451,7 +451,20 @@ const wrapRepo = (
451
451
  readCommit: Effect.fn(
452
452
  "Artifacts.readCommit",
453
453
  spanOptions(binding, "readCommit"),
454
- )((hash: string) => tryArtifactsPromise(binding, "readCommit", () => repo.readCommit(hash))),
454
+ )(function* (hash: string) {
455
+ const commit = yield* tryArtifactsPromise(binding, "readCommit", () => repo.readCommit(hash));
456
+
457
+ if (commit === null)
458
+ return yield* new ArtifactsOperationError({
459
+ binding,
460
+ operation: "readCommit",
461
+ code: "NOT_FOUND",
462
+ numericCode: artifactsErrorNumericCodes.NOT_FOUND,
463
+ cause: new Error("Commit not found"),
464
+ });
465
+
466
+ return commit;
467
+ }),
455
468
  readTree: Effect.fn(
456
469
  "Artifacts.readTree",
457
470
  spanOptions(binding, "readTree"),
@@ -148,6 +148,7 @@ interface LegacyAttachmentMetadata {
148
148
  readonly version: 1;
149
149
  readonly clientId: number;
150
150
  readonly hasPendingRequests: boolean;
151
+ readonly serialization?: string | undefined;
151
152
  }
152
153
 
153
154
  interface PersistedPendingBatch {
@@ -170,6 +171,7 @@ interface ResumableAttachmentMetadata {
170
171
  readonly hasPendingRequests: boolean;
171
172
  readonly hasNonResumableRequests: boolean;
172
173
  readonly subscriptions: ReadonlyArray<PersistedSubscription>;
174
+ readonly serialization?: string | undefined;
173
175
  }
174
176
 
175
177
  type AttachmentMetadata = LegacyAttachmentMetadata | ResumableAttachmentMetadata;
@@ -200,6 +202,7 @@ const ResumableAttachmentMetadataSchema = Schema.Struct({
200
202
  hasPendingRequests: Schema.Boolean,
201
203
  hasNonResumableRequests: Schema.Boolean,
202
204
  subscriptions: Schema.Array(PersistedSubscriptionSchema),
205
+ serialization: Schema.optionalKey(Schema.String),
203
206
  });
204
207
 
205
208
  const decodeResumableAttachment = Schema.decodeUnknownOption(ResumableAttachmentMetadataSchema);
@@ -279,6 +282,7 @@ export const layer = (
279
282
  version: legacyAttachmentVersion,
280
283
  clientId: id,
281
284
  hasPendingRequests: false,
285
+ serialization: serialization.contentType,
282
286
  };
283
287
 
284
288
  writeAttachment(socket.raw, attachmentKey, created);
@@ -392,6 +396,7 @@ export const layer = (
392
396
  version: legacyAttachmentVersion,
393
397
  clientId: connection.id,
394
398
  hasPendingRequests: connection.requestIds.size > 0,
399
+ serialization: serialization.contentType,
395
400
  });
396
401
 
397
402
  return;
@@ -407,6 +412,7 @@ export const layer = (
407
412
  hasPendingRequests: connection.requestIds.size > 0,
408
413
  hasNonResumableRequests: connection.nonResumableRequestIds.size > 0,
409
414
  subscriptions,
415
+ serialization: serialization.contentType,
410
416
  });
411
417
  },
412
418
  catch: (cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
@@ -589,6 +595,14 @@ export const layer = (
589
595
 
590
596
  const metadata = attachment.metadata;
591
597
 
598
+ if (
599
+ metadata.serialization !== undefined &&
600
+ metadata.serialization !== serialization.contentType
601
+ ) {
602
+ yield* resetSocket(socket);
603
+ continue;
604
+ }
605
+
592
606
  if (metadata.version === legacyAttachmentVersion) {
593
607
  if (metadata.hasPendingRequests) {
594
608
  yield* resetSocket(socket);
@@ -1036,7 +1050,11 @@ export const layer = (
1036
1050
  : send(connection, RpcMessage.ResponseDefectEncoded(encodeDefect(cause)));
1037
1051
  }),
1038
1052
  ),
1039
- close: unregister,
1053
+ close: (socket) =>
1054
+ Effect.gen(function* () {
1055
+ resetSockets.add(socket.raw);
1056
+ yield* socket.close(1000).pipe(Effect.ignore);
1057
+ }).pipe(Effect.ensuring(unregister(socket)), Effect.uninterruptible),
1040
1058
  error: resetSocket,
1041
1059
  checkpoint,
1042
1060
  });
@@ -1102,13 +1120,17 @@ const readAttachment = (socket: WebSocket, key: string): AttachmentRead => {
1102
1120
  const hasPendingRequests = Predicate.hasProperty(metadata, "hasPendingRequests")
1103
1121
  ? metadata.hasPendingRequests
1104
1122
  : false;
1123
+ const serialization = Predicate.hasProperty(metadata, "serialization")
1124
+ ? metadata.serialization
1125
+ : undefined;
1105
1126
 
1106
1127
  if (
1107
1128
  (!Predicate.hasProperty(metadata, "version") || metadata.version === legacyAttachmentVersion) &&
1108
1129
  Predicate.hasProperty(metadata, "clientId") &&
1109
1130
  Predicate.isNumber(metadata.clientId) &&
1110
1131
  isClientId(metadata.clientId) &&
1111
- Predicate.isBoolean(hasPendingRequests)
1132
+ Predicate.isBoolean(hasPendingRequests) &&
1133
+ (serialization === undefined || Predicate.isString(serialization))
1112
1134
  ) {
1113
1135
  return {
1114
1136
  _tag: "Valid",
@@ -1116,6 +1138,7 @@ const readAttachment = (socket: WebSocket, key: string): AttachmentRead => {
1116
1138
  version: legacyAttachmentVersion,
1117
1139
  clientId: metadata.clientId,
1118
1140
  hasPendingRequests,
1141
+ serialization,
1119
1142
  },
1120
1143
  };
1121
1144
  }
@@ -0,0 +1,45 @@
1
+ import type * as Context from "effect/Context";
2
+ import * as Layer from "effect/Layer";
3
+ import type * as Rpc from "effect/unstable/rpc/Rpc";
4
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
5
+ import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
6
+ import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";
7
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
8
+ import * as Socket from "effect/unstable/socket/Socket";
9
+
10
+ export interface LayerOptions {
11
+ readonly socket?: Parameters<typeof Socket.layerWebSocket>[1];
12
+ readonly protocol?: Parameters<typeof RpcClient.layerProtocolSocket>[0];
13
+ readonly client?: Omit<NonNullable<Parameters<typeof RpcClient.make>[1]>, "flatten">;
14
+ /** Must match the server's serialization. Defaults to Effect RPC JSON. */
15
+ readonly serialization?: RpcSerialization.RpcSerialization["Service"] | undefined;
16
+ }
17
+
18
+ /**
19
+ * Provides a typed RPC client and its WebSocket transport for the layer's lifetime.
20
+ * Callers yield the service without supplying a Scope. Provide this layer once
21
+ * around the application or session to share the connection across calls.
22
+ *
23
+ * Provide Socket.layerWebSocketConstructorGlobal in browsers, or a platform's
24
+ * WebSocketConstructor layer elsewhere. Streaming RPCs returned as queues still
25
+ * require a consumer scope; ordinary Stream operations manage their own scopes.
26
+ */
27
+ export const layer = <I, Rpcs extends Rpc.Any>(
28
+ service: Context.Key<I, RpcClient.RpcClient<Rpcs, RpcClientError>>,
29
+ group: RpcGroup.RpcGroup<Rpcs>,
30
+ url: Parameters<typeof Socket.layerWebSocket>[0],
31
+ options: LayerOptions = {},
32
+ ): Layer.Layer<I, never, Socket.WebSocketConstructor | Rpc.MiddlewareClient<Rpcs>> =>
33
+ Layer.effect(service, RpcClient.make(group, options.client)).pipe(
34
+ Layer.provide(
35
+ RpcClient.layerProtocolSocket(options.protocol).pipe(
36
+ Layer.provide(Socket.layerWebSocket(url, options.socket)),
37
+ Layer.provide(
38
+ Layer.succeed(
39
+ RpcSerialization.RpcSerialization,
40
+ options.serialization ?? RpcSerialization.json,
41
+ ),
42
+ ),
43
+ ),
44
+ ),
45
+ );
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export * as Rpc from "./Rpc";
30
30
  export * as RpcDefinition from "./RpcDefinition";
31
31
  export * as RpcTracing from "./RpcTracing";
32
32
  export * as RpcTargets from "./RpcTargets";
33
+ export * as RpcWebSocketClient from "./RpcWebSocketClient";
33
34
  export * as ServiceBinding from "./ServiceBinding";
34
35
  export * as Vectorize from "./Vectorize";
35
36
  export * as WebTransport from "./WebTransport";