@ggui-ai/mcp-server 0.25.0 → 0.27.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/README.md +2 -2
- package/dist/api-renders-routes.d.ts +46 -5
- package/dist/api-renders-routes.d.ts.map +1 -1
- package/dist/api-renders-routes.js +98 -39
- package/dist/api-renders-stream-route.d.ts +2 -1
- package/dist/api-renders-stream-route.d.ts.map +1 -1
- package/dist/api-renders-stream-route.js +14 -2
- package/dist/build-mcp.d.ts +16 -0
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +48 -20
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/first-seen.d.ts +14 -0
- package/dist/first-seen.d.ts.map +1 -0
- package/dist/first-seen.js +19 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
- package/dist/ggui-session-channel/action-ingress.js +96 -8
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
- package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
- package/dist/ggui-session-channel/internal-types.d.ts +20 -12
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
- package/dist/ggui-session-channel/outbound.js +18 -6
- package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
- package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
- package/dist/ggui-session-channel/stream-epoch.js +47 -0
- package/dist/ggui-session-channel/subscribe.d.ts +29 -17
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscribe.js +247 -124
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
- package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
- package/dist/ggui-session-channel.d.ts +13 -5
- package/dist/ggui-session-channel.d.ts.map +1 -1
- package/dist/ggui-session-channel.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/instructions-presets.d.ts +22 -21
- package/dist/instructions-presets.d.ts.map +1 -1
- package/dist/instructions-presets.js +38 -38
- package/dist/llm-backed-negotiator.d.ts +1 -36
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +0 -196
- package/dist/mcp-apps-outbound.d.ts +39 -90
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +45 -3
- package/dist/mcp-endpoint-routes.d.ts.map +1 -1
- package/dist/mcp-endpoint-routes.js +4 -1
- package/dist/server.d.ts +108 -33
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +181 -48
- package/dist/storage.d.ts +9 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +4 -3
- package/dist/view-proof-gate.d.ts +96 -0
- package/dist/view-proof-gate.d.ts.map +1 -0
- package/dist/view-proof-gate.js +167 -0
- package/package.json +14 -15
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createFirstSeen(capacity) {
|
|
2
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
3
|
+
throw new RangeError(`capacity must be a positive integer, got ${capacity}`);
|
|
4
|
+
}
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
return {
|
|
7
|
+
first(key) {
|
|
8
|
+
if (seen.has(key))
|
|
9
|
+
return false;
|
|
10
|
+
if (seen.size >= capacity) {
|
|
11
|
+
const oldest = seen.values().next();
|
|
12
|
+
if (!oldest.done)
|
|
13
|
+
seen.delete(oldest.value);
|
|
14
|
+
}
|
|
15
|
+
seen.add(key);
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"action-ingress.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/action-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAEhB,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AA+G9C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,mBAAmB,CACjB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CA6O1E"}
|
|
@@ -177,13 +177,63 @@ export function createActionIngress(deps) {
|
|
|
177
177
|
// reaped) degrades to ledger-only with a warn — the WS client has
|
|
178
178
|
// no `ui/message` fallback to branch on, so a new error frame
|
|
179
179
|
// would be vocabulary without a consumer.
|
|
180
|
+
//
|
|
181
|
+
// The append's outcome (ggui#1517) is not read here: this path mints
|
|
182
|
+
// the pipe id per frame (`toConsumeEventEntry`), so a duplicate cannot
|
|
183
|
+
// arise, and the ledger write is this path's load-bearing one.
|
|
184
|
+
// The pipe entry (and its minted actionId, which also names the holder of
|
|
185
|
+
// a oneShot spend, ggui#1424) is built once per frame.
|
|
186
|
+
const entry = envelope.type === "data:submit" ? toConsumeEventEntry(envelope, sub.sessionId) : null;
|
|
187
|
+
// ggui#1424 — the channel door refuses a second gesture on a spent
|
|
188
|
+
// oneShot too, so the server's refusal holds at both doors. The claim is
|
|
189
|
+
// taken BEFORE the writes under this frame's minted actionId; the answer
|
|
190
|
+
// decides as it does on the tool door (`claimDispatchSpend` there):
|
|
191
|
+
// already-spent by another holder whose delivery is marked → refused with
|
|
192
|
+
// the channel's existing CONTRACT_VIOLATION error frame (#1358), one
|
|
193
|
+
// violation at `actionSpec.<name>.oneShot`, no ledger row, no pipe entry,
|
|
194
|
+
// one named line; an unmarked holder is taken over; the same holder, no
|
|
195
|
+
// holder, a non-oneShot action, a store that answers no claim, or a store
|
|
196
|
+
// failure (fail-open, named) → the frame goes on as before. The store
|
|
197
|
+
// enforces `delivered`: the refusal arm reads it only as the store
|
|
198
|
+
// answers it. Delivery is marked after the writes below.
|
|
199
|
+
const framePayload = envelope.payload;
|
|
200
|
+
const oneShotPayload = entry !== null &&
|
|
201
|
+
activeItem !== undefined &&
|
|
202
|
+
framePayload !== null &&
|
|
203
|
+
typeof framePayload === "object" &&
|
|
204
|
+
!Array.isArray(framePayload) &&
|
|
205
|
+
typeof framePayload.action === "string"
|
|
206
|
+
? { action: framePayload.action, data: framePayload.data }
|
|
207
|
+
: null;
|
|
208
|
+
if (oneShotPayload !== null && entry !== null && activeItem !== undefined) {
|
|
209
|
+
const verdict = await claimChannelSpend(deps, sub.sessionId, activeItem, oneShotPayload, entry.actionId);
|
|
210
|
+
if (verdict.kind === "refuse") {
|
|
211
|
+
deps.logger.warn("render_channel_one_shot_refused", {
|
|
212
|
+
sessionId: sub.sessionId,
|
|
213
|
+
action: oneShotPayload.action,
|
|
214
|
+
actionId: entry.actionId,
|
|
215
|
+
by: verdict.by,
|
|
216
|
+
});
|
|
217
|
+
deps.sendError(ws, "CONTRACT_VIOLATION", `actionSpec.${oneShotPayload.action} is declared oneShot and already fired on this card; a second gesture is refused`, message.requestId, {
|
|
218
|
+
error: "contract_violation",
|
|
219
|
+
violations: [
|
|
220
|
+
{
|
|
221
|
+
field: `actionSpec.${oneShotPayload.action}.oneShot`,
|
|
222
|
+
keyword: "oneShot",
|
|
223
|
+
message: "this one-shot action already fired on this card",
|
|
224
|
+
expected: verdict.by,
|
|
225
|
+
received: entry.actionId,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
hint: "The action already fired on this card. Render a new card to fire it again.",
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
180
233
|
const consumeWrite = (() => {
|
|
181
|
-
if (deps.pendingEventConsumer === undefined ||
|
|
234
|
+
if (deps.pendingEventConsumer === undefined || entry === null) {
|
|
182
235
|
return Promise.resolve();
|
|
183
236
|
}
|
|
184
|
-
const entry = toConsumeEventEntry(envelope, sub.sessionId);
|
|
185
|
-
if (entry === null)
|
|
186
|
-
return Promise.resolve();
|
|
187
237
|
return deps.pendingEventConsumer.append(sub.sessionId, {
|
|
188
238
|
// The pipe entry's stable id doubles as the `drain_ack` key —
|
|
189
239
|
// same convention as the relay path's iframe-supplied id.
|
|
@@ -218,10 +268,12 @@ export function createActionIngress(deps) {
|
|
|
218
268
|
return;
|
|
219
269
|
}
|
|
220
270
|
const seq = ledgerResult.value;
|
|
221
|
-
// ggui#1223 / #1305 — the gesture is in the ledger, so
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
// tool path: a failed record is named
|
|
271
|
+
// ggui#1223 / #1305 / #1424 — the gesture is in the ledger, so the
|
|
272
|
+
// holder MARKS its spend delivered (claimed above, before the writes),
|
|
273
|
+
// BEFORE the ack: a client that holds the ack can reload and find the
|
|
274
|
+
// card spent. FAIL-OPEN, as on the tool path: a failed record is named
|
|
275
|
+
// and never withholds the ack. With no pipe entry (the frame did not
|
|
276
|
+
// parse as a dispatch) the spend is recorded held by nobody, as before.
|
|
225
277
|
if (envelope.type === "data:submit" && activeItem !== undefined) {
|
|
226
278
|
const payload = envelope.payload;
|
|
227
279
|
if (payload !== null &&
|
|
@@ -235,6 +287,7 @@ export function createActionIngress(deps) {
|
|
|
235
287
|
render: activeItem,
|
|
236
288
|
action: payload.action,
|
|
237
289
|
data: payload.data,
|
|
290
|
+
...(entry !== null ? { actionId: entry.actionId, delivered: true } : {}),
|
|
238
291
|
});
|
|
239
292
|
}
|
|
240
293
|
catch (err) {
|
|
@@ -254,3 +307,38 @@ export function createActionIngress(deps) {
|
|
|
254
307
|
}
|
|
255
308
|
return { handleInboundAction };
|
|
256
309
|
}
|
|
310
|
+
async function claimChannelSpend(deps, sessionId, render, payload, actionId) {
|
|
311
|
+
const claimOnce = async (spend) => {
|
|
312
|
+
try {
|
|
313
|
+
return await recordCommittedOneShot({
|
|
314
|
+
store: deps.renderStore,
|
|
315
|
+
sessionId,
|
|
316
|
+
render,
|
|
317
|
+
action: payload.action,
|
|
318
|
+
data: payload.data,
|
|
319
|
+
...spend,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
deps.logger.warn("render_channel_spent_oneshot_persist_failed", {
|
|
324
|
+
sessionId,
|
|
325
|
+
action: payload.action,
|
|
326
|
+
error: err instanceof Error ? err.message : String(err),
|
|
327
|
+
});
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const first = await claimOnce({ actionId });
|
|
332
|
+
const claim = first?.claim;
|
|
333
|
+
if (claim === undefined || claim.outcome !== "already-spent" || claim.by === undefined || claim.by === actionId) {
|
|
334
|
+
return { kind: "proceed" };
|
|
335
|
+
}
|
|
336
|
+
if (claim.delivered === true)
|
|
337
|
+
return { kind: "refuse", by: claim.by };
|
|
338
|
+
const retaken = await claimOnce({ actionId, reclaimFrom: claim.by });
|
|
339
|
+
const second = retaken?.claim;
|
|
340
|
+
if (second === undefined || second.outcome !== "already-spent" || second.by === undefined || second.by === actionId) {
|
|
341
|
+
return { kind: "proceed" };
|
|
342
|
+
}
|
|
343
|
+
return { kind: "refuse", by: second.by };
|
|
344
|
+
}
|
|
@@ -69,7 +69,6 @@ export interface ChannelSubscriptionsDeps {
|
|
|
69
69
|
* subscriber.
|
|
70
70
|
*/
|
|
71
71
|
readonly subscribersByWs: WeakMap<WebSocket, WsSubscriber>;
|
|
72
|
-
readonly send: Outbound["send"];
|
|
73
72
|
readonly sendChannelError: Outbound["sendChannelError"];
|
|
74
73
|
}
|
|
75
74
|
export interface ChannelSubscriptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAA4B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAa9C;;;;;;;GAOG;AACH,MAAM,WAAW,mCAAmC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,mCAAmC,GAAG,SAAS,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"channel-subscriptions.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/channel-subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAA4B,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAa9C;;;;;;;GAOG;AACH,MAAM,WAAW,mCAAmC;IAClD;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,mCAAmC,GAAG,SAAS,CAAC;IACrE;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC3D,QAAQ,CAAC,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,sBAAsB,CACpB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;OAKG;IACH,wBAAwB,CACtB,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,qBAAqB,CAAA;KAAE,GAC1D,IAAI,CAAC;CACT;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAoQ/F"}
|
|
@@ -68,7 +68,10 @@ export function createChannelSubscriptions(deps) {
|
|
|
68
68
|
if (sub.ws.readyState !== sub.ws.OPEN)
|
|
69
69
|
return;
|
|
70
70
|
state.seq += 1;
|
|
71
|
-
|
|
71
|
+
// Through the subscriber's sink, like every data-carrying writer,
|
|
72
|
+
// so a poll that lands while the subscribe tail is still writing
|
|
73
|
+
// its replay is held until after it (ggui#1525).
|
|
74
|
+
sub.sink.write({
|
|
72
75
|
type: "channel_payload",
|
|
73
76
|
payload: {
|
|
74
77
|
sessionId: sub.sessionId,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* of the package's public surface — everything publishable is exported
|
|
4
4
|
* (or re-exported) from `../ggui-session-channel.ts`.
|
|
5
5
|
*/
|
|
6
|
+
import type { StreamReplayCursor } from './stream-epoch.js';
|
|
6
7
|
import type { AuthResult, BufferedStreamEnvelope } from "@ggui-ai/mcp-server-core";
|
|
7
8
|
import type { JsonObject } from "@ggui-ai/protocol";
|
|
8
9
|
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
@@ -30,12 +31,16 @@ export interface SubscriberSink {
|
|
|
30
31
|
}): void;
|
|
31
32
|
/**
|
|
32
33
|
* Terminate the transport. WS maps `service_restart` → close 1012
|
|
33
|
-
* (reconnect immediately — same server family is still up)
|
|
34
|
+
* (reconnect immediately — same server family is still up),
|
|
34
35
|
* `session_expired` → close 1008 (policy violation — do not blind-
|
|
35
|
-
* reconnect)
|
|
36
|
-
*
|
|
36
|
+
* reconnect), and `internal_error` → close 1011 (the server could not
|
|
37
|
+
* complete the subscribe; a client reconnects with its ordinary
|
|
38
|
+
* backoff, since a failure that may repeat must not invite an
|
|
39
|
+
* immediate retry, ggui#1528). SSE ends the response for every reason,
|
|
40
|
+
* letting `EventSource` auto-reconnect hit the HTTP pre-gate for the
|
|
41
|
+
* authoritative verdict.
|
|
37
42
|
*/
|
|
38
|
-
end(reason: "service_restart" | "session_expired"): void;
|
|
43
|
+
end(reason: "service_restart" | "session_expired" | "internal_error"): void;
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
46
|
* Fields shared by every subscriber regardless of transport (one
|
|
@@ -48,16 +53,19 @@ interface SubscriberBase {
|
|
|
48
53
|
readonly identity: AuthResult;
|
|
49
54
|
readonly connectedAt: number;
|
|
50
55
|
/**
|
|
51
|
-
*
|
|
52
|
-
* covered for this subscriber
|
|
53
|
-
* `seq
|
|
54
|
-
*
|
|
56
|
+
* The replay boundary: the largest outbound `seq` the initial replay
|
|
57
|
+
* (or subscribe snapshot) covered for this subscriber, and the epoch
|
|
58
|
+
* that `seq` counts in (ggui#1531). Live fan-out skips envelopes of the
|
|
59
|
+
* same epoch with `seq <= replayCursor.seq` to prevent double delivery,
|
|
60
|
+
* since those were (or will be) delivered via the replay phase. A live
|
|
61
|
+
* envelope of another epoch means the counter restarted: it is
|
|
62
|
+
* delivered and moves the cursor (`admitLiveEnvelope`).
|
|
55
63
|
*
|
|
56
|
-
* For fresh subscribers (no `fromSeq`),
|
|
57
|
-
*
|
|
58
|
-
*
|
|
64
|
+
* For fresh subscribers (no `fromSeq`), `seq` is the stream cursor at
|
|
65
|
+
* subscribe time; they never see the pre-existing buffer, only new
|
|
66
|
+
* deliveries.
|
|
59
67
|
*/
|
|
60
|
-
readonly
|
|
68
|
+
readonly replayCursor: StreamReplayCursor;
|
|
61
69
|
/**
|
|
62
70
|
* Per-subscriber live-tail iterator from `streamFanout.subscribe`.
|
|
63
71
|
* Owned by the subscriber for its full lifetime; ending it (via
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E
|
|
1
|
+
{"version":3,"file":"internal-types.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/internal-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,MAAM,IAAI,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E;;;;;;;;;;OAUG;IACH,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,CAAC;CAC7E;AAED;;;;GAIG;AACH,UAAU,cAAc;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,sBAAsB,CAAC,CAAC;IACrD;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;IAC5D,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;CAC3B;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,wBAAwB;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,iBAAiB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* `GguiSessionChannelServer` interface in `../ggui-session-channel.ts`;
|
|
12
12
|
* inline comments here cover impl-level decisions only.
|
|
13
13
|
*/
|
|
14
|
+
import { admitLiveEnvelope } from "./stream-epoch.js";
|
|
14
15
|
import { assertStreamContract } from "@ggui-ai/mcp-server-handlers/renders";
|
|
15
16
|
/**
|
|
16
17
|
* Low-level ws wire write shared by the closure-bound `send` helper and
|
|
@@ -43,7 +44,7 @@ export function createWsSink(ws, logger) {
|
|
|
43
44
|
},
|
|
44
45
|
end: (reason) => {
|
|
45
46
|
try {
|
|
46
|
-
ws.close(reason === "service_restart" ? 1012 : 1008, reason);
|
|
47
|
+
ws.close(reason === "service_restart" ? 1012 : reason === "internal_error" ? 1011 : 1008, reason);
|
|
47
48
|
}
|
|
48
49
|
catch {
|
|
49
50
|
/* best-effort — socket may already be closing */
|
|
@@ -204,11 +205,22 @@ export function createOutbound(deps) {
|
|
|
204
205
|
// subscribe-replay (policy 'all'/'latest') or is deliberately
|
|
205
206
|
// invisible to late subscribers (policy 'none') — never re-send.
|
|
206
207
|
// Unstamped frames were never buffered; replay cannot duplicate
|
|
207
|
-
// them, so they pass.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// them, so they pass. A frame of another epoch means the counter
|
|
209
|
+
// restarted: it passes and moves the subscriber's cursor, by the
|
|
210
|
+
// same rule as the pump (ggui#1531). Non-data frames carry no seq
|
|
211
|
+
// semantics.
|
|
212
|
+
if (frame.type === "data") {
|
|
213
|
+
const admission = admitLiveEnvelope(sub.replayCursor, frame.payload);
|
|
214
|
+
if (admission.epochChanged !== undefined) {
|
|
215
|
+
deps.logger.info("stream_epoch_changed", {
|
|
216
|
+
sessionId,
|
|
217
|
+
from: admission.epochChanged.from,
|
|
218
|
+
to: admission.epochChanged.to,
|
|
219
|
+
transport: sub.transport,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (!admission.deliver)
|
|
223
|
+
continue;
|
|
212
224
|
}
|
|
213
225
|
sub.sink.write(frame);
|
|
214
226
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live tail's replay boundary, with the stream counter's epoch
|
|
3
|
+
* (ggui#1531). One rule, shared by the two places a live data frame is
|
|
4
|
+
* filtered against a subscriber's replay: the in-process pump
|
|
5
|
+
* (`subscriber-lifecycle.ts`) and the cross-replica direct walk
|
|
6
|
+
* (`outbound.ts`, `externalBroadcast`).
|
|
7
|
+
*
|
|
8
|
+
* A `seq` counts within one counter. When the counter restarts while a
|
|
9
|
+
* subscriber stays attached (a hosted counter evicted or expired, a
|
|
10
|
+
* server restarted over a persistent session store), the next frame
|
|
11
|
+
* arrives at a low `seq` with a new epoch. Filtering it by the old
|
|
12
|
+
* boundary would drop it, and every later one, with no signal: the
|
|
13
|
+
* frame of another epoch is delivered instead, and the subscriber moves
|
|
14
|
+
* to the new epoch with an empty boundary.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* A subscriber's replay boundary: the latest `seq` its snapshot and
|
|
18
|
+
* replay covered, and the epoch that `seq` counts in (`undefined` when
|
|
19
|
+
* the snapshot had none: no counter yet, or a buffer that predates
|
|
20
|
+
* epochs). Mutable: a live frame of another epoch moves it.
|
|
21
|
+
*/
|
|
22
|
+
export interface StreamReplayCursor {
|
|
23
|
+
seq: number;
|
|
24
|
+
epoch: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The epochs this cursor has moved away from, newest last, at most
|
|
27
|
+
* {@link RETIRED_EPOCHS_KEPT}. A late frame of one of them (a
|
|
28
|
+
* cross-replica frame published just before the restart) is dropped, so
|
|
29
|
+
* it cannot move the cursor back and empty its boundary again.
|
|
30
|
+
*/
|
|
31
|
+
retired?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** How many epochs a cursor remembers having left. */
|
|
34
|
+
export declare const RETIRED_EPOCHS_KEPT = 4;
|
|
35
|
+
/** What to do with one live data frame for one subscriber. */
|
|
36
|
+
export interface LiveAdmission {
|
|
37
|
+
readonly deliver: boolean;
|
|
38
|
+
/** Set when the frame moved the subscriber to another epoch. */
|
|
39
|
+
readonly epochChanged?: {
|
|
40
|
+
readonly from: string;
|
|
41
|
+
readonly to: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decide whether a live data frame reaches a subscriber, and move its
|
|
46
|
+
* cursor when the frame's epoch differs:
|
|
47
|
+
* - an unstamped frame (no `seq`) was never buffered and always passes;
|
|
48
|
+
* - a late frame of an epoch the cursor has left is dropped;
|
|
49
|
+
* - a frame of another epoch than the cursor's is delivered, and the
|
|
50
|
+
* cursor moves to that epoch with an empty boundary (a restart);
|
|
51
|
+
* - the first epoch a cursor without one sees is adopted, and its seq
|
|
52
|
+
* boundary stands (the snapshot simply had no epoch yet);
|
|
53
|
+
* - otherwise a frame at or below the boundary was, or will be, sent
|
|
54
|
+
* by the replay, and is skipped.
|
|
55
|
+
* An absent epoch on the frame is unknown, never a mismatch.
|
|
56
|
+
*/
|
|
57
|
+
export declare function admitLiveEnvelope(cursor: StreamReplayCursor, envelope: {
|
|
58
|
+
readonly seq?: number;
|
|
59
|
+
readonly streamEpoch?: string;
|
|
60
|
+
}): LiveAdmission;
|
|
61
|
+
//# sourceMappingURL=stream-epoch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-epoch.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/stream-epoch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CACxE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE,aAAa,CAcf"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live tail's replay boundary, with the stream counter's epoch
|
|
3
|
+
* (ggui#1531). One rule, shared by the two places a live data frame is
|
|
4
|
+
* filtered against a subscriber's replay: the in-process pump
|
|
5
|
+
* (`subscriber-lifecycle.ts`) and the cross-replica direct walk
|
|
6
|
+
* (`outbound.ts`, `externalBroadcast`).
|
|
7
|
+
*
|
|
8
|
+
* A `seq` counts within one counter. When the counter restarts while a
|
|
9
|
+
* subscriber stays attached (a hosted counter evicted or expired, a
|
|
10
|
+
* server restarted over a persistent session store), the next frame
|
|
11
|
+
* arrives at a low `seq` with a new epoch. Filtering it by the old
|
|
12
|
+
* boundary would drop it, and every later one, with no signal: the
|
|
13
|
+
* frame of another epoch is delivered instead, and the subscriber moves
|
|
14
|
+
* to the new epoch with an empty boundary.
|
|
15
|
+
*/
|
|
16
|
+
/** How many epochs a cursor remembers having left. */
|
|
17
|
+
export const RETIRED_EPOCHS_KEPT = 4;
|
|
18
|
+
/**
|
|
19
|
+
* Decide whether a live data frame reaches a subscriber, and move its
|
|
20
|
+
* cursor when the frame's epoch differs:
|
|
21
|
+
* - an unstamped frame (no `seq`) was never buffered and always passes;
|
|
22
|
+
* - a late frame of an epoch the cursor has left is dropped;
|
|
23
|
+
* - a frame of another epoch than the cursor's is delivered, and the
|
|
24
|
+
* cursor moves to that epoch with an empty boundary (a restart);
|
|
25
|
+
* - the first epoch a cursor without one sees is adopted, and its seq
|
|
26
|
+
* boundary stands (the snapshot simply had no epoch yet);
|
|
27
|
+
* - otherwise a frame at or below the boundary was, or will be, sent
|
|
28
|
+
* by the replay, and is skipped.
|
|
29
|
+
* An absent epoch on the frame is unknown, never a mismatch.
|
|
30
|
+
*/
|
|
31
|
+
export function admitLiveEnvelope(cursor, envelope) {
|
|
32
|
+
if (envelope.seq === undefined)
|
|
33
|
+
return { deliver: true };
|
|
34
|
+
const epoch = envelope.streamEpoch;
|
|
35
|
+
if (epoch !== undefined && cursor.retired?.includes(epoch) === true)
|
|
36
|
+
return { deliver: false };
|
|
37
|
+
if (epoch !== undefined && epoch !== cursor.epoch) {
|
|
38
|
+
const from = cursor.epoch;
|
|
39
|
+
cursor.epoch = epoch;
|
|
40
|
+
if (from !== undefined) {
|
|
41
|
+
cursor.retired = [...(cursor.retired ?? []), from].slice(-RETIRED_EPOCHS_KEPT);
|
|
42
|
+
cursor.seq = 0;
|
|
43
|
+
return { deliver: true, epochChanged: { from, to: epoch } };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { deliver: envelope.seq > cursor.seq };
|
|
47
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Subscribe / credential-path family for the live channel — upgrade-
|
|
3
3
|
* time identity resolution across the three auth planes (bearer via
|
|
4
4
|
* the AuthAdapter, ws-token bootstrap, console cookie) and the
|
|
5
|
-
* `subscribe` handler itself: version handshake, bootstrap verify
|
|
6
|
-
*
|
|
5
|
+
* `subscribe` handler itself: version handshake, bootstrap verify,
|
|
6
|
+
* identity-default appId resolution,
|
|
7
7
|
* dev-mode render provisioning, replay, registration, and the ack.
|
|
8
8
|
*/
|
|
9
9
|
import type { AuthAdapter, AuthResult, GguiSessionStore, GguiSessionStreamBuffer, StoredGguiSession, StreamFanout } from "@ggui-ai/mcp-server-core";
|
|
@@ -25,10 +25,11 @@ import type { SubscriberLifecycle } from "./subscriber-lifecycle.js";
|
|
|
25
25
|
* (invalid sig, expired, wrong kind, replayed, etc.).
|
|
26
26
|
* 2. The bound `sessionId` MUST match the one on the subscribe
|
|
27
27
|
* payload. Mismatches are rejected with a clean error.
|
|
28
|
-
* 3. On success, the
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
28
|
+
* 3. On success, the subscribe binds to that identity. No reconnect
|
|
29
|
+
* credential is minted (ggui#1488): a reconnect presents a live
|
|
30
|
+
* `wsToken` again, renewed before it expires through
|
|
31
|
+
* `GET /api/sessions/:id/state`, or at any age through
|
|
32
|
+
* `ggui_runtime_refresh_ws_token`.
|
|
32
33
|
*
|
|
33
34
|
* Bootstrap auth is MUTUALLY EXCLUSIVE with the upstream `AuthAdapter`
|
|
34
35
|
* bearer path at subscribe time — when a bootstrap token is present,
|
|
@@ -67,12 +68,6 @@ export interface GguiSessionChannelBootstrap {
|
|
|
67
68
|
* kind failures.
|
|
68
69
|
*/
|
|
69
70
|
verify(token: string): GguiSessionChannelBootstrapVerifyResult;
|
|
70
|
-
/**
|
|
71
|
-
* Mint a longer-lived reconnect credential to return in
|
|
72
|
-
* `AckPayload.sessionToken`. Called only after a successful
|
|
73
|
-
* `verify()` on a bootstrap subscribe.
|
|
74
|
-
*/
|
|
75
|
-
issueSessionToken(sessionId: string, appId: string): string;
|
|
76
71
|
}
|
|
77
72
|
/**
|
|
78
73
|
* Cookie-based authentication for the live-channel upgrade. Used
|
|
@@ -128,6 +123,12 @@ export interface SubscribeDeps {
|
|
|
128
123
|
* `/api/sessions/:sessionId/stream`) runs BEFORE this; the tail trusts
|
|
129
124
|
* `stored` + `identity` as already-authorized.
|
|
130
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* A subscribe's credential when it is not the auth adapter's bearer
|
|
128
|
+
* (ggui#1488): a `wsToken`, or the console cookie. Logged as
|
|
129
|
+
* `render_channel_subscribed.source`.
|
|
130
|
+
*/
|
|
131
|
+
export type SubscribeCredential = "ws_token" | "console_cookie";
|
|
131
132
|
export type CompleteSubscribeArgs = {
|
|
132
133
|
/** Resolved render row the subscriber binds to. */
|
|
133
134
|
readonly stored: StoredGguiSession;
|
|
@@ -139,10 +140,17 @@ export type CompleteSubscribeArgs = {
|
|
|
139
140
|
readonly sinceSequence?: number;
|
|
140
141
|
/** Stream-buffer cursor — replays `data` frames per policy when set. */
|
|
141
142
|
readonly fromSeq?: number;
|
|
143
|
+
/** The epoch `fromSeq` was counted in (ggui#1531); read only beside it. */
|
|
144
|
+
readonly fromEpoch?: string;
|
|
142
145
|
/** Correlates the ack / error frames to a WS request. SSE has none. */
|
|
143
146
|
readonly requestId?: string;
|
|
144
|
-
/**
|
|
145
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The credential the subscribe came on, when it is not the auth
|
|
149
|
+
* adapter's bearer: a `wsToken` (the bootstrap path, on the WebSocket or
|
|
150
|
+
* the SSE stream) or the console cookie. Both resolve to a synthetic
|
|
151
|
+
* identity whose own `source` would misreport them.
|
|
152
|
+
*/
|
|
153
|
+
readonly credential?: SubscribeCredential;
|
|
146
154
|
} & ({
|
|
147
155
|
readonly transport: "ws";
|
|
148
156
|
/** The socket — kept on the subscriber row for inbound dispatch. */
|
|
@@ -168,9 +176,13 @@ export interface SubscribeHandlers {
|
|
|
168
176
|
/**
|
|
169
177
|
* The transport-neutral subscribe tail, order preserved exactly:
|
|
170
178
|
* stream-cursor snapshot → stream-buffer replay read → StreamFanout
|
|
171
|
-
* iterator → register (pump starts
|
|
172
|
-
*
|
|
173
|
-
*
|
|
179
|
+
* iterator → register (pump starts; every write to the subscriber's
|
|
180
|
+
* sink is held until the replay below is written, ggui#1525) → the
|
|
181
|
+
* first GguiSessionEvent-ledger page read, when `sinceSequence` is set
|
|
182
|
+
* (before the ack, so a failed read sends none, ggui#1528) → ack →
|
|
183
|
+
* ledger replay (`render_event` frames, `resumeId` = ledger seq; later
|
|
184
|
+
* pages read as they go) → stream-replay `data` frames → release of the
|
|
185
|
+
* held frames. WS `handleSubscribe` calls this after
|
|
174
186
|
* its auth/provision head; SSE attaches call it via
|
|
175
187
|
* `GguiSessionChannelServer.attachExternalSubscriber`.
|
|
176
188
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/subscribe.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EAEb,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAMpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,uCAAuC,GAC/C;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnE,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,uCAAuC,CAAC;CAChE;AAED;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,WAAW,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5E;;;;OAIG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1E;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,sEAAsE;IACtE,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC;IAC/C,mEAAmE;IACnE,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC;IACnD,oFAAoF;IACpF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC5D,iGAAiG;IACjG,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,iGAAiG;IACjG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC7D,gFAAgF;IAChF,QAAQ,CAAC,aAAa,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;CACxD;AAED;;;;;;GAMG;AACH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAC;CAC3C,GAAG,CACA;IACE,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;CACxB,GACD;IAAE,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAA;CAAE,CAChC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,0BAA0B,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtE,iEAAiE;IACjE,eAAe,CACb,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,WAAW,CAAA;KAAE,EACjD,WAAW,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACnE,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAC;CAC1F;AAwED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,GAAG,iBAAiB,CAyoB9E"}
|