effect-durable-agent-cloudflare 0.1.0-alpha.21 → 0.1.0-alpha.23
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/durable-object.d.ts +7 -2
- package/dist/durable-object.js +21 -2
- package/dist/durable-object.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{session-controller-BGfz1DdS.js → session-controller-BnNMF12R.js} +595 -8
- package/dist/session-controller-BnNMF12R.js.map +1 -0
- package/dist/{session-controller-DoeFP_Ej.d.ts → session-controller-CJ7qI4-U.d.ts} +51 -3
- package/dist/session-controller.d.ts +1 -1
- package/dist/session-controller.js +1 -1
- package/package.json +2 -2
- package/dist/session-controller-BGfz1DdS.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "effect-durable-agent/services/prompt-projector";
|
|
2
2
|
import * as Layer from "effect/Layer";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
4
5
|
import * as Schema from "effect/Schema";
|
|
5
6
|
import { ContextRebasedPayload, DurableEventEnvelope, SummaryCreatedPayload, assistantMessageCommittedEventType, assistantMessageImportedEventType, assistantPartialCommittedEventType, commandAdmittedEventType, commandCancelledEventType, commandCompletedEventType, commandFailedEventType, commandStartedEventType, contextRebasedEventType, decodeDurableEventSync, effectDurableAgentNamespace, steeringMessageQueuedEventType, summaryCreatedEventType, systemMessageCommittedEventType, userMessageCommittedEventType } from "effect-durable-agent/types/events";
|
|
6
7
|
import { SequenceNumber, SessionId, SummaryId, durablePosition } from "effect-durable-agent/types/core";
|
|
@@ -20,7 +21,8 @@ import { EDAKeepAlive } from "effect-durable-agent/services/keep-alive";
|
|
|
20
21
|
import { EDAReducerRegistry } from "effect-durable-agent/services/reducer-registry";
|
|
21
22
|
import { makeEDARuntimeLayer } from "effect-durable-agent/services/runtime-layer";
|
|
22
23
|
import { SessionEventObserver } from "effect-durable-agent/services/session-event-observer";
|
|
23
|
-
import { EDAWebSocketDeliveryCheckpoint, EDA_WEB_SOCKET_PING_MESSAGE, EDA_WEB_SOCKET_PONG_MESSAGE, EDA_WS_CLOSE_PROTOCOL_ERROR, EDA_WS_CLOSE_SEND_FAILED, SubscriberId, checkpointWebSocketDeliveryState, decodeEDAWebSocketClientMessage, defaultEDAWebSocketFlowControl, deliveryHelloFrame, encodeEDAWebSocketServerFrame, makeWebSocketDeliveryState, onClientAck, onDurableEvents, onEphemeralEvent, onHostSuppressedFrame, restoreWebSocketDeliveryState } from "effect-durable-agent/websocket";
|
|
24
|
+
import { EDATranscriptClientFrame, EDATranscriptDeliveryCheckpoint, EDAWebSocketDeliveryCheckpoint, EDA_WEB_SOCKET_PING_MESSAGE, EDA_WEB_SOCKET_PONG_MESSAGE, EDA_WEB_SOCKET_PROTOCOL_VERSION, EDA_WS_CLOSE_LAGGED, EDA_WS_CLOSE_PROTOCOL_ERROR, EDA_WS_CLOSE_SEND_FAILED, EDA_WS_CLOSE_TRANSCRIPT_RESUBSCRIBE, SubscriberId, checkpointTranscriptDelivery, checkpointWebSocketDeliveryState, decodeEDAWebSocketClientMessage, defaultEDATranscriptDeliveryLimits, defaultEDAWebSocketFlowControl, deliveryHelloFrame, edaTranscriptNegotiation, edaTranscriptUpdateFrameText, encodeEDATranscriptServerFrame, encodeEDATranscriptUpdate, encodeEDAWebSocketServerFrame, enqueueTranscriptRequest, laggedCloseReason, makeTranscriptDeliveryState, makeWebSocketDeliveryState, onClientAck, onDurableEvents, onEphemeralEvent, onHostSuppressedFrame, onTranscriptAck, pumpTranscriptTransfer, restoreTranscriptDeliveryState, restoreWebSocketDeliveryState, startTranscriptUpdate, transcriptAckTimedOut, transcriptDeliveryCapacity, utf8ByteLength } from "effect-durable-agent/websocket";
|
|
25
|
+
import { defaultTranscriptSubscriptionPolicy, emptyTranscriptActiveContentState, foldTranscriptActiveContent, planTranscriptUpdate, readTranscriptSubscription, transcriptActiveContentOf, transcriptSubscriptionBaseOf } from "effect-durable-agent/transcript";
|
|
24
26
|
/**
|
|
25
27
|
* Alarm-backed in-memory keepalive for a raw Durable Object host.
|
|
26
28
|
*
|
|
@@ -986,13 +988,27 @@ const EDAWebSocketAttachment = Schema.Struct({
|
|
|
986
988
|
state: Schema.Unknown
|
|
987
989
|
}))
|
|
988
990
|
});
|
|
991
|
+
/**
|
|
992
|
+
* State required to resume a transcript-window socket after hibernation: the transport receipts,
|
|
993
|
+
* the applied cut, the subscription base and unanswered requests. Staged chunk data is never
|
|
994
|
+
* persisted; a checkpoint that names a transfer cannot resume it.
|
|
995
|
+
*/
|
|
996
|
+
const EDATranscriptWebSocketAttachment = Schema.Struct({
|
|
997
|
+
kind: Schema.Literal("eda-transcript-v1"),
|
|
998
|
+
sessionId: SessionId,
|
|
999
|
+
subscriberId: SubscriberId,
|
|
1000
|
+
trace: EDATraceMetadata,
|
|
1001
|
+
delivery: EDATranscriptDeliveryCheckpoint
|
|
1002
|
+
});
|
|
1003
|
+
/** Either socket kind an EDA Durable Object accepts. */
|
|
1004
|
+
const EDAAnyWebSocketAttachment = Schema.Union([EDAWebSocketAttachment, EDATranscriptWebSocketAttachment]);
|
|
989
1005
|
/** Decode and classify an attachment read from Cloudflare's structured-clone boundary. */
|
|
990
1006
|
const decodeWebSocketAttachment = async (input) => {
|
|
991
1007
|
if (input === null || input === void 0) return { _tag: "Missing" };
|
|
992
1008
|
try {
|
|
993
1009
|
return {
|
|
994
1010
|
_tag: "Decoded",
|
|
995
|
-
attachment: await Effect.runPromise(Schema.decodeUnknownEffect(
|
|
1011
|
+
attachment: await Effect.runPromise(Schema.decodeUnknownEffect(EDAAnyWebSocketAttachment)(input))
|
|
996
1012
|
};
|
|
997
1013
|
} catch {
|
|
998
1014
|
return { _tag: "Malformed" };
|
|
@@ -1000,6 +1016,505 @@ const decodeWebSocketAttachment = async (input) => {
|
|
|
1000
1016
|
};
|
|
1001
1017
|
/** Encode before serialization so schema transformations cannot leak into attachments. */
|
|
1002
1018
|
const encodeWebSocketAttachment = (attachment) => Schema.encodeSync(EDAWebSocketAttachment)(attachment);
|
|
1019
|
+
/** Encode a transcript socket attachment for Cloudflare's structured-clone boundary. */
|
|
1020
|
+
const encodeTranscriptWebSocketAttachment = (attachment) => Schema.encodeSync(EDATranscriptWebSocketAttachment)(attachment);
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/websocket/transcript-sockets.ts
|
|
1023
|
+
const defaultActiveContentCadenceMs = 50;
|
|
1024
|
+
/** Largest client frame accepted; requests are persisted, so this bounds the attachment too. */
|
|
1025
|
+
const maxClientFrameLength = 2048;
|
|
1026
|
+
const decodeClientFrame = Schema.decodeUnknownExit(EDATranscriptClientFrame);
|
|
1027
|
+
/**
|
|
1028
|
+
* Owns transcript-window sockets: the window data source beneath the frame encoder.
|
|
1029
|
+
*
|
|
1030
|
+
* Every subscriber is served strictly in order. Committed changes are coalesced to the complete
|
|
1031
|
+
* boundary a store read returns; a request is answered by one read at one cut; and each update is
|
|
1032
|
+
* sent from its own captured snapshot before the next is planned.
|
|
1033
|
+
*
|
|
1034
|
+
* **Publication fence.** Append publication, ephemeral publication and window capture share one
|
|
1035
|
+
* fence: the active-content accumulator folds every published event in order, and a capture is
|
|
1036
|
+
* served with live content only when the accumulator has folded exactly the window's cut. Durable
|
|
1037
|
+
* commit precedes live publication, so a window read between the two would otherwise pair a
|
|
1038
|
+
* committed inference with its stale live parts. A capture that would cross the fence waits for
|
|
1039
|
+
* publication to catch up.
|
|
1040
|
+
*/
|
|
1041
|
+
var EDATranscriptSockets = class {
|
|
1042
|
+
options;
|
|
1043
|
+
sockets = /* @__PURE__ */ new Map();
|
|
1044
|
+
active = emptyTranscriptActiveContentState;
|
|
1045
|
+
timerArmed = false;
|
|
1046
|
+
background = /* @__PURE__ */ new Set();
|
|
1047
|
+
flowControl;
|
|
1048
|
+
policy;
|
|
1049
|
+
limits;
|
|
1050
|
+
cadenceMs;
|
|
1051
|
+
fenceWaitMs;
|
|
1052
|
+
maxTotalStagedBytes;
|
|
1053
|
+
schedule;
|
|
1054
|
+
now;
|
|
1055
|
+
constructor(options) {
|
|
1056
|
+
this.options = options;
|
|
1057
|
+
this.flowControl = options.flowControl ?? defaultEDAWebSocketFlowControl;
|
|
1058
|
+
this.policy = options.subscriptionPolicy ?? defaultTranscriptSubscriptionPolicy;
|
|
1059
|
+
this.limits = options.deliveryLimits ?? defaultEDATranscriptDeliveryLimits;
|
|
1060
|
+
this.cadenceMs = options.activeContentCadenceMs ?? 50;
|
|
1061
|
+
this.fenceWaitMs = options.fenceWaitMs ?? 1e3;
|
|
1062
|
+
this.maxTotalStagedBytes = options.maxTotalStagedBytes ?? 33554432;
|
|
1063
|
+
this.schedule = options.schedule ?? ((delayMs, run) => {
|
|
1064
|
+
setTimeout(run, delayMs);
|
|
1065
|
+
});
|
|
1066
|
+
this.now = options.now ?? Date.now;
|
|
1067
|
+
}
|
|
1068
|
+
has(webSocket) {
|
|
1069
|
+
return this.sockets.has(webSocket);
|
|
1070
|
+
}
|
|
1071
|
+
/** Transcript sockets this isolate holds, for hosts that cannot enumerate accepted sockets. */
|
|
1072
|
+
webSockets() {
|
|
1073
|
+
return [...this.sockets.keys()];
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Register a freshly accepted socket and greet it. Synchronous so the attachment exists before
|
|
1077
|
+
* any platform callback can observe the socket.
|
|
1078
|
+
*/
|
|
1079
|
+
accept(input) {
|
|
1080
|
+
const socket = {
|
|
1081
|
+
sessionId: input.sessionId,
|
|
1082
|
+
subscriberId: SubscriberId.make(crypto.randomUUID()),
|
|
1083
|
+
trace: input.trace,
|
|
1084
|
+
delivery: makeTranscriptDeliveryState(this.flowControl),
|
|
1085
|
+
queue: Promise.resolve(),
|
|
1086
|
+
dirty: false,
|
|
1087
|
+
sentActiveRevision: void 0,
|
|
1088
|
+
last: void 0,
|
|
1089
|
+
fenceWaitingSinceMs: void 0,
|
|
1090
|
+
scheduledDrain: void 0,
|
|
1091
|
+
stagedBytes: 0,
|
|
1092
|
+
activeSentAtMs: void 0,
|
|
1093
|
+
cadenceDue: false
|
|
1094
|
+
};
|
|
1095
|
+
this.sockets.set(input.webSocket, socket);
|
|
1096
|
+
if (!this.persist(input.webSocket, socket)) return;
|
|
1097
|
+
this.sendText(input.webSocket, encodeEDATranscriptServerFrame({
|
|
1098
|
+
_tag: "hello",
|
|
1099
|
+
protocolVersion: EDA_WEB_SOCKET_PROTOCOL_VERSION,
|
|
1100
|
+
subscriberId: socket.subscriberId,
|
|
1101
|
+
resumeSeq: 0,
|
|
1102
|
+
flowControl: this.flowControl,
|
|
1103
|
+
negotiation: edaTranscriptNegotiation
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Adopt a socket that survived hibernation. A socket that was mid-transfer lost its staged
|
|
1108
|
+
* chunks with the isolate: it is closed, and the client reconnects and resubscribes.
|
|
1109
|
+
*/
|
|
1110
|
+
restore(webSocket, attachment) {
|
|
1111
|
+
const restored = restoreTranscriptDeliveryState(this.flowControl, attachment.delivery);
|
|
1112
|
+
if (restored._tag === "TransferLost") {
|
|
1113
|
+
this.close(webSocket, EDA_WS_CLOSE_TRANSCRIPT_RESUBSCRIBE, "transfer-lost");
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
this.sockets.set(webSocket, {
|
|
1117
|
+
sessionId: attachment.sessionId,
|
|
1118
|
+
subscriberId: attachment.subscriberId,
|
|
1119
|
+
trace: attachment.trace,
|
|
1120
|
+
delivery: restored.state,
|
|
1121
|
+
queue: Promise.resolve(),
|
|
1122
|
+
dirty: true,
|
|
1123
|
+
sentActiveRevision: void 0,
|
|
1124
|
+
last: void 0,
|
|
1125
|
+
fenceWaitingSinceMs: void 0,
|
|
1126
|
+
scheduledDrain: void 0,
|
|
1127
|
+
stagedBytes: 0,
|
|
1128
|
+
activeSentAtMs: void 0,
|
|
1129
|
+
cadenceDue: false
|
|
1130
|
+
});
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
closed(webSocket) {
|
|
1134
|
+
this.sockets.delete(webSocket);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Observe one published event, in publication order. Folds it into the active-content
|
|
1138
|
+
* accumulator synchronously, then lets each listed subscriber act on it: a durable event
|
|
1139
|
+
* schedules a read at the new boundary; changed live content is coalesced to the display
|
|
1140
|
+
* cadence.
|
|
1141
|
+
*
|
|
1142
|
+
* Reads are scheduled, not awaited, so subscribers never hold the session's append gate. The
|
|
1143
|
+
* fence does not need the gate: every capture is checked against the accumulator's folded head
|
|
1144
|
+
* when its read resolves.
|
|
1145
|
+
*/
|
|
1146
|
+
onPublished(event, webSockets) {
|
|
1147
|
+
const revision = this.active.revision;
|
|
1148
|
+
this.active = foldTranscriptActiveContent(this.active, event);
|
|
1149
|
+
if (event.event.durability !== "durable") {
|
|
1150
|
+
if (this.active.revision !== revision) this.armCadence();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
for (const webSocket of webSockets) {
|
|
1154
|
+
const socket = this.sockets.get(webSocket);
|
|
1155
|
+
if (socket === void 0) continue;
|
|
1156
|
+
socket.dirty = true;
|
|
1157
|
+
this.track(this.drain(webSocket, socket));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/** Serve sockets restored from hibernation after a publication was folded. */
|
|
1161
|
+
serveRestored(webSockets) {
|
|
1162
|
+
for (const webSocket of webSockets) {
|
|
1163
|
+
const socket = this.sockets.get(webSocket);
|
|
1164
|
+
if (socket === void 0) continue;
|
|
1165
|
+
socket.dirty = true;
|
|
1166
|
+
this.track(this.drain(webSocket, socket));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* The session's log was destroyed: live content belongs to a log that no longer exists and
|
|
1171
|
+
* every subscription names its incarnation. Close transcript sockets so clients resubscribe.
|
|
1172
|
+
*/
|
|
1173
|
+
reset() {
|
|
1174
|
+
this.active = emptyTranscriptActiveContentState;
|
|
1175
|
+
for (const webSocket of [...this.sockets.keys()]) this.close(webSocket, EDA_WS_CLOSE_TRANSCRIPT_RESUBSCRIBE, "session-reset");
|
|
1176
|
+
}
|
|
1177
|
+
async message(webSocket, message) {
|
|
1178
|
+
const socket = this.sockets.get(webSocket);
|
|
1179
|
+
if (socket === void 0) {
|
|
1180
|
+
this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (message === EDA_WEB_SOCKET_PING_MESSAGE) {
|
|
1184
|
+
this.sendText(webSocket, EDA_WEB_SOCKET_PONG_MESSAGE);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (message.length > maxClientFrameLength) {
|
|
1188
|
+
this.protocolError(webSocket, "Client frame too large");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
let json;
|
|
1192
|
+
try {
|
|
1193
|
+
json = JSON.parse(message);
|
|
1194
|
+
} catch {
|
|
1195
|
+
this.protocolError(webSocket, "Malformed client frame");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const decoded = decodeClientFrame(json);
|
|
1199
|
+
if (Exit.isFailure(decoded)) {
|
|
1200
|
+
this.protocolError(webSocket, "Unknown client frame");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const frame = decoded.value;
|
|
1204
|
+
if (frame._tag === "ping") return;
|
|
1205
|
+
await this.enqueue(webSocket, socket, async () => {
|
|
1206
|
+
if (frame._tag === "ack") {
|
|
1207
|
+
const acked = onTranscriptAck(socket.delivery, frame);
|
|
1208
|
+
if (acked._tag === "ProtocolError") {
|
|
1209
|
+
this.protocolError(webSocket, acked.message);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (acked._tag === "Ignored") return;
|
|
1213
|
+
socket.delivery = acked.state;
|
|
1214
|
+
if (!this.persist(webSocket, socket)) return;
|
|
1215
|
+
} else {
|
|
1216
|
+
const queued = enqueueTranscriptRequest(socket.delivery, frame, this.limits);
|
|
1217
|
+
if (queued === void 0) {
|
|
1218
|
+
this.protocolError(webSocket, "Too many unanswered transcript requests");
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
socket.delivery = queued;
|
|
1222
|
+
if (!this.persist(webSocket, socket)) return;
|
|
1223
|
+
}
|
|
1224
|
+
await this.drainNow(webSocket, socket);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
/** Wait for scheduled reads and cadence flushes; for orderly shutdown and tests. */
|
|
1228
|
+
async settled() {
|
|
1229
|
+
while (this.background.size > 0) await Promise.all([...this.background]);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Own scheduled subscriber work until it settles. Drains never reject: `enqueue` contains every
|
|
1233
|
+
* failure and closes the socket.
|
|
1234
|
+
*/
|
|
1235
|
+
track(work) {
|
|
1236
|
+
const tracked = work.finally(() => {
|
|
1237
|
+
this.background.delete(tracked);
|
|
1238
|
+
});
|
|
1239
|
+
this.background.add(tracked);
|
|
1240
|
+
}
|
|
1241
|
+
armCadence() {
|
|
1242
|
+
if (this.timerArmed) return;
|
|
1243
|
+
this.timerArmed = true;
|
|
1244
|
+
this.schedule(this.cadenceMs, () => {
|
|
1245
|
+
this.timerArmed = false;
|
|
1246
|
+
for (const [webSocket, socket] of this.sockets) {
|
|
1247
|
+
socket.cadenceDue = true;
|
|
1248
|
+
this.track(this.drain(webSocket, socket));
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Schedule one drain, coalescing with a drain that is scheduled but not yet running. A drain
|
|
1254
|
+
* that is already running re-checks `dirty` before it returns, so nothing is missed.
|
|
1255
|
+
*/
|
|
1256
|
+
drain(webSocket, socket) {
|
|
1257
|
+
if (socket.scheduledDrain !== void 0) return socket.scheduledDrain;
|
|
1258
|
+
const scheduled = this.enqueue(webSocket, socket, () => {
|
|
1259
|
+
socket.scheduledDrain = void 0;
|
|
1260
|
+
return this.drainNow(webSocket, socket);
|
|
1261
|
+
});
|
|
1262
|
+
socket.scheduledDrain = scheduled;
|
|
1263
|
+
return scheduled;
|
|
1264
|
+
}
|
|
1265
|
+
/** Send everything this subscriber is owed, one complete update at a time. */
|
|
1266
|
+
async drainNow(webSocket, socket) {
|
|
1267
|
+
for (;;) {
|
|
1268
|
+
if (this.sockets.get(webSocket) !== socket) return;
|
|
1269
|
+
const nowMs = this.now();
|
|
1270
|
+
if (transcriptAckTimedOut(socket.delivery, nowMs)) {
|
|
1271
|
+
this.sendText(webSocket, encodeEDATranscriptServerFrame({
|
|
1272
|
+
_tag: "lagged",
|
|
1273
|
+
resumeSeq: socket.delivery.appliedThroughSeq,
|
|
1274
|
+
reason: "ack-timeout"
|
|
1275
|
+
}));
|
|
1276
|
+
this.close(webSocket, EDA_WS_CLOSE_LAGGED, laggedCloseReason(socket.delivery.appliedThroughSeq));
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (socket.delivery.transfer !== void 0) {
|
|
1280
|
+
const pumped = pumpTranscriptTransfer(socket.delivery, nowMs);
|
|
1281
|
+
if (pumped.frames.length === 0) return;
|
|
1282
|
+
socket.delivery = pumped.state;
|
|
1283
|
+
if (pumped.state.transfer === void 0) socket.stagedBytes = 0;
|
|
1284
|
+
if (!this.persistAndSend(webSocket, socket, pumped.frames)) return;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
if (transcriptDeliveryCapacity(socket.delivery) <= 0) return;
|
|
1288
|
+
const request = socket.delivery.requests[0];
|
|
1289
|
+
const base = socket.delivery.subscription;
|
|
1290
|
+
if (request === void 0) {
|
|
1291
|
+
if (base === void 0) return;
|
|
1292
|
+
if (!socket.dirty) {
|
|
1293
|
+
if (socket.sentActiveRevision === this.active.revision) {
|
|
1294
|
+
socket.cadenceDue = false;
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (!(socket.cadenceDue || socket.activeSentAtMs === void 0 || nowMs - socket.activeSentAtMs >= this.cadenceMs)) {
|
|
1298
|
+
this.armCadence();
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const sent = await this.sendActiveOnly(webSocket, socket);
|
|
1302
|
+
if (sent === "NeedsRead") {
|
|
1303
|
+
socket.dirty = true;
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (sent === "Failed") return;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
const subscriptionRequest = request === void 0 ? { _tag: "Refresh" } : toSubscriptionRequest(request);
|
|
1311
|
+
socket.dirty = false;
|
|
1312
|
+
const read = await Effect.runPromiseExit(readTranscriptSubscription(this.options.transcriptStore, base, subscriptionRequest, this.policy));
|
|
1313
|
+
if (this.sockets.get(webSocket) !== socket) return;
|
|
1314
|
+
if (Exit.isFailure(read)) {
|
|
1315
|
+
await Effect.runPromise(Effect.logError("EDA transcript window read failed", {
|
|
1316
|
+
cause: read.cause,
|
|
1317
|
+
subscriberId: socket.subscriberId
|
|
1318
|
+
}));
|
|
1319
|
+
this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "read-failed");
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const { window, patchBase } = read.value;
|
|
1323
|
+
const active = this.active;
|
|
1324
|
+
const fenced = !(window.readiness._tag === "Ready") || window.throughSeq === active.publishedThroughSeq || window.throughSeq > active.publishedThroughSeq && active.inference === void 0;
|
|
1325
|
+
if (!fenced) {
|
|
1326
|
+
socket.fenceWaitingSinceMs ??= nowMs;
|
|
1327
|
+
if (nowMs - socket.fenceWaitingSinceMs < this.fenceWaitMs) {
|
|
1328
|
+
socket.dirty = true;
|
|
1329
|
+
this.armCadence();
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
socket.fenceWaitingSinceMs = void 0;
|
|
1334
|
+
const update = planTranscriptUpdate({
|
|
1335
|
+
patchBase,
|
|
1336
|
+
window,
|
|
1337
|
+
updateSeq: socket.delivery.nextUpdateSeq,
|
|
1338
|
+
activeContent: fenced ? transcriptActiveContentOf(active) : [],
|
|
1339
|
+
includeActiveContent: !fenced || socket.sentActiveRevision !== active.revision || window.readiness._tag === "Ready" && socket.last !== void 0 && socket.last.readiness._tag !== "Ready",
|
|
1340
|
+
...request?.requestId === void 0 ? {} : { requestId: request.requestId }
|
|
1341
|
+
});
|
|
1342
|
+
const remaining = request === void 0 ? socket.delivery.requests : socket.delivery.requests.slice(1);
|
|
1343
|
+
if (request === void 0 && this.isNoop(socket, update)) continue;
|
|
1344
|
+
if (!await this.start(webSocket, socket, update, transcriptSubscriptionBaseOf(window), remaining)) return;
|
|
1345
|
+
socket.sentActiveRevision = active.revision;
|
|
1346
|
+
if (update.activeContent !== void 0) {
|
|
1347
|
+
socket.cadenceDue = false;
|
|
1348
|
+
socket.activeSentAtMs = this.now();
|
|
1349
|
+
}
|
|
1350
|
+
socket.last = {
|
|
1351
|
+
readiness: window.readiness,
|
|
1352
|
+
hasOlder: window.hasOlder,
|
|
1353
|
+
hasNewer: window.hasNewer,
|
|
1354
|
+
dependencyIds: update.dependencyIds
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Publish changed live content without a store read. Valid only while the accumulator has
|
|
1360
|
+
* folded exactly the subscriber's last cut; otherwise a commit is pending and a read is needed.
|
|
1361
|
+
*/
|
|
1362
|
+
async sendActiveOnly(webSocket, socket) {
|
|
1363
|
+
const base = socket.delivery.subscription;
|
|
1364
|
+
const last = socket.last;
|
|
1365
|
+
const active = this.active;
|
|
1366
|
+
if (last !== void 0 && last.readiness._tag !== "Ready") {
|
|
1367
|
+
socket.sentActiveRevision = active.revision;
|
|
1368
|
+
return "Sent";
|
|
1369
|
+
}
|
|
1370
|
+
if (base === void 0 || last === void 0 || active.publishedThroughSeq !== base.throughSeq) return "NeedsRead";
|
|
1371
|
+
const update = {
|
|
1372
|
+
updateSeq: socket.delivery.nextUpdateSeq,
|
|
1373
|
+
mode: "patch",
|
|
1374
|
+
incarnation: base.incarnation,
|
|
1375
|
+
version: base.version,
|
|
1376
|
+
throughSeq: base.throughSeq,
|
|
1377
|
+
...base.coverage === void 0 ? {} : { coverage: base.coverage },
|
|
1378
|
+
hasOlder: last.hasOlder,
|
|
1379
|
+
hasNewer: last.hasNewer,
|
|
1380
|
+
readiness: last.readiness,
|
|
1381
|
+
records: [],
|
|
1382
|
+
dependencies: [],
|
|
1383
|
+
dependencyIds: last.dependencyIds,
|
|
1384
|
+
activeContent: transcriptActiveContentOf(active)
|
|
1385
|
+
};
|
|
1386
|
+
if (!await this.start(webSocket, socket, update, base, socket.delivery.requests)) return "Failed";
|
|
1387
|
+
socket.sentActiveRevision = active.revision;
|
|
1388
|
+
socket.cadenceDue = false;
|
|
1389
|
+
socket.activeSentAtMs = this.now();
|
|
1390
|
+
return "Sent";
|
|
1391
|
+
}
|
|
1392
|
+
/** A refresh that changes nothing the client holds is not sent. */
|
|
1393
|
+
isNoop(socket, update) {
|
|
1394
|
+
const base = socket.delivery.subscription;
|
|
1395
|
+
const last = socket.last;
|
|
1396
|
+
return base !== void 0 && last !== void 0 && update.mode === "patch" && update.records.length === 0 && update.dependencies.length === 0 && update.activeContent === void 0 && update.throughSeq === base.throughSeq && update.hasOlder === last.hasOlder && update.hasNewer === last.hasNewer && JSON.stringify(update.coverage) === JSON.stringify(base.coverage) && JSON.stringify(update.readiness) === JSON.stringify(last.readiness);
|
|
1397
|
+
}
|
|
1398
|
+
async start(webSocket, socket, update, subscription, requests) {
|
|
1399
|
+
const encodedUpdate = encodeEDATranscriptUpdate(update);
|
|
1400
|
+
const bytes = utf8ByteLength(encodedUpdate);
|
|
1401
|
+
if (utf8ByteLength(edaTranscriptUpdateFrameText(socket.delivery.nextFrameId, encodedUpdate)) > this.flowControl.maxFrameBytes && this.stagedBytes() + bytes > this.maxTotalStagedBytes) {
|
|
1402
|
+
this.close(webSocket, EDA_WS_CLOSE_TRANSCRIPT_RESUBSCRIBE, "staging-budget");
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
const started = startTranscriptUpdate({
|
|
1406
|
+
...socket.delivery,
|
|
1407
|
+
requests
|
|
1408
|
+
}, {
|
|
1409
|
+
update,
|
|
1410
|
+
encodedUpdate,
|
|
1411
|
+
subscription
|
|
1412
|
+
}, this.limits, this.now());
|
|
1413
|
+
if (started._tag === "TooLarge") {
|
|
1414
|
+
await Effect.runPromise(Effect.logError("EDA transcript update exceeds the staging bound", {
|
|
1415
|
+
bytes: started.bytes,
|
|
1416
|
+
subscriberId: socket.subscriberId
|
|
1417
|
+
}));
|
|
1418
|
+
this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "update-too-large");
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
socket.delivery = started.state;
|
|
1422
|
+
socket.stagedBytes = started.state.transfer === void 0 ? 0 : bytes;
|
|
1423
|
+
return this.persistAndSend(webSocket, socket, started.frames);
|
|
1424
|
+
}
|
|
1425
|
+
/** Bytes of chunked updates still being transferred, across every socket of this object. */
|
|
1426
|
+
stagedBytes() {
|
|
1427
|
+
let total = 0;
|
|
1428
|
+
for (const socket of this.sockets.values()) total += socket.stagedBytes;
|
|
1429
|
+
return total;
|
|
1430
|
+
}
|
|
1431
|
+
/** Persist the checkpoint before any frame it accounts for becomes client-visible. */
|
|
1432
|
+
persistAndSend(webSocket, socket, frames) {
|
|
1433
|
+
if (!this.persist(webSocket, socket)) return false;
|
|
1434
|
+
for (const frame of frames) if (!this.sendText(webSocket, frame)) return false;
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
persist(webSocket, socket) {
|
|
1438
|
+
try {
|
|
1439
|
+
webSocket.serializeAttachment(encodeTranscriptWebSocketAttachment(EDATranscriptWebSocketAttachment.make({
|
|
1440
|
+
kind: "eda-transcript-v1",
|
|
1441
|
+
sessionId: socket.sessionId,
|
|
1442
|
+
subscriberId: socket.subscriberId,
|
|
1443
|
+
trace: socket.trace,
|
|
1444
|
+
delivery: checkpointTranscriptDelivery(socket.delivery)
|
|
1445
|
+
})));
|
|
1446
|
+
return true;
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
Effect.runSync(Effect.logError("EDA transcript socket attachment failed", {
|
|
1449
|
+
error,
|
|
1450
|
+
subscriberId: socket.subscriberId
|
|
1451
|
+
}));
|
|
1452
|
+
this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "attachment-failed");
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
sendText(webSocket, text) {
|
|
1457
|
+
try {
|
|
1458
|
+
webSocket.send(text);
|
|
1459
|
+
return true;
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
Effect.runSync(Effect.logWarning("EDA transcript socket send failed", { error }));
|
|
1462
|
+
this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "send-failed");
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
protocolError(webSocket, message) {
|
|
1467
|
+
this.sendText(webSocket, encodeEDATranscriptServerFrame({
|
|
1468
|
+
_tag: "error",
|
|
1469
|
+
message: message.slice(0, 120)
|
|
1470
|
+
}));
|
|
1471
|
+
this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
|
|
1472
|
+
}
|
|
1473
|
+
enqueue(webSocket, socket, operation) {
|
|
1474
|
+
const run = async () => {
|
|
1475
|
+
try {
|
|
1476
|
+
await operation();
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
await Effect.runPromise(Effect.logError("EDA transcript socket operation failed", {
|
|
1479
|
+
error,
|
|
1480
|
+
subscriberId: socket.subscriberId
|
|
1481
|
+
}));
|
|
1482
|
+
this.close(webSocket, EDA_WS_CLOSE_SEND_FAILED, "subscriber-failed");
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
const next = socket.queue.then(run);
|
|
1486
|
+
socket.queue = next;
|
|
1487
|
+
return next;
|
|
1488
|
+
}
|
|
1489
|
+
close(webSocket, code, reason) {
|
|
1490
|
+
this.sockets.delete(webSocket);
|
|
1491
|
+
try {
|
|
1492
|
+
webSocket.close(code, reason);
|
|
1493
|
+
} catch {}
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
const toSubscriptionRequest = (frame) => {
|
|
1497
|
+
switch (frame._tag) {
|
|
1498
|
+
case "subscribe": return {
|
|
1499
|
+
_tag: "Subscribe",
|
|
1500
|
+
tail: frame.tail ?? false,
|
|
1501
|
+
...frame.range === void 0 ? {} : { range: frame.range },
|
|
1502
|
+
...frame.since === void 0 ? {} : { since: frame.since },
|
|
1503
|
+
...frame.count === void 0 ? {} : { count: frame.count }
|
|
1504
|
+
};
|
|
1505
|
+
case "page": return {
|
|
1506
|
+
_tag: "Page",
|
|
1507
|
+
direction: frame.direction,
|
|
1508
|
+
cursor: frame.cursor,
|
|
1509
|
+
count: frame.count
|
|
1510
|
+
};
|
|
1511
|
+
case "retain": return {
|
|
1512
|
+
_tag: "Retain",
|
|
1513
|
+
from: frame.from,
|
|
1514
|
+
to: frame.to
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1003
1518
|
//#endregion
|
|
1004
1519
|
//#region src/websocket/connection-manager.ts
|
|
1005
1520
|
/**
|
|
@@ -1010,8 +1525,33 @@ var EDAWebSocketConnectionManager = class {
|
|
|
1010
1525
|
options;
|
|
1011
1526
|
observerLayer = SessionEventObserver.FromHandler((event) => Effect.tryPromise(() => this.fanoutPublishedEvent(event)).pipe(Effect.catchCause((cause) => Effect.logError("EDA WebSocket event observer recovered from a host failure", { cause }))));
|
|
1012
1527
|
sockets = /* @__PURE__ */ new Map();
|
|
1528
|
+
transcript;
|
|
1013
1529
|
constructor(options) {
|
|
1014
1530
|
this.options = options;
|
|
1531
|
+
this.transcript = options.transcript === void 0 ? void 0 : new EDATranscriptSockets(options.transcript);
|
|
1532
|
+
}
|
|
1533
|
+
/** Whether this host serves transcript-window sockets. */
|
|
1534
|
+
get supportsTranscript() {
|
|
1535
|
+
return this.transcript !== void 0;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Accept a transcript-window socket. Reads are storage-level, so this never prepares the
|
|
1539
|
+
* session runtime. Call immediately after `ctx.acceptWebSocket()`.
|
|
1540
|
+
*/
|
|
1541
|
+
acceptTranscript(input) {
|
|
1542
|
+
if (this.transcript === void 0) {
|
|
1543
|
+
this.close(input.webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "unsupported");
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
this.transcript.accept(input);
|
|
1547
|
+
}
|
|
1548
|
+
/** The session's log was destroyed: close transcript sockets and drop live content. */
|
|
1549
|
+
resetTranscript() {
|
|
1550
|
+
this.transcript?.reset();
|
|
1551
|
+
}
|
|
1552
|
+
/** Wait for scheduled transcript flushes; for orderly shutdown and tests. */
|
|
1553
|
+
async settled() {
|
|
1554
|
+
await this.transcript?.settled();
|
|
1015
1555
|
}
|
|
1016
1556
|
resolveProjection(id) {
|
|
1017
1557
|
if (id === void 0) return void 0;
|
|
@@ -1055,7 +1595,13 @@ var EDAWebSocketConnectionManager = class {
|
|
|
1055
1595
|
}
|
|
1056
1596
|
return;
|
|
1057
1597
|
}
|
|
1058
|
-
const
|
|
1598
|
+
const restored = await this.restore(webSocket);
|
|
1599
|
+
if (restored === "transcript") {
|
|
1600
|
+
await this.transcript?.message(webSocket, message);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (restored === "closed") return;
|
|
1604
|
+
const state = restored;
|
|
1059
1605
|
if (state === void 0) {
|
|
1060
1606
|
this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
|
|
1061
1607
|
return;
|
|
@@ -1074,6 +1620,7 @@ var EDAWebSocketConnectionManager = class {
|
|
|
1074
1620
|
}
|
|
1075
1621
|
closed(webSocket) {
|
|
1076
1622
|
this.sockets.delete(webSocket);
|
|
1623
|
+
this.transcript?.closed(webSocket);
|
|
1077
1624
|
}
|
|
1078
1625
|
async flushDeferredCatchUp(sessionId) {
|
|
1079
1626
|
const deliveries = [];
|
|
@@ -1088,11 +1635,22 @@ var EDAWebSocketConnectionManager = class {
|
|
|
1088
1635
|
await Promise.all(deliveries);
|
|
1089
1636
|
}
|
|
1090
1637
|
async fanoutPublishedEvent(event) {
|
|
1091
|
-
const
|
|
1638
|
+
const heldTranscriptSockets = new Set(this.transcript?.webSockets() ?? []);
|
|
1639
|
+
this.transcript?.onPublished(event, [...heldTranscriptSockets]);
|
|
1640
|
+
const webSockets = /* @__PURE__ */ new Set([
|
|
1641
|
+
...this.options.getWebSockets?.() ?? [],
|
|
1642
|
+
...this.sockets.keys(),
|
|
1643
|
+
...this.transcript?.webSockets() ?? []
|
|
1644
|
+
]);
|
|
1092
1645
|
const deliveries = [];
|
|
1646
|
+
const transcriptSockets = [];
|
|
1093
1647
|
for (const webSocket of webSockets) {
|
|
1094
1648
|
const state = await this.restore(webSocket);
|
|
1095
|
-
if (state ===
|
|
1649
|
+
if (state === "transcript") {
|
|
1650
|
+
transcriptSockets.push(webSocket);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (state === void 0 || state === "closed") continue;
|
|
1096
1654
|
deliveries.push(this.enqueue(webSocket, state, async () => {
|
|
1097
1655
|
if (event.event.durability !== "durable") {
|
|
1098
1656
|
await this.interpret(webSocket, state, onEphemeralEvent(state.delivery, event, Date.now()));
|
|
@@ -1111,16 +1669,28 @@ var EDAWebSocketConnectionManager = class {
|
|
|
1111
1669
|
await this.readAndDeliver(webSocket, state, state.delivery.sentDurableThroughSeq);
|
|
1112
1670
|
}));
|
|
1113
1671
|
}
|
|
1672
|
+
this.transcript?.serveRestored(transcriptSockets.filter((webSocket) => !heldTranscriptSockets.has(webSocket)));
|
|
1114
1673
|
await Promise.all(deliveries);
|
|
1115
1674
|
}
|
|
1116
1675
|
async restore(webSocket) {
|
|
1117
1676
|
const existing = this.sockets.get(webSocket);
|
|
1118
1677
|
if (existing !== void 0) return existing;
|
|
1678
|
+
if (this.transcript?.has(webSocket) === true) return "transcript";
|
|
1119
1679
|
const decoded = await decodeWebSocketAttachment(webSocket.deserializeAttachment());
|
|
1120
1680
|
if (decoded._tag !== "Decoded") {
|
|
1121
1681
|
this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
|
|
1122
1682
|
return;
|
|
1123
1683
|
}
|
|
1684
|
+
const restoredMeanwhile = this.sockets.get(webSocket);
|
|
1685
|
+
if (restoredMeanwhile !== void 0) return restoredMeanwhile;
|
|
1686
|
+
if (this.transcript?.has(webSocket) === true) return "transcript";
|
|
1687
|
+
if (decoded.attachment.kind === "eda-transcript-v1") {
|
|
1688
|
+
if (this.transcript === void 0) {
|
|
1689
|
+
this.close(webSocket, EDA_WS_CLOSE_PROTOCOL_ERROR, "protocol");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
return this.transcript.restore(webSocket, decoded.attachment) ? "transcript" : "closed";
|
|
1693
|
+
}
|
|
1124
1694
|
const attachment = decoded.attachment;
|
|
1125
1695
|
const projectionState = attachment.projection === void 0 ? void 0 : await this.decodeProjectionState(attachment.projection.id, attachment.projection.state);
|
|
1126
1696
|
if (attachment.projection !== void 0 && projectionState === void 0) {
|
|
@@ -1328,7 +1898,8 @@ var EDASessionController = class {
|
|
|
1328
1898
|
"eda.seq.after": afterSeq
|
|
1329
1899
|
})))),
|
|
1330
1900
|
webSocketProjection: options.webSocketProjection,
|
|
1331
|
-
webSocketProtocol: options.webSocketProtocol
|
|
1901
|
+
webSocketProtocol: options.webSocketProtocol,
|
|
1902
|
+
...options.transcriptSockets === void 0 ? {} : { transcript: options.transcriptSockets }
|
|
1332
1903
|
});
|
|
1333
1904
|
this.runtime = new EDASessionRuntime({
|
|
1334
1905
|
...options,
|
|
@@ -1415,6 +1986,21 @@ var EDASessionController = class {
|
|
|
1415
1986
|
acceptPreparedEventWebSocket(input) {
|
|
1416
1987
|
return this.webSockets.accept(input);
|
|
1417
1988
|
}
|
|
1989
|
+
/** Whether transcript-window sockets are served. */
|
|
1990
|
+
get supportsTranscriptWebSocket() {
|
|
1991
|
+
return this.webSockets.supportsTranscript;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Register a transcript-window socket right after the host accepted it. Never prepares the
|
|
1995
|
+
* model runtime: windows are storage-level reads.
|
|
1996
|
+
*/
|
|
1997
|
+
acceptTranscriptWebSocket(input) {
|
|
1998
|
+
this.webSockets.acceptTranscript({
|
|
1999
|
+
sessionId: input.sessionId,
|
|
2000
|
+
trace: input.trace ?? makeRootEDATraceMetadata(),
|
|
2001
|
+
webSocket: input.webSocket
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
1418
2004
|
webSocketMessage(webSocket, message) {
|
|
1419
2005
|
return this.webSockets.message(webSocket, message);
|
|
1420
2006
|
}
|
|
@@ -1426,6 +2012,7 @@ var EDASessionController = class {
|
|
|
1426
2012
|
}
|
|
1427
2013
|
async destroy(_input) {
|
|
1428
2014
|
await this.runtime.destroy();
|
|
2015
|
+
this.webSockets.resetTranscript();
|
|
1429
2016
|
}
|
|
1430
2017
|
async alarm(input) {
|
|
1431
2018
|
if (input !== void 0) await this.prepareSession(input.sessionId);
|
|
@@ -1460,6 +2047,6 @@ const hostSpanOptions = (trace, attributes) => ({
|
|
|
1460
2047
|
...trace.parent === null ? {} : { parent: toEdaExternalSpan(trace.parent) }
|
|
1461
2048
|
});
|
|
1462
2049
|
//#endregion
|
|
1463
|
-
export {
|
|
2050
|
+
export { makeEDADurableObjectRuntimeLayer as i, defaultActiveContentCadenceMs as n, EDAWebSocketProjectionId as r, EDASessionController as t };
|
|
1464
2051
|
|
|
1465
|
-
//# sourceMappingURL=session-controller-
|
|
2052
|
+
//# sourceMappingURL=session-controller-BnNMF12R.js.map
|