effect-cf 0.31.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/CHANGELOG.md +6 -0
- package/README.md +94 -1
- package/dist/ComputerWorkspace.d.mts +1 -1
- package/dist/ComputerWorkspace.mjs +1 -1
- package/dist/{DurableObjectState-DVIgf0sA.d.mts → DurableObjectState-B0A0Y1ef.d.mts} +1 -1
- package/dist/{DurableObjectState-CxS5Y2hf.mjs → DurableObjectState-D1Ibr6k0.mjs} +1 -1
- package/dist/Vitest.d.mts +2 -2
- package/dist/Vitest.mjs +1 -1
- package/dist/{WorkflowBinding-C9KRmfDU.d.mts → WorkflowBinding-B9_S8tX2.d.mts} +1 -1
- package/dist/index.d.mts +92 -6
- package/dist/index.mjs +452 -28
- package/package.json +1 -1
- package/src/DurableObjectRpcWebSocket.ts +1030 -42
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
|
|
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
|
|
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
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
2514
|
-
const
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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;
|
|
2521
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;
|
|
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)
|
|
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 (
|
|
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
|
|
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]:
|
|
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 });
|