effect-cf 0.31.0 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { a as WorkerEnvironment, i as WorkerConfig, n as violationsMessage, r as Environment_exports, t as causeMessage } from "./ErrorMessage-BM0K-Nik.mjs";
3
3
  import { n as Service$2, r as layer$19, t as Binding_exports } from "./Binding-pfilNJmP.mjs";
4
4
  import { n as Artifacts_exports } from "./Artifacts-CSIk1Oml.mjs";
5
- import { a as DurableObjectWebSocket_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as fromWebSocket, r as fromDurableObjectState, s as DurableObjectStorage_exports, t as DurableObjectState } from "./DurableObjectState-CxS5Y2hf.mjs";
5
+ import { a as DurableObjectWebSocket_exports, c as DurableObjectStorage_exports, i as fromExecutionContext, n as DurableObjectState_exports, o as DurableWebSocketAttachmentError, r as fromDurableObjectState, s as fromWebSocket, t as DurableObjectState } from "./DurableObjectState-D1Ibr6k0.mjs";
6
6
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Function as Function$1, Layer, ManagedRuntime, Option, Predicate, Queue, Result, Schema } from "effect";
7
7
  import { FetchHttpClient, HttpClient, HttpClientRequest, HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
8
8
  import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
@@ -2457,68 +2457,378 @@ var DurableObjectAlarm = class DurableObjectAlarm extends Context.Service()("eff
2457
2457
  //#region src/DurableObjectRpcWebSocket.ts
2458
2458
  var DurableObjectRpcWebSocket_exports = /* @__PURE__ */ __exportAll({
2459
2459
  DurableObjectRpcWebSocket: () => DurableObjectRpcWebSocket,
2460
- layer: () => layer$8
2460
+ layer: () => layer$8,
2461
+ resumableStream: () => resumableStream
2461
2462
  });
2462
2463
  const defaultAttachmentKey = "effectCloudflareRpcClientId";
2463
2464
  const defaultTag = "effect-cf-rpc";
2465
+ const legacyAttachmentVersion = 1;
2466
+ const resumableAttachmentVersion = 2;
2467
+ const serviceRestartCode = 1012;
2468
+ const serviceRestartReason = "Durable Object RPC activation reset";
2469
+ /** Declares an explicitly resumable RPC stream. */
2470
+ const resumableStream = (declaration) => declaration;
2471
+ const PersistedPendingBatchSchema = Schema.Struct({ checkpointTokens: Schema.Array(Schema.String) });
2472
+ const PersistedSubscriptionSchema = Schema.Struct({
2473
+ definitionId: Schema.String,
2474
+ subscriptionKey: Schema.String,
2475
+ requestId: Schema.Union([Schema.String, Schema.Finite]),
2476
+ rpcTag: Schema.String,
2477
+ resumeDescriptor: Schema.Unknown,
2478
+ acknowledgedCheckpoint: Schema.Unknown,
2479
+ pending: Schema.optionalKey(PersistedPendingBatchSchema)
2480
+ });
2481
+ const ResumableAttachmentMetadataSchema = Schema.Struct({
2482
+ version: Schema.Literal(resumableAttachmentVersion),
2483
+ clientId: Schema.Finite,
2484
+ hasPendingRequests: Schema.Boolean,
2485
+ hasNonResumableRequests: Schema.Boolean,
2486
+ subscriptions: Schema.Array(PersistedSubscriptionSchema)
2487
+ });
2488
+ const decodeResumableAttachment = Schema.decodeUnknownOption(ResumableAttachmentMetadataSchema);
2464
2489
  /**
2465
2490
  * Context tag for the Durable Object RPC websocket service.
2466
2491
  */
2467
2492
  var DurableObjectRpcWebSocket = class extends Context.Service()("effect-cf/DurableObjectRpcWebSocket") {};
2468
2493
  /**
2469
- * Builds a layer that bridges Durable Object websocket events to `RpcServer.Protocol`.
2494
+ * Builds a hibernation-aware transport that bridges Durable Object websocket
2495
+ * events to `RpcServer.Protocol`.
2496
+ *
2497
+ * The layer restores idle tagged sockets and explicitly declared resumable
2498
+ * streams after constructor recreation. A socket that lost any other in-flight
2499
+ * request is closed with code 1012, which the stock Effect socket protocol
2500
+ * reports as an `RpcClientError` containing `SocketCloseError`.
2470
2501
  */
2471
2502
  const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2472
2503
  const tag = options.tag ?? defaultTag;
2473
2504
  const attachmentKey = options.attachmentKey ?? defaultAttachmentKey;
2505
+ const heartbeatPolicy = options.heartbeat ?? "auto-response";
2506
+ const declarations = options.resumableStreams ?? [];
2474
2507
  const durableObjectState = yield* DurableObjectState;
2475
2508
  const serialization = yield* RpcSerialization.RpcSerialization;
2476
2509
  const disconnects = yield* Queue.make();
2477
2510
  const connectionsBySocket = /* @__PURE__ */ new Map();
2478
2511
  const connectionsById = /* @__PURE__ */ new Map();
2479
2512
  const clientIds = /* @__PURE__ */ new Set();
2513
+ const resetSockets = /* @__PURE__ */ new WeakSet();
2514
+ const declarationsById = /* @__PURE__ */ new Map();
2515
+ const declarationsByTag = /* @__PURE__ */ new Map();
2516
+ const replayRequests = [];
2480
2517
  let nextClientId = 0;
2518
+ let activeNonResumableRequestCount = 0;
2481
2519
  let writeRequest;
2520
+ for (const declaration of declarations) {
2521
+ if (declaration.id.length === 0 || declaration.rpcTag.length === 0) return yield* Effect.die(/* @__PURE__ */ new Error("Resumable RPC stream declaration ids and tags must be non-empty"));
2522
+ if (declarationsById.has(declaration.id)) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate resumable RPC stream declaration id: ${declaration.id}`));
2523
+ if (declarationsByTag.has(declaration.rpcTag)) return yield* Effect.die(/* @__PURE__ */ new Error(`Duplicate resumable RPC stream tag: ${declaration.rpcTag}`));
2524
+ declarationsById.set(declaration.id, declaration);
2525
+ declarationsByTag.set(declaration.rpcTag, declaration);
2526
+ }
2482
2527
  const reserveClientId = (socket) => {
2483
2528
  const attachment = readAttachment(socket.raw, attachmentKey);
2484
- if (attachment !== void 0) {
2485
- nextClientId = Math.max(nextClientId, attachment + 1);
2486
- return attachment;
2529
+ if (attachment._tag === "Invalid") throw new Error("Invalid Durable Object RPC websocket attachment metadata");
2530
+ if (attachment._tag === "Valid") {
2531
+ const metadata = attachment.metadata;
2532
+ nextClientId = Math.max(nextClientId, metadata.clientId + 1);
2533
+ writeAttachment(socket.raw, attachmentKey, metadata);
2534
+ return metadata;
2487
2535
  }
2488
2536
  const id = nextClientId++;
2489
- writeAttachment(socket.raw, attachmentKey, id);
2490
- return id;
2537
+ const created = {
2538
+ version: legacyAttachmentVersion,
2539
+ clientId: id,
2540
+ hasPendingRequests: false
2541
+ };
2542
+ writeAttachment(socket.raw, attachmentKey, created);
2543
+ return created;
2544
+ };
2545
+ const addSubscription = (connection, subscription) => {
2546
+ if (connection.subscriptionsByRequestId.has(subscription.requestId)) return false;
2547
+ let byKey = connection.subscriptionsByDefinition.get(subscription.definitionId);
2548
+ if (byKey?.has(subscription.subscriptionKey) === true) return false;
2549
+ if (byKey === void 0) {
2550
+ byKey = /* @__PURE__ */ new Map();
2551
+ connection.subscriptionsByDefinition.set(subscription.definitionId, byKey);
2552
+ }
2553
+ byKey.set(subscription.subscriptionKey, subscription);
2554
+ connection.subscriptionsByRequestId.set(subscription.requestId, subscription);
2555
+ connection.requestIds.add(subscription.requestId);
2556
+ connection.metadataVersion = resumableAttachmentVersion;
2557
+ return true;
2558
+ };
2559
+ const removeSubscription = (connection, requestId) => {
2560
+ const subscription = connection.subscriptionsByRequestId.get(requestId);
2561
+ if (subscription === void 0) return;
2562
+ connection.subscriptionsByRequestId.delete(requestId);
2563
+ const byKey = connection.subscriptionsByDefinition.get(subscription.definitionId);
2564
+ byKey?.delete(subscription.subscriptionKey);
2565
+ if (byKey?.size === 0) connection.subscriptionsByDefinition.delete(subscription.definitionId);
2566
+ return subscription;
2491
2567
  };
2492
- const register = (socket) => {
2568
+ const register = (socket, restoredMetadata, restoredSubscriptions = []) => {
2493
2569
  const existing = connectionsBySocket.get(socket.raw);
2494
2570
  if (existing !== void 0) return existing;
2571
+ const metadata = restoredMetadata ?? reserveClientId(socket);
2495
2572
  const connection = {
2496
- id: reserveClientId(socket),
2573
+ id: metadata.clientId,
2497
2574
  socket,
2498
- parser: serialization.makeUnsafe()
2575
+ parser: serialization.makeUnsafe(),
2576
+ requestIds: /* @__PURE__ */ new Set(),
2577
+ nonResumableRequestIds: /* @__PURE__ */ new Set(),
2578
+ subscriptionsByRequestId: /* @__PURE__ */ new Map(),
2579
+ subscriptionsByDefinition: /* @__PURE__ */ new Map(),
2580
+ suppressedAckRequestIds: /* @__PURE__ */ new Set(),
2581
+ currentPendingRequestIds: /* @__PURE__ */ new Set(),
2582
+ metadataVersion: metadata.version
2499
2583
  };
2584
+ if (connectionsById.has(connection.id)) throw new Error(`Duplicate Durable Object RPC websocket client id: ${connection.id}`);
2585
+ for (const subscription of restoredSubscriptions) if (!addSubscription(connection, subscription)) throw new Error("Duplicate restored resumable RPC stream subscription identity");
2500
2586
  connectionsBySocket.set(socket.raw, connection);
2501
2587
  connectionsById.set(connection.id, connection);
2502
2588
  clientIds.add(connection.id);
2589
+ nextClientId = Math.max(nextClientId, connection.id + 1);
2503
2590
  return connection;
2504
2591
  };
2505
- const unregister = (socket) => Effect.sync(() => {
2592
+ const heartbeat = yield* makeHeartbeatController(durableObjectState, serialization, heartbeatPolicy);
2593
+ const syncHeartbeat = () => heartbeat.setEnabled(connectionsBySocket.size > 0 && activeNonResumableRequestCount === 0);
2594
+ const persistConnection = Effect.fn("DurableObjectRpcWebSocket.persistConnection")(function* (connection) {
2595
+ return yield* Effect.try({
2596
+ try: () => {
2597
+ if (connection.metadataVersion === legacyAttachmentVersion) {
2598
+ writeAttachment(connection.socket.raw, attachmentKey, {
2599
+ version: legacyAttachmentVersion,
2600
+ clientId: connection.id,
2601
+ hasPendingRequests: connection.requestIds.size > 0
2602
+ });
2603
+ return;
2604
+ }
2605
+ const subscriptions = Array.from(connection.subscriptionsByRequestId.values()).map(({ declaration: _, ...subscription }) => subscription);
2606
+ writeAttachment(connection.socket.raw, attachmentKey, {
2607
+ version: resumableAttachmentVersion,
2608
+ clientId: connection.id,
2609
+ hasPendingRequests: connection.requestIds.size > 0,
2610
+ hasNonResumableRequests: connection.nonResumableRequestIds.size > 0,
2611
+ subscriptions
2612
+ });
2613
+ },
2614
+ catch: (cause) => new DurableWebSocketAttachmentError({
2615
+ operation: "serialize",
2616
+ cause
2617
+ })
2618
+ });
2619
+ });
2620
+ const unregister = Effect.fn("DurableObjectRpcWebSocket.unregister")(function* (socket) {
2506
2621
  const connection = connectionsBySocket.get(socket.raw);
2507
2622
  if (connection === void 0) return;
2508
2623
  connectionsBySocket.delete(socket.raw);
2509
2624
  connectionsById.delete(connection.id);
2510
2625
  clientIds.delete(connection.id);
2626
+ activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
2627
+ connection.requestIds.clear();
2628
+ connection.nonResumableRequestIds.clear();
2629
+ connection.subscriptionsByRequestId.clear();
2630
+ connection.subscriptionsByDefinition.clear();
2631
+ connection.suppressedAckRequestIds.clear();
2632
+ connection.currentPendingRequestIds.clear();
2511
2633
  Queue.offerUnsafe(disconnects, connection.id);
2512
- });
2513
- for (const socket of yield* durableObjectState.getWebSockets(tag)) register(socket);
2514
- const send = (connection, response) => Effect.sync(() => {
2515
- try {
2516
- const encoded = connection.parser.encode(response);
2517
- if (encoded !== void 0) connection.socket.raw.send(encoded);
2518
- } catch (cause) {
2519
- const encoded = connection.parser.encode(RpcMessage.ResponseDefectEncoded(cause));
2520
- if (encoded !== void 0) connection.socket.raw.send(encoded);
2634
+ yield* syncHeartbeat();
2635
+ });
2636
+ const resetSocket = Effect.fn("DurableObjectRpcWebSocket.resetSocket")(function* (socket, cause) {
2637
+ resetSockets.add(socket.raw);
2638
+ yield* unregister(socket).pipe(Effect.catchCause((unregisterCause) => Effect.logDebug("Failed to unregister Durable Object RPC websocket", unregisterCause)));
2639
+ if (cause !== void 0) yield* Effect.logDebug("Resetting Durable Object RPC websocket", cause);
2640
+ yield* socket.close(serviceRestartCode, serviceRestartReason).pipe(Effect.catch((closeError) => Effect.logDebug("Failed to close Durable Object RPC websocket", closeError)));
2641
+ });
2642
+ const persistOrReset = Effect.fn("DurableObjectRpcWebSocket.persistOrReset")(function* (connection) {
2643
+ return yield* persistConnection(connection).pipe(Effect.as(true), Effect.catch((cause) => resetSocket(connection.socket, cause).pipe(Effect.as(false))));
2644
+ });
2645
+ const trackRequest = Effect.fn("DurableObjectRpcWebSocket.trackRequest")(function* (connection, request) {
2646
+ if (connection.requestIds.has(request.id)) return true;
2647
+ const declaration = declarationsByTag.get(request.tag);
2648
+ const identified = declaration === void 0 ? Option.none() : yield* Effect.sync(() => declaration.identify(request));
2649
+ if (declaration !== void 0 && Option.isSome(identified)) {
2650
+ if (!addSubscription(connection, {
2651
+ declaration,
2652
+ definitionId: declaration.id,
2653
+ subscriptionKey: identified.value.subscriptionKey,
2654
+ requestId: request.id,
2655
+ rpcTag: declaration.rpcTag,
2656
+ resumeDescriptor: identified.value.resumeDescriptor,
2657
+ acknowledgedCheckpoint: identified.value.acknowledgedCheckpoint
2658
+ })) {
2659
+ yield* resetSocket(connection.socket, /* @__PURE__ */ new Error(`Duplicate resumable RPC stream subscription: ${declaration.id}`));
2660
+ return false;
2661
+ }
2662
+ } else {
2663
+ connection.requestIds.add(request.id);
2664
+ connection.nonResumableRequestIds.add(request.id);
2665
+ activeNonResumableRequestCount++;
2666
+ }
2667
+ if (!(yield* persistOrReset(connection))) return false;
2668
+ yield* syncHeartbeat();
2669
+ return true;
2670
+ });
2671
+ const completeRequest = Effect.fn("DurableObjectRpcWebSocket.completeRequest")(function* (connection, requestId, preserveAckFilter = false) {
2672
+ if (connection.requestIds.delete(requestId)) {
2673
+ if (connection.nonResumableRequestIds.delete(requestId)) activeNonResumableRequestCount--;
2674
+ const subscription = removeSubscription(connection, requestId);
2675
+ connection.currentPendingRequestIds.delete(requestId);
2676
+ if (preserveAckFilter && subscription !== void 0) connection.suppressedAckRequestIds.add(requestId);
2677
+ if (!(yield* persistOrReset(connection))) return;
2678
+ }
2679
+ if (!preserveAckFilter) connection.suppressedAckRequestIds.delete(requestId);
2680
+ yield* syncHeartbeat();
2681
+ });
2682
+ const completeAllRequests = Effect.fn("DurableObjectRpcWebSocket.completeAllRequests")(function* (connection) {
2683
+ const hadRequests = connection.requestIds.size > 0;
2684
+ if (connection.requestIds.size > 0) activeNonResumableRequestCount -= connection.nonResumableRequestIds.size;
2685
+ connection.requestIds.clear();
2686
+ connection.nonResumableRequestIds.clear();
2687
+ connection.subscriptionsByRequestId.clear();
2688
+ connection.subscriptionsByDefinition.clear();
2689
+ connection.suppressedAckRequestIds.clear();
2690
+ connection.currentPendingRequestIds.clear();
2691
+ if (hadRequests && !(yield* persistOrReset(connection))) return;
2692
+ yield* syncHeartbeat();
2693
+ });
2694
+ const restoredAttachments = (yield* durableObjectState.getWebSockets(tag)).map((socket) => ({
2695
+ socket,
2696
+ attachment: readAttachment(socket.raw, attachmentKey)
2697
+ }));
2698
+ for (const { attachment, socket } of restoredAttachments) {
2699
+ if (attachment._tag === "Invalid") {
2700
+ yield* resetSocket(socket);
2701
+ continue;
2702
+ }
2703
+ if (attachment._tag === "Missing") {
2704
+ yield* resetSocket(socket);
2705
+ continue;
2706
+ }
2707
+ const metadata = attachment.metadata;
2708
+ if (metadata.version === legacyAttachmentVersion) {
2709
+ if (metadata.hasPendingRequests) yield* resetSocket(socket);
2710
+ else if (connectionsById.has(metadata.clientId)) yield* resetSocket(socket);
2711
+ else yield* persistOrReset(register(socket, metadata));
2712
+ continue;
2713
+ }
2714
+ const prepared = yield* Effect.sync(() => {
2715
+ try {
2716
+ if (metadata.hasNonResumableRequests || metadata.hasPendingRequests !== (metadata.hasNonResumableRequests || metadata.subscriptions.length > 0)) throw new Error("Dirty resumable RPC websocket attachment metadata");
2717
+ const requestIds = /* @__PURE__ */ new Set();
2718
+ const subscriptionKeys = /* @__PURE__ */ new Map();
2719
+ const subscriptions = [];
2720
+ const requests = [];
2721
+ for (const persisted of metadata.subscriptions) {
2722
+ const declaration = declarationsById.get(persisted.definitionId);
2723
+ if (declaration === void 0 || declaration.rpcTag !== persisted.rpcTag) throw new Error("Missing resumable RPC stream declaration");
2724
+ let keys = subscriptionKeys.get(persisted.definitionId);
2725
+ if (keys === void 0) {
2726
+ keys = /* @__PURE__ */ new Set();
2727
+ subscriptionKeys.set(persisted.definitionId, keys);
2728
+ }
2729
+ if (requestIds.has(persisted.requestId) || keys.has(persisted.subscriptionKey)) throw new Error("Duplicate persisted resumable RPC stream subscription");
2730
+ requestIds.add(persisted.requestId);
2731
+ keys.add(persisted.subscriptionKey);
2732
+ if (persisted.pending !== void 0) {
2733
+ const { checkpointTokens } = persisted.pending;
2734
+ if (checkpointTokens.length === 0 || checkpointTokens.some((token) => token.length === 0) || new Set(checkpointTokens).size !== checkpointTokens.length) throw new Error("Invalid persisted resumable RPC stream pending batch");
2735
+ }
2736
+ const resumeDescriptor = Schema.decodeUnknownOption(declaration.resumeDescriptorSchema)(persisted.resumeDescriptor);
2737
+ const acknowledgedCheckpoint = Schema.decodeUnknownOption(declaration.checkpointSchema)(persisted.acknowledgedCheckpoint);
2738
+ if (Option.isNone(resumeDescriptor) || Option.isNone(acknowledgedCheckpoint)) throw new Error("Invalid persisted resumable RPC stream state");
2739
+ const rebuilt = declaration.rebuild({
2740
+ subscriptionKey: persisted.subscriptionKey,
2741
+ resumeDescriptor: resumeDescriptor.value,
2742
+ acknowledgedCheckpoint: acknowledgedCheckpoint.value
2743
+ });
2744
+ const request = {
2745
+ _tag: "Request",
2746
+ id: persisted.requestId,
2747
+ tag: declaration.rpcTag,
2748
+ payload: rebuilt.payload,
2749
+ headers: rebuilt.headers?.map(([name, value]) => [name, value]) ?? []
2750
+ };
2751
+ if (rebuilt.traceId !== void 0) Object.assign(request, { traceId: rebuilt.traceId });
2752
+ if (rebuilt.spanId !== void 0) Object.assign(request, { spanId: rebuilt.spanId });
2753
+ if (rebuilt.sampled !== void 0) Object.assign(request, { sampled: rebuilt.sampled });
2754
+ subscriptions.push({
2755
+ ...persisted,
2756
+ resumeDescriptor: resumeDescriptor.value,
2757
+ acknowledgedCheckpoint: acknowledgedCheckpoint.value,
2758
+ declaration
2759
+ });
2760
+ requests.push(request);
2761
+ }
2762
+ return {
2763
+ _tag: "Success",
2764
+ requests,
2765
+ subscriptions
2766
+ };
2767
+ } catch (cause) {
2768
+ return {
2769
+ _tag: "Failure",
2770
+ cause
2771
+ };
2772
+ }
2773
+ });
2774
+ if (prepared._tag === "Failure") {
2775
+ yield* Effect.logDebug("Invalid resumable RPC websocket attachment", prepared.cause);
2776
+ yield* resetSocket(socket);
2777
+ continue;
2778
+ }
2779
+ if (connectionsById.has(metadata.clientId)) {
2780
+ yield* resetSocket(socket);
2781
+ continue;
2782
+ }
2783
+ const connection = register(socket, metadata, prepared.subscriptions);
2784
+ for (const request of prepared.requests) replayRequests.push({
2785
+ clientId: connection.id,
2786
+ request
2787
+ });
2788
+ }
2789
+ yield* syncHeartbeat();
2790
+ const send = (connection, response) => Effect.gen(function* () {
2791
+ let delivered = response;
2792
+ if (response._tag === "Chunk") {
2793
+ const subscription = connection.subscriptionsByRequestId.get(response.requestId);
2794
+ if (subscription !== void 0) {
2795
+ subscription.pending = { checkpointTokens: yield* Effect.sync(() => {
2796
+ const tokens = [];
2797
+ for (const value of response.values) {
2798
+ const checkpoint = subscription.declaration.checkpointFromValue(value);
2799
+ if (Option.isNone(checkpoint)) throw new Error(`Resumable RPC stream ${subscription.definitionId} did not identify a checkpoint`);
2800
+ const token = subscription.declaration.checkpointToken(checkpoint.value);
2801
+ if (token.length === 0 || tokens.includes(token)) throw new Error(`Resumable RPC stream ${subscription.definitionId} produced invalid checkpoint tokens`);
2802
+ tokens.push(token);
2803
+ }
2804
+ if (tokens.length === 0) throw new Error("Resumable RPC stream chunks must contain checkpointed values");
2805
+ return tokens;
2806
+ }) };
2807
+ connection.currentPendingRequestIds.add(subscription.requestId);
2808
+ if (!(yield* persistOrReset(connection))) return;
2809
+ }
2521
2810
  }
2811
+ if (!(yield* Effect.sync(() => {
2812
+ try {
2813
+ const encoded = connection.parser.encode(response);
2814
+ if (encoded === void 0) return false;
2815
+ connection.socket.raw.send(encoded);
2816
+ return true;
2817
+ } catch (cause) {
2818
+ delivered = RpcMessage.ResponseDefectEncoded(cause);
2819
+ const encoded = connection.parser.encode(delivered);
2820
+ if (encoded === void 0) return false;
2821
+ connection.socket.raw.send(encoded);
2822
+ return true;
2823
+ }
2824
+ }).pipe(Effect.catchCause((cause) => Effect.logDebug("Durable Object RPC websocket send failed", cause).pipe(Effect.as(false)))))) {
2825
+ resetSockets.add(connection.socket.raw);
2826
+ yield* connection.socket.close(serviceRestartCode, serviceRestartReason).pipe(Effect.ignore);
2827
+ yield* unregister(connection.socket);
2828
+ return;
2829
+ }
2830
+ if (delivered._tag === "Exit") yield* completeRequest(connection, delivered.requestId);
2831
+ else if (delivered._tag === "Defect" || delivered._tag === "ClientProtocolError") yield* completeAllRequests(connection);
2522
2832
  });
2523
2833
  const protocol = yield* RpcServer.Protocol.make((writeRequest_) => {
2524
2834
  writeRequest = writeRequest_;
@@ -2538,12 +2848,64 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2538
2848
  supportsSpanPropagation: true
2539
2849
  });
2540
2850
  });
2851
+ const replay = writeRequest;
2852
+ if (replay === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("RPC server protocol did not install its request writer"));
2853
+ for (const restoredRequest of replayRequests) yield* replay(restoredRequest.clientId, restoredRequest.request);
2854
+ const checkpoint = Effect.fn("DurableObjectRpcWebSocket.checkpoint")(function* (declaration, checkpointOptions) {
2855
+ const connection = connectionsById.get(checkpointOptions.clientId);
2856
+ const configured = declarationsById.get(declaration.id);
2857
+ const subscription = connection?.subscriptionsByDefinition.get(declaration.id)?.get(checkpointOptions.subscriptionKey);
2858
+ if (connection === void 0 || configured === void 0 || configured.rpcTag !== declaration.rpcTag || subscription === void 0 || subscription.pending === void 0) return false;
2859
+ const token = configured.checkpointToken(checkpointOptions.checkpoint);
2860
+ const checkpointIndex = subscription.pending.checkpointTokens.indexOf(token);
2861
+ if (token.length === 0 || checkpointIndex < 0) return false;
2862
+ const pending = subscription.pending;
2863
+ const isFinal = checkpointIndex === pending.checkpointTokens.length - 1;
2864
+ const shouldAck = isFinal && connection.currentPendingRequestIds.has(subscription.requestId);
2865
+ return yield* Effect.uninterruptible(Effect.gen(function* () {
2866
+ subscription.acknowledgedCheckpoint = checkpointOptions.checkpoint;
2867
+ if (isFinal) {
2868
+ connection.currentPendingRequestIds.delete(subscription.requestId);
2869
+ delete subscription.pending;
2870
+ } else subscription.pending = { checkpointTokens: pending.checkpointTokens.slice(checkpointIndex + 1) };
2871
+ yield* persistConnection(connection).pipe(Effect.catch((cause) => Effect.gen(function* () {
2872
+ yield* resetSocket(connection.socket, cause);
2873
+ return yield* cause;
2874
+ })));
2875
+ if (shouldAck) {
2876
+ const run = writeRequest;
2877
+ if (run === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("RPC server is not ready to accept a checkpoint Ack"));
2878
+ yield* run(connection.id, {
2879
+ _tag: "Ack",
2880
+ requestId: subscription.requestId
2881
+ });
2882
+ }
2883
+ return true;
2884
+ }));
2885
+ });
2541
2886
  const service = DurableObjectRpcWebSocket.of({
2542
- accept: (socket) => Effect.gen(function* () {
2887
+ accept: (socket, tags = []) => Effect.gen(function* () {
2543
2888
  register(socket);
2544
- yield* durableObjectState.acceptWebSocket(socket, [tag]);
2889
+ yield* durableObjectState.acceptWebSocket(socket, Array.from(/* @__PURE__ */ new Set([tag, ...tags])));
2890
+ yield* syncHeartbeat();
2891
+ }),
2892
+ acceptUpgrade: (upgradeOptions = {}) => Effect.gen(function* () {
2893
+ const pair = new WebSocketPair();
2894
+ const client = pair[0];
2895
+ const server = fromWebSocket(pair[1]);
2896
+ if (upgradeOptions.attachment !== void 0) yield* server.serializeAttachment(upgradeOptions.attachment);
2897
+ yield* service.accept(server, upgradeOptions.tags);
2898
+ return {
2899
+ client,
2900
+ server,
2901
+ response: new Response(null, {
2902
+ status: 101,
2903
+ webSocket: client
2904
+ })
2905
+ };
2545
2906
  }),
2546
2907
  message: (socket, message) => Effect.gen(function* () {
2908
+ if (resetSockets.has(socket.raw)) return;
2547
2909
  const connection = register(socket);
2548
2910
  const decoded = yield* Effect.try({
2549
2911
  try: () => connection.parser.decode(normalizeMessage(message)),
@@ -2554,32 +2916,91 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2554
2916
  yield* send(connection, RpcMessage.ResponseDefectEncoded("RPC server is not ready"));
2555
2917
  return;
2556
2918
  }
2557
- for (const current of decoded) yield* run(connection.id, current);
2919
+ for (const current of decoded) {
2920
+ const request = current;
2921
+ if (request._tag === "Request" && request.isNotification !== true) {
2922
+ if (!(yield* trackRequest(connection, request))) return;
2923
+ }
2924
+ if (request._tag === "Ack" && (connection.subscriptionsByRequestId.has(request.requestId) || connection.suppressedAckRequestIds.has(request.requestId))) continue;
2925
+ if (request._tag === "Interrupt" && connection.subscriptionsByRequestId.has(request.requestId)) yield* completeRequest(connection, request.requestId, true);
2926
+ yield* run(connection.id, request);
2927
+ if (request._tag === "Interrupt" && !connection.suppressedAckRequestIds.has(request.requestId)) yield* completeRequest(connection, request.requestId);
2928
+ }
2558
2929
  }).pipe(Effect.catch((cause) => {
2559
2930
  const connection = register(socket);
2560
2931
  return Predicate.isTagged(cause, "MaxBufferSizeExceeded") ? Effect.ignore(connection.socket.close(1009, String(cause))) : send(connection, RpcMessage.ResponseDefectEncoded(cause));
2561
2932
  })),
2562
2933
  close: unregister,
2563
- error: (socket, cause) => Effect.gen(function* () {
2564
- yield* Effect.logDebug("Durable Object RPC websocket error", cause);
2565
- yield* unregister(socket);
2566
- })
2934
+ error: resetSocket,
2935
+ checkpoint
2567
2936
  });
2568
2937
  return Context.mergeAll(Context.make(RpcServer.Protocol, protocol), Context.make(DurableObjectRpcWebSocket, service));
2569
2938
  }));
2570
2939
  const normalizeMessage = (message) => Predicate.isString(message) ? message : new Uint8Array(message);
2571
2940
  const readAttachment = (socket, key) => {
2572
2941
  const value = socket.deserializeAttachment();
2573
- if (Predicate.isObjectOrArray(value) && Predicate.hasProperty(value, key) && Predicate.isNumber(value[key])) return value[key];
2942
+ if (value === null || value === void 0) return { _tag: "Missing" };
2943
+ if (!Predicate.isObject(value)) return { _tag: "Invalid" };
2944
+ if (!Predicate.hasProperty(value, key)) return { _tag: "Missing" };
2945
+ const metadata = value[key];
2946
+ if (Predicate.isNumber(metadata) && isClientId(metadata)) return {
2947
+ _tag: "Valid",
2948
+ metadata: {
2949
+ version: legacyAttachmentVersion,
2950
+ clientId: metadata,
2951
+ hasPendingRequests: false
2952
+ }
2953
+ };
2954
+ if (!Predicate.isObject(metadata)) return { _tag: "Invalid" };
2955
+ if (Predicate.hasProperty(metadata, "version") && metadata.version === resumableAttachmentVersion) {
2956
+ const decoded = decodeResumableAttachment(metadata);
2957
+ return Option.isSome(decoded) && isClientId(decoded.value.clientId) ? {
2958
+ _tag: "Valid",
2959
+ metadata: decoded.value
2960
+ } : { _tag: "Invalid" };
2961
+ }
2962
+ const hasPendingRequests = Predicate.hasProperty(metadata, "hasPendingRequests") ? metadata.hasPendingRequests : false;
2963
+ if ((!Predicate.hasProperty(metadata, "version") || metadata.version === legacyAttachmentVersion) && Predicate.hasProperty(metadata, "clientId") && Predicate.isNumber(metadata.clientId) && isClientId(metadata.clientId) && Predicate.isBoolean(hasPendingRequests)) return {
2964
+ _tag: "Valid",
2965
+ metadata: {
2966
+ version: legacyAttachmentVersion,
2967
+ clientId: metadata.clientId,
2968
+ hasPendingRequests
2969
+ }
2970
+ };
2971
+ return { _tag: "Invalid" };
2574
2972
  };
2575
- const writeAttachment = (socket, key, clientId) => {
2973
+ const isClientId = (value) => Number.isSafeInteger(value) && value >= 0;
2974
+ const writeAttachment = (socket, key, metadata) => {
2576
2975
  const current = socket.deserializeAttachment();
2976
+ if (current !== null && current !== void 0 && !Predicate.isObject(current)) throw new Error("DurableObjectRpcWebSocket requires object-shaped websocket attachments so adapter metadata can preserve application fields");
2577
2977
  const attachment = Predicate.isObject(current) ? current : {};
2978
+ const currentMetadata = Predicate.hasProperty(attachment, key) && Predicate.isObject(attachment[key]) ? attachment[key] : {};
2578
2979
  socket.serializeAttachment({
2579
2980
  ...attachment,
2580
- [key]: clientId
2981
+ [key]: {
2982
+ ...currentMetadata,
2983
+ ...metadata
2984
+ }
2581
2985
  });
2582
2986
  };
2987
+ const makeHeartbeatController = (state, serialization, policy) => Effect.gen(function* () {
2988
+ if (policy === "passthrough") return { setEnabled: () => Effect.void };
2989
+ const parser = serialization.makeUnsafe();
2990
+ const request = parser.encode(RpcMessage.constPing);
2991
+ const response = parser.encode(RpcMessage.constPong);
2992
+ if (!Predicate.isString(request) || !Predicate.isString(response)) return yield* Effect.die(/* @__PURE__ */ new Error("DurableObjectRpcWebSocket heartbeat auto-response requires a text RPC serialization; use heartbeat: \"passthrough\" to manage heartbeats externally"));
2993
+ const configured = yield* state.getWebSocketAutoResponse;
2994
+ if (configured !== null && (configured.request !== request || configured.response !== response)) return yield* Effect.die(/* @__PURE__ */ new Error("DurableObjectRpcWebSocket cannot replace the application WebSocket auto-response; use heartbeat: \"passthrough\" to preserve it"));
2995
+ const pair = new WebSocketRequestResponsePair(request, response);
2996
+ let enabled = configured !== null;
2997
+ return { setEnabled: (nextEnabled) => {
2998
+ if (enabled === nextEnabled) return Effect.void;
2999
+ return state.setWebSocketAutoResponse(nextEnabled ? pair : void 0).pipe(Effect.tap(() => Effect.sync(() => {
3000
+ enabled = nextEnabled;
3001
+ })));
3002
+ } };
3003
+ });
2583
3004
  //#endregion
2584
3005
  //#region src/DurableObjectSqlite.ts
2585
3006
  var DurableObjectSqlite_exports = /* @__PURE__ */ __exportAll({ layer: () => layer$7 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.31.0",
3
+ "version": "0.32.1",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {