effect-cf 0.30.0 → 0.32.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/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-CepagPkX.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
+ }
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;
2521
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,7 +2916,16 @@ 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));
@@ -2563,23 +2934,76 @@ const layer$8 = (options = {}) => Layer.effectContext(Effect.gen(function* () {
2563
2934
  error: (socket, cause) => Effect.gen(function* () {
2564
2935
  yield* Effect.logDebug("Durable Object RPC websocket error", cause);
2565
2936
  yield* unregister(socket);
2566
- })
2937
+ }),
2938
+ checkpoint
2567
2939
  });
2568
2940
  return Context.mergeAll(Context.make(RpcServer.Protocol, protocol), Context.make(DurableObjectRpcWebSocket, service));
2569
2941
  }));
2570
2942
  const normalizeMessage = (message) => Predicate.isString(message) ? message : new Uint8Array(message);
2571
2943
  const readAttachment = (socket, key) => {
2572
2944
  const value = socket.deserializeAttachment();
2573
- if (Predicate.isObjectOrArray(value) && Predicate.hasProperty(value, key) && Predicate.isNumber(value[key])) return value[key];
2945
+ if (value === null || value === void 0) return { _tag: "Missing" };
2946
+ if (!Predicate.isObject(value)) return { _tag: "Invalid" };
2947
+ if (!Predicate.hasProperty(value, key)) return { _tag: "Missing" };
2948
+ const metadata = value[key];
2949
+ if (Predicate.isNumber(metadata) && isClientId(metadata)) return {
2950
+ _tag: "Valid",
2951
+ metadata: {
2952
+ version: legacyAttachmentVersion,
2953
+ clientId: metadata,
2954
+ hasPendingRequests: false
2955
+ }
2956
+ };
2957
+ if (!Predicate.isObject(metadata)) return { _tag: "Invalid" };
2958
+ if (Predicate.hasProperty(metadata, "version") && metadata.version === resumableAttachmentVersion) {
2959
+ const decoded = decodeResumableAttachment(metadata);
2960
+ return Option.isSome(decoded) && isClientId(decoded.value.clientId) ? {
2961
+ _tag: "Valid",
2962
+ metadata: decoded.value
2963
+ } : { _tag: "Invalid" };
2964
+ }
2965
+ const hasPendingRequests = Predicate.hasProperty(metadata, "hasPendingRequests") ? metadata.hasPendingRequests : false;
2966
+ if ((!Predicate.hasProperty(metadata, "version") || metadata.version === legacyAttachmentVersion) && Predicate.hasProperty(metadata, "clientId") && Predicate.isNumber(metadata.clientId) && isClientId(metadata.clientId) && Predicate.isBoolean(hasPendingRequests)) return {
2967
+ _tag: "Valid",
2968
+ metadata: {
2969
+ version: legacyAttachmentVersion,
2970
+ clientId: metadata.clientId,
2971
+ hasPendingRequests
2972
+ }
2973
+ };
2974
+ return { _tag: "Invalid" };
2574
2975
  };
2575
- const writeAttachment = (socket, key, clientId) => {
2976
+ const isClientId = (value) => Number.isSafeInteger(value) && value >= 0;
2977
+ const writeAttachment = (socket, key, metadata) => {
2576
2978
  const current = socket.deserializeAttachment();
2979
+ 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
2980
  const attachment = Predicate.isObject(current) ? current : {};
2981
+ const currentMetadata = Predicate.hasProperty(attachment, key) && Predicate.isObject(attachment[key]) ? attachment[key] : {};
2578
2982
  socket.serializeAttachment({
2579
2983
  ...attachment,
2580
- [key]: clientId
2984
+ [key]: {
2985
+ ...currentMetadata,
2986
+ ...metadata
2987
+ }
2581
2988
  });
2582
2989
  };
2990
+ const makeHeartbeatController = (state, serialization, policy) => Effect.gen(function* () {
2991
+ if (policy === "passthrough") return { setEnabled: () => Effect.void };
2992
+ const parser = serialization.makeUnsafe();
2993
+ const request = parser.encode(RpcMessage.constPing);
2994
+ const response = parser.encode(RpcMessage.constPong);
2995
+ 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"));
2996
+ const configured = yield* state.getWebSocketAutoResponse;
2997
+ 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"));
2998
+ const pair = new WebSocketRequestResponsePair(request, response);
2999
+ let enabled = configured !== null;
3000
+ return { setEnabled: (nextEnabled) => {
3001
+ if (enabled === nextEnabled) return Effect.void;
3002
+ return state.setWebSocketAutoResponse(nextEnabled ? pair : void 0).pipe(Effect.tap(() => Effect.sync(() => {
3003
+ enabled = nextEnabled;
3004
+ })));
3005
+ } };
3006
+ });
2583
3007
  //#endregion
2584
3008
  //#region src/DurableObjectSqlite.ts
2585
3009
  var DurableObjectSqlite_exports = /* @__PURE__ */ __exportAll({ layer: () => layer$7 });
@@ -2918,11 +3342,11 @@ const tryImagesSync = (binding, operation, evaluate) => Effect.try({
2918
3342
  });
2919
3343
  const maybe = (value) => value === null ? Option.none() : Option.some(value);
2920
3344
  const hasFunction = (value, key) => Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
2921
- const isHostedImagesBinding = (value) => hasFunction(value, "image") && hasFunction(value, "upload") && hasFunction(value, "list");
3345
+ const isHostedImagesBinding = (value) => hasFunction(value, "image") && hasFunction(value, "upload") && hasFunction(value, "list") && hasFunction(value, "createDirectUpload");
2922
3346
  const isImagesBinding = (value) => hasFunction(value, "info") && hasFunction(value, "input");
2923
3347
  const wrapResult = (binding, result) => ({
2924
3348
  raw: result,
2925
- response: tryImagesSync(binding, "response", () => result.response()),
3349
+ response: (options) => tryImagesSync(binding, "response", () => result.response(options)),
2926
3350
  contentType: tryImagesSync(binding, "contentType", () => result.contentType()),
2927
3351
  image: (options) => tryImagesSync(binding, "image", () => result.image(options))
2928
3352
  });
@@ -2930,6 +3354,7 @@ const wrapHandle = (binding, handle) => ({
2930
3354
  raw: handle,
2931
3355
  details: tryImagesPromise(binding, "details", () => handle.details()).pipe(Effect.map(maybe), Effect.withSpan("Images.details")),
2932
3356
  bytes: tryImagesPromise(binding, "bytes", () => handle.bytes()).pipe(Effect.map(maybe), Effect.withSpan("Images.bytes")),
3357
+ signedUrl: Effect.fn("Images.signedUrl")((options) => tryImagesPromise(binding, "signedUrl", () => handle.signedUrl(options))),
2933
3358
  update: Effect.fn("Images.update")((options) => tryImagesPromise(binding, "update", () => handle.update(options))),
2934
3359
  delete: tryImagesPromise(binding, "delete", () => handle.delete()).pipe(Effect.withSpan("Images.delete"))
2935
3360
  });
@@ -2937,12 +3362,17 @@ const wrapHosted = (binding, hosted) => ({
2937
3362
  image: (imageId) => wrapHandle(binding, hosted.image(imageId)),
2938
3363
  upload: Effect.fn("Images.upload")((image, options) => tryImagesPromise(binding, "upload", () => hosted.upload(image, options))),
2939
3364
  list: Effect.fn("Images.list")((options) => tryImagesPromise(binding, "list", () => hosted.list(options))),
3365
+ createDirectUpload: Effect.fn("Images.createDirectUpload")((options) => tryImagesPromise(binding, "createDirectUpload", () => hosted.createDirectUpload(options))),
2940
3366
  rawUnsafe: Effect.succeed(hosted)
2941
3367
  });
2942
3368
  const makeClient$4 = (definition) => (images) => {
2943
3369
  const input = (image, options) => tryImagesSync(definition.binding, "input", () => images.input(image, options));
3370
+ const text = (content, options) => tryImagesSync(definition.binding, "text", () => {
3371
+ if (!hasFunction(images, "text")) throw new Error("Images binding is missing text()");
3372
+ return images.text(content, options);
3373
+ });
2944
3374
  const process = Effect.fn("Images.process")(function* (steps, options) {
2945
- let transformer = yield* input(options.stream, options.inputOptions);
3375
+ let transformer = "text" in options ? yield* text(options.text, options.textOptions) : yield* input(options.stream, options.inputOptions);
2946
3376
  for (const step of steps.steps) transformer = yield* Step.$match(step, {
2947
3377
  Draw: (drawStep) => tryImagesSync(definition.binding, "draw", () => transformer.draw(drawStep.image, drawStep.options)),
2948
3378
  Transform: (transformStep) => tryImagesSync(definition.binding, "transform", () => transformer.transform(transformStep.transform))
@@ -2954,6 +3384,7 @@ const makeClient$4 = (definition) => (images) => {
2954
3384
  definition,
2955
3385
  info: Effect.fn("Images.info")((image, options) => tryImagesPromise(definition.binding, "info", () => images.info(image, options))),
2956
3386
  input,
3387
+ text,
2957
3388
  process,
2958
3389
  hosted: isHostedImagesBinding(images.hosted) ? Option.some(wrapHosted(definition.binding, images.hosted)) : Option.none(),
2959
3390
  rawUnsafe: Effect.succeed(images)
@@ -3377,8 +3808,9 @@ const makeClient = (definition) => {
3377
3808
  id: raw.id,
3378
3809
  pause: operation("pause", () => raw.pause()),
3379
3810
  resume: operation("resume", () => raw.resume()),
3380
- terminate: operation("terminate", () => raw.terminate()),
3811
+ terminate: (options) => operation("terminate", () => raw.terminate(options)),
3381
3812
  restart: (options) => operation("restart", () => raw.restart(options)),
3813
+ delete: operation("delete", () => raw.delete()),
3382
3814
  status: operation("status", () => raw.status()).pipe(Effect.flatMap((status) => Effect.gen(function* () {
3383
3815
  const output = status.output === void 0 || status.output === null && status.status !== "complete" ? Option.none() : Option.some(yield* decodeResult(status.output).pipe(Effect.mapError((cause) => new WorkflowResultDecodeError({
3384
3816
  binding: definition.binding,