@immediately-run/sdk 0.45.3 → 0.46.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.
Files changed (59) hide show
  1. package/dist/hostAttention.cjs +57 -0
  2. package/dist/hostAttention.cjs.map +1 -0
  3. package/dist/hostAttention.d.cts +39 -0
  4. package/dist/hostAttention.d.ts +39 -0
  5. package/dist/hostAttention.js +31 -0
  6. package/dist/hostAttention.js.map +1 -0
  7. package/dist/hostTransport.cjs +55 -0
  8. package/dist/hostTransport.cjs.map +1 -0
  9. package/dist/hostTransport.d.cts +12 -0
  10. package/dist/hostTransport.d.ts +12 -0
  11. package/dist/hostTransport.js +30 -0
  12. package/dist/hostTransport.js.map +1 -0
  13. package/dist/index.cjs +4 -0
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +3 -1
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/linkSpace.cjs +11 -2
  20. package/dist/linkSpace.cjs.map +1 -1
  21. package/dist/linkSpace.d.cts +23 -0
  22. package/dist/linkSpace.d.ts +23 -0
  23. package/dist/linkSpace.js +11 -2
  24. package/dist/linkSpace.js.map +1 -1
  25. package/dist/llm.cjs +18 -3
  26. package/dist/llm.cjs.map +1 -1
  27. package/dist/llm.d.cts +38 -3
  28. package/dist/llm.d.ts +38 -3
  29. package/dist/llm.js +14 -2
  30. package/dist/llm.js.map +1 -1
  31. package/dist/protocolDeadline.cjs +205 -0
  32. package/dist/protocolDeadline.cjs.map +1 -0
  33. package/dist/protocolDeadline.d.cts +146 -0
  34. package/dist/protocolDeadline.d.ts +146 -0
  35. package/dist/protocolDeadline.js +168 -0
  36. package/dist/protocolDeadline.js.map +1 -0
  37. package/dist/protocolStream.cjs +60 -6
  38. package/dist/protocolStream.cjs.map +1 -1
  39. package/dist/protocolStream.d.cts +9 -2
  40. package/dist/protocolStream.d.ts +9 -2
  41. package/dist/protocolStream.js +67 -6
  42. package/dist/protocolStream.js.map +1 -1
  43. package/dist/pushChannel.cjs +13 -9
  44. package/dist/pushChannel.cjs.map +1 -1
  45. package/dist/pushChannel.js +12 -8
  46. package/dist/pushChannel.js.map +1 -1
  47. package/dist/sandboxUtils.cjs +74 -24
  48. package/dist/sandboxUtils.cjs.map +1 -1
  49. package/dist/sandboxUtils.d.cts +37 -4
  50. package/dist/sandboxUtils.d.ts +37 -4
  51. package/dist/sandboxUtils.js +79 -22
  52. package/dist/sandboxUtils.js.map +1 -1
  53. package/dist/version.cjs +1 -1
  54. package/dist/version.cjs.map +1 -1
  55. package/dist/version.d.cts +1 -1
  56. package/dist/version.d.ts +1 -1
  57. package/dist/version.js +1 -1
  58. package/dist/version.js.map +1 -1
  59. package/package.json +2 -2
@@ -24,6 +24,7 @@ __export(protocolStream_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(protocolStream_exports);
26
26
  var import_sandboxUtils = require("./sandboxUtils");
27
+ var import_protocolDeadline = require("./protocolDeadline");
27
28
  class StreamError extends Error {
28
29
  constructor(code, message) {
29
30
  super(message);
@@ -36,7 +37,7 @@ const nextMsgId = () => {
36
37
  streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;
37
38
  return streamCounter;
38
39
  };
39
- async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal) {
40
+ async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal, opts) {
40
41
  const queue = [];
41
42
  let wake = null;
42
43
  let settled = false;
@@ -57,6 +58,52 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
57
58
  w?.();
58
59
  };
59
60
  if (signal) signal.addEventListener("abort", onAbort);
61
+ const scheme = type.startsWith("protocol-") ? type.slice("protocol-".length) : type;
62
+ const call = `${scheme}:${method}`;
63
+ const attendance = (0, import_protocolDeadline.attendanceOf)(scheme, method);
64
+ const firstFrameMs = opts?.timeoutMs ?? (0, import_protocolDeadline.firstFrameTimeoutFor)(scheme, method);
65
+ const idleMs = opts?.idleTimeoutMs ?? import_protocolDeadline.STREAM_IDLE_TIMEOUT_MS;
66
+ let sawFrame = false;
67
+ let noticed = false;
68
+ const awaitFrame = async () => {
69
+ const budget = sawFrame ? idleMs : firstFrameMs;
70
+ if (!Number.isFinite(budget)) {
71
+ await new Promise((resolve) => {
72
+ wake = resolve;
73
+ });
74
+ return;
75
+ }
76
+ const startedAt = Date.now();
77
+ let deadline;
78
+ let notice;
79
+ try {
80
+ await new Promise((resolve, reject) => {
81
+ wake = resolve;
82
+ deadline = setTimeout(
83
+ () => reject(new import_protocolDeadline.ProtocolTimeoutError(call, budget, attendance)),
84
+ budget
85
+ );
86
+ if (opts?.onPending && !noticed) {
87
+ notice = setTimeout(() => {
88
+ noticed = true;
89
+ try {
90
+ opts.onPending?.({
91
+ call,
92
+ attendance,
93
+ elapsedMs: Date.now() - startedAt,
94
+ ...(0, import_protocolDeadline.attendanceReason)(scheme, method) ? { reason: (0, import_protocolDeadline.attendanceReason)(scheme, method) } : {}
95
+ });
96
+ } catch {
97
+ }
98
+ }, import_protocolDeadline.PENDING_NOTICE_MS);
99
+ }
100
+ });
101
+ } finally {
102
+ if (deadline !== void 0) clearTimeout(deadline);
103
+ if (notice !== void 0) clearTimeout(notice);
104
+ wake = null;
105
+ }
106
+ };
60
107
  try {
61
108
  if (signal?.aborted) throw new StreamError("aborted", "stream aborted before start");
62
109
  transport.send({ type, method, params, msgId, stream: true });
@@ -64,11 +111,10 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
64
111
  while (true) {
65
112
  if (signal?.aborted) throw new StreamError("aborted", "stream aborted");
66
113
  if (queue.length === 0) {
67
- await new Promise((resolve) => {
68
- wake = resolve;
69
- });
114
+ await awaitFrame();
70
115
  continue;
71
116
  }
117
+ sawFrame = true;
72
118
  const frame = queue.shift();
73
119
  if (frame.kind === "event") {
74
120
  yield frame.value;
@@ -91,8 +137,16 @@ const bundlerTransport = {
91
137
  subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg)),
92
138
  cancel: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg)
93
139
  };
94
- function protocolStream(protocolName, method, params, signal) {
95
- return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal);
140
+ function protocolStream(protocolName, method, params, signal, opts) {
141
+ return consumeStream(
142
+ bundlerTransport,
143
+ protocolName,
144
+ method,
145
+ params,
146
+ void 0,
147
+ signal,
148
+ opts
149
+ );
96
150
  }
97
151
  // Annotate the CommonJS export names for ESM import in node:
98
152
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AA8BlC,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AAeA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GAC1B,QAC4B;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAIhC,MAAI,UAAU;AAGd,MAAI,UAAU;AACd,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,UAAU,MAAM;AACpB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AACA,MAAI,OAAQ,QAAO,iBAAiB,SAAS,OAAO;AAEpD,MAAI;AACF,QAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,6BAA6B;AACnF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,cAAU;AACV,WAAO,MAAM;AACX,UAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,gBAAgB;AACtE,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,kBAAU;AACV,eAAO,MAAM;AAAA,MACf,OAAO;AACL,kBAAU;AACV,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AACZ,QAAI,OAAQ,QAAO,oBAAoB,SAAS,OAAO;AAGvD,QAAI,WAAW,CAAC,QAAS,WAAU,SAAS,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3E;AACF;AAKA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA,EACrF,QAAQ,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAClF;AASO,SAAS,eACd,cACA,QACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,QAAQ,QAAW,MAAM;AAC9F;","names":[]}
1
+ {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(\n () => reject(new ProtocolTimeoutError(call, budget, attendance)),\n budget,\n );\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(\n bundlerTransport,\n protocolName,\n method,\n params,\n undefined,\n signal,\n opts\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,6 @@
1
+ import { BoundedCallOptions } from './protocolDeadline.cjs';
2
+ import './hostAttention.cjs';
3
+
1
4
  /** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */
2
5
  type StreamFrame = {
3
6
  kind: 'event';
@@ -47,7 +50,9 @@ declare class StreamError extends Error {
47
50
  * it, aborting only stops the app-side iterator while the upstream provider keeps
48
51
  * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).
49
52
  */
50
- declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal): AsyncGenerator<T, R, void>;
53
+ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal, opts?: BoundedCallOptions & {
54
+ idleTimeoutMs?: number;
55
+ }): AsyncGenerator<T, R, void>;
51
56
  /**
52
57
  * Consume an elevated streaming protocol method from app code.
53
58
  *
@@ -55,6 +60,8 @@ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransp
55
60
  *
56
61
  * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.
57
62
  */
58
- declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal): AsyncGenerator<T, R, void>;
63
+ declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal, opts?: BoundedCallOptions & {
64
+ idleTimeoutMs?: number;
65
+ }): AsyncGenerator<T, R, void>;
59
66
 
60
67
  export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };
@@ -1,3 +1,6 @@
1
+ import { BoundedCallOptions } from './protocolDeadline.js';
2
+ import './hostAttention.js';
3
+
1
4
  /** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */
2
5
  type StreamFrame = {
3
6
  kind: 'event';
@@ -47,7 +50,9 @@ declare class StreamError extends Error {
47
50
  * it, aborting only stops the app-side iterator while the upstream provider keeps
48
51
  * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).
49
52
  */
50
- declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal): AsyncGenerator<T, R, void>;
53
+ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal, opts?: BoundedCallOptions & {
54
+ idleTimeoutMs?: number;
55
+ }): AsyncGenerator<T, R, void>;
51
56
  /**
52
57
  * Consume an elevated streaming protocol method from app code.
53
58
  *
@@ -55,6 +60,8 @@ declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransp
55
60
  *
56
61
  * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.
57
62
  */
58
- declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal): AsyncGenerator<T, R, void>;
63
+ declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal, opts?: BoundedCallOptions & {
64
+ idleTimeoutMs?: number;
65
+ }): AsyncGenerator<T, R, void>;
59
66
 
60
67
  export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };
@@ -1,5 +1,13 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { addListener, sendMessage } from "./sandboxUtils";
3
+ import {
4
+ attendanceOf,
5
+ attendanceReason,
6
+ firstFrameTimeoutFor,
7
+ PENDING_NOTICE_MS,
8
+ ProtocolTimeoutError,
9
+ STREAM_IDLE_TIMEOUT_MS
10
+ } from "./protocolDeadline";
3
11
  class StreamError extends Error {
4
12
  constructor(code, message) {
5
13
  super(message);
@@ -12,7 +20,7 @@ const nextMsgId = () => {
12
20
  streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;
13
21
  return streamCounter;
14
22
  };
15
- async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal) {
23
+ async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal, opts) {
16
24
  const queue = [];
17
25
  let wake = null;
18
26
  let settled = false;
@@ -33,6 +41,52 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
33
41
  w?.();
34
42
  };
35
43
  if (signal) signal.addEventListener("abort", onAbort);
44
+ const scheme = type.startsWith("protocol-") ? type.slice("protocol-".length) : type;
45
+ const call = `${scheme}:${method}`;
46
+ const attendance = attendanceOf(scheme, method);
47
+ const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);
48
+ const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;
49
+ let sawFrame = false;
50
+ let noticed = false;
51
+ const awaitFrame = async () => {
52
+ const budget = sawFrame ? idleMs : firstFrameMs;
53
+ if (!Number.isFinite(budget)) {
54
+ await new Promise((resolve) => {
55
+ wake = resolve;
56
+ });
57
+ return;
58
+ }
59
+ const startedAt = Date.now();
60
+ let deadline;
61
+ let notice;
62
+ try {
63
+ await new Promise((resolve, reject) => {
64
+ wake = resolve;
65
+ deadline = setTimeout(
66
+ () => reject(new ProtocolTimeoutError(call, budget, attendance)),
67
+ budget
68
+ );
69
+ if (opts?.onPending && !noticed) {
70
+ notice = setTimeout(() => {
71
+ noticed = true;
72
+ try {
73
+ opts.onPending?.({
74
+ call,
75
+ attendance,
76
+ elapsedMs: Date.now() - startedAt,
77
+ ...attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) } : {}
78
+ });
79
+ } catch {
80
+ }
81
+ }, PENDING_NOTICE_MS);
82
+ }
83
+ });
84
+ } finally {
85
+ if (deadline !== void 0) clearTimeout(deadline);
86
+ if (notice !== void 0) clearTimeout(notice);
87
+ wake = null;
88
+ }
89
+ };
36
90
  try {
37
91
  if (signal?.aborted) throw new StreamError("aborted", "stream aborted before start");
38
92
  transport.send({ type, method, params, msgId, stream: true });
@@ -40,11 +94,10 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
40
94
  while (true) {
41
95
  if (signal?.aborted) throw new StreamError("aborted", "stream aborted");
42
96
  if (queue.length === 0) {
43
- await new Promise((resolve) => {
44
- wake = resolve;
45
- });
97
+ await awaitFrame();
46
98
  continue;
47
99
  }
100
+ sawFrame = true;
48
101
  const frame = queue.shift();
49
102
  if (frame.kind === "event") {
50
103
  yield frame.value;
@@ -67,8 +120,16 @@ const bundlerTransport = {
67
120
  subscribe: (type, handler) => addListener(type, (msg) => handler(msg)),
68
121
  cancel: (msg) => sendMessage(msg.type, msg)
69
122
  };
70
- function protocolStream(protocolName, method, params, signal) {
71
- return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal);
123
+ function protocolStream(protocolName, method, params, signal, opts) {
124
+ return consumeStream(
125
+ bundlerTransport,
126
+ protocolName,
127
+ method,
128
+ params,
129
+ void 0,
130
+ signal,
131
+ opts
132
+ );
72
133
  }
73
134
  export {
74
135
  StreamError,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal);\n}\n"],"mappings":";AAUA,SAAS,aAAa,mBAAmB;AA8BlC,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AAeA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GAC1B,QAC4B;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAIhC,MAAI,UAAU;AAGd,MAAI,UAAU;AACd,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,UAAU,MAAM;AACpB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AACA,MAAI,OAAQ,QAAO,iBAAiB,SAAS,OAAO;AAEpD,MAAI;AACF,QAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,6BAA6B;AACnF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,cAAU;AACV,WAAO,MAAM;AACX,UAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,gBAAgB;AACtE,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,kBAAU;AACV,eAAO,MAAM;AAAA,MACf,OAAO;AACL,kBAAU;AACV,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AACZ,QAAI,OAAQ,QAAO,oBAAoB,SAAS,OAAO;AAGvD,QAAI,WAAW,CAAC,QAAS,WAAU,SAAS,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3E;AACF;AAKA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA,EACrF,QAAQ,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAClF;AASO,SAAS,eACd,cACA,QACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,QAAQ,QAAW,MAAM;AAC9F;","names":[]}
1
+ {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(\n () => reject(new ProtocolTimeoutError(call, budget, attendance)),\n budget,\n );\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(\n bundlerTransport,\n protocolName,\n method,\n params,\n undefined,\n signal,\n opts\n );\n}\n"],"mappings":"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","names":[]}
@@ -22,21 +22,25 @@ __export(pushChannel_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(pushChannel_exports);
24
24
  var import_react = require("react");
25
- var import_sandboxUtils = require("./sandboxUtils");
26
- function createPushChannel(opts, transport = { sendMessage: import_sandboxUtils.sendMessage, addListener: import_sandboxUtils.addListener }) {
25
+ var import_hostTransport = require("./hostTransport");
26
+ function createPushChannel(opts, transport = { sendMessage: import_hostTransport.sendMessage, addListener: import_hostTransport.addListener }) {
27
27
  let current = opts.initial;
28
28
  const listeners = /* @__PURE__ */ new Set();
29
29
  let started = false;
30
30
  const start = () => {
31
31
  if (started) return;
32
+ try {
33
+ transport.addListener(opts.pushType, (msg) => {
34
+ const next = opts.parse(msg);
35
+ if (next !== void 0) {
36
+ current = next;
37
+ listeners.forEach((l) => l(current));
38
+ }
39
+ });
40
+ } catch {
41
+ return;
42
+ }
32
43
  started = true;
33
- transport.addListener(opts.pushType, (msg) => {
34
- const next = opts.parse(msg);
35
- if (next !== void 0) {
36
- current = next;
37
- listeners.forEach((l) => l(current));
38
- }
39
- });
40
44
  if (opts.requestType) {
41
45
  try {
42
46
  transport.sendMessage(opts.requestType);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,0BAA8E;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,oBAAAA,aAAa,aAAa,oBAAAC,YAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAY,GAAG;AACzC,gCAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":["defaultSend","defaultAddListener"]}
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './hostTransport';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n try {\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n } catch {\n // No host transport in this realm yet (the resolver throws when neither the injected\n // bundler bus nor the §4 discovery global is present). Leave `started` false so the\n // NEXT read retries, and let `initial` stand meanwhile — which is exactly the\n // documented behaviour for a channel the host never answers. Throwing here instead\n // would make a plain `getHostTheme()` fatal outside a sandbox, and since R3-307 the\n // deadline on every host call reads a channel, so one throw would be every call.\n return;\n }\n started = true;\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,mBAAoC;AACpC,2BAA8E;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,qBAAAA,aAAa,aAAa,qBAAAC,YAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,QAAI;AACF,gBAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,cAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,YAAI,SAAS,QAAW;AACtB,oBAAU;AACV,oBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,QACrC;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAON;AAAA,IACF;AACA,cAAU;AACV,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAY,GAAG;AACzC,gCAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":["defaultSend","defaultAddListener"]}
@@ -1,20 +1,24 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { useEffect, useState } from "react";
3
- import { sendMessage as defaultSend, addListener as defaultAddListener } from "./sandboxUtils";
3
+ import { sendMessage as defaultSend, addListener as defaultAddListener } from "./hostTransport";
4
4
  function createPushChannel(opts, transport = { sendMessage: defaultSend, addListener: defaultAddListener }) {
5
5
  let current = opts.initial;
6
6
  const listeners = /* @__PURE__ */ new Set();
7
7
  let started = false;
8
8
  const start = () => {
9
9
  if (started) return;
10
+ try {
11
+ transport.addListener(opts.pushType, (msg) => {
12
+ const next = opts.parse(msg);
13
+ if (next !== void 0) {
14
+ current = next;
15
+ listeners.forEach((l) => l(current));
16
+ }
17
+ });
18
+ } catch {
19
+ return;
20
+ }
10
21
  started = true;
11
- transport.addListener(opts.pushType, (msg) => {
12
- const next = opts.parse(msg);
13
- if (next !== void 0) {
14
- current = next;
15
- listeners.forEach((l) => l(current));
16
- }
17
- });
18
22
  if (opts.requestType) {
19
23
  try {
20
24
  transport.sendMessage(opts.requestType);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './sandboxUtils';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n started = true;\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe,aAAa,eAAe,0BAA0B;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,aAAa,aAAa,mBAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,cAAU;AACV,cAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,YAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,UAAI,SAAS,QAAW;AACtB,kBAAU;AACV,kBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,MACrC;AAAA,IACF,CAAC;AACD,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,GAAG;AACzC,cAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":[]}
1
+ {"version":3,"sources":["../src/pushChannel.ts"],"sourcesContent":["// Generic host→sandbox state channel read over the TRANSPORT\n// (SDK_PACKAGING_SPEC §4, expose-transport). The host pushes a `pushType`\n// message whenever the value changes and answers a `requestType` poll with the\n// current value — gated per-frame by the read ACL (site-main channelBridge.ts /\n// channelRouter). Historically the SDK read these values off injected sandbox\n// service objects (`module.evaluation.module.bundler.auth` etc.); this caches\n// the latest value SDK-side instead, so the SDK is self-contained and the\n// bundler services can eventually be retired.\n//\n// Every state helper (formFactor, auth, editorContext, theme, catalog) is a thin\n// wrapper over one of these — the get / onChange / use trio is identical.\nimport { useEffect, useState } from 'react';\nimport { sendMessage as defaultSend, addListener as defaultAddListener } from './hostTransport';\n\nexport interface PushChannel<T> {\n /** Pollable snapshot of the current value. */\n get(): T;\n /** Subscribe; invoked immediately with the current value, then on every change. Returns unsubscribe. */\n onChange(listener: (value: T) => void): () => void;\n /** React hook returning the current value, re-rendering on change. */\n use(): T;\n}\n\n/** Injectable transport — defaults to the real one; overridden in tests. */\nexport interface ChannelTransport {\n sendMessage: (type: string, data?: Record<string, unknown>) => void;\n addListener: (type: string, handler: (msg: Record<string, unknown>) => void) => () => void;\n}\n\nexport function createPushChannel<T>(\n opts: {\n /** Host→sandbox push message type (e.g. `form-factor`). */\n pushType: string;\n /** Poll message type the SDK sends to pull the current value (e.g. `request-form-factor`). */\n requestType?: string;\n /** Value assumed before the host answers — also the value when the app may not read the channel. */\n initial: T;\n /** Extract + validate the value from a push message; return `undefined` to ignore the message. */\n parse: (msg: Record<string, unknown>) => T | undefined;\n },\n transport: ChannelTransport = { sendMessage: defaultSend, addListener: defaultAddListener },\n): PushChannel<T> {\n let current = opts.initial;\n const listeners = new Set<(value: T) => void>();\n let started = false;\n\n // Lazily start on first read: register the push listener + send one poll, so a\n // late-mounting app still gets the current value. A channel the app may not\n // read is simply never answered → `initial` stands.\n const start = () => {\n if (started) return;\n try {\n transport.addListener(opts.pushType, (msg) => {\n const next = opts.parse(msg);\n if (next !== undefined) {\n current = next;\n listeners.forEach((l) => l(current));\n }\n });\n } catch {\n // No host transport in this realm yet (the resolver throws when neither the injected\n // bundler bus nor the §4 discovery global is present). Leave `started` false so the\n // NEXT read retries, and let `initial` stand meanwhile — which is exactly the\n // documented behaviour for a channel the host never answers. Throwing here instead\n // would make a plain `getHostTheme()` fatal outside a sandbox, and since R3-307 the\n // deadline on every host call reads a channel, so one throw would be every call.\n return;\n }\n started = true;\n if (opts.requestType) {\n try {\n transport.sendMessage(opts.requestType);\n } catch {\n /* transport not ready — a proactive push will still arrive */\n }\n }\n };\n\n const get = (): T => {\n start();\n return current;\n };\n\n const onChange = (listener: (value: T) => void): (() => void) => {\n start();\n listeners.add(listener);\n listener(current);\n return () => {\n listeners.delete(listener);\n };\n };\n\n const use = (): T => {\n const [value, setValue] = useState<T>(get);\n useEffect(() => onChange(setValue), []);\n return value;\n };\n\n return { get, onChange, use };\n}\n"],"mappings":";AAWA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe,aAAa,eAAe,0BAA0B;AAiBvE,SAAS,kBACd,MAUA,YAA8B,EAAE,aAAa,aAAa,aAAa,mBAAmB,GAC1E;AAChB,MAAI,UAAU,KAAK;AACnB,QAAM,YAAY,oBAAI,IAAwB;AAC9C,MAAI,UAAU;AAKd,QAAM,QAAQ,MAAM;AAClB,QAAI,QAAS;AACb,QAAI;AACF,gBAAU,YAAY,KAAK,UAAU,CAAC,QAAQ;AAC5C,cAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,YAAI,SAAS,QAAW;AACtB,oBAAU;AACV,oBAAU,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;AAAA,QACrC;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAON;AAAA,IACF;AACA,cAAU;AACV,QAAI,KAAK,aAAa;AACpB,UAAI;AACF,kBAAU,YAAY,KAAK,WAAW;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,CAAC,aAA+C;AAC/D,UAAM;AACN,cAAU,IAAI,QAAQ;AACtB,aAAS,OAAO;AAChB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,MAAM,MAAS;AACnB,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAY,GAAG;AACzC,cAAU,MAAM,SAAS,QAAQ,GAAG,CAAC,CAAC;AACtC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,UAAU,IAAI;AAC9B;","names":[]}
@@ -18,39 +18,89 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var sandboxUtils_exports = {};
20
20
  __export(sandboxUtils_exports, {
21
- addListener: () => addListener,
21
+ addListener: () => import_hostTransport2.addListener,
22
22
  protocolRequest: () => protocolRequest,
23
- sendMessage: () => sendMessage
23
+ sendMessage: () => import_hostTransport2.sendMessage,
24
+ withDeadline: () => withDeadline
24
25
  });
25
26
  module.exports = __toCommonJS(sandboxUtils_exports);
26
- var import_hostRuntime = require("./hostRuntime");
27
- function transport() {
27
+ var import_hostTransport = require("./hostTransport");
28
+ var import_hostAttention = require("./hostAttention");
29
+ var import_protocolDeadline = require("./protocolDeadline");
30
+ var import_hostTransport2 = require("./hostTransport");
31
+ const protocolRequest = (protocolName, method, params, opts) => withDeadline(
32
+ protocolName,
33
+ method,
34
+ () => (0, import_hostTransport.transport)().protocolRequest(protocolName, method, params),
35
+ opts
36
+ );
37
+ async function withDeadline(scheme, method, start, opts) {
38
+ const call = `${scheme}:${method}`;
39
+ const attendance = (0, import_protocolDeadline.attendanceOf)(scheme, method);
40
+ const bounds = opts?.timeoutMs !== void 0 ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs } : (0, import_protocolDeadline.boundsFor)(scheme, method);
41
+ const signal = opts?.signal;
42
+ if (signal?.aborted) throw new import_protocolDeadline.ProtocolCancelledError(call);
43
+ const work = start();
44
+ if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;
45
+ let deadline;
46
+ let notice;
47
+ let unsubscribeAttention;
48
+ let onAbort;
49
+ const started = Date.now();
28
50
  try {
29
- const injected = module?.evaluation?.module?.bundler?.messageBus;
30
- if (injected && typeof injected.sendMessage === "function") return injected;
31
- } catch {
51
+ return await new Promise((resolve, reject) => {
52
+ work.then(resolve, reject);
53
+ let attention;
54
+ let noticeFired = false;
55
+ const fireNotice = () => {
56
+ try {
57
+ opts?.onPending?.({
58
+ call,
59
+ attendance,
60
+ elapsedMs: Date.now() - started,
61
+ ...(0, import_protocolDeadline.attendanceReason)(scheme, method) ? { reason: (0, import_protocolDeadline.attendanceReason)(scheme, method) } : {},
62
+ ...attention?.awaiting ? { awaiting: { kind: attention.kind, since: attention.since } } : {}
63
+ });
64
+ } catch {
65
+ }
66
+ };
67
+ deadline = (0, import_protocolDeadline.createSuspendableDeadline)({
68
+ bounds,
69
+ onExpire: (bound, elapsedBoundMs) => reject(new import_protocolDeadline.ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound))
70
+ });
71
+ if (opts?.onPending) {
72
+ notice = setTimeout(() => {
73
+ noticeFired = true;
74
+ fireNotice();
75
+ }, import_protocolDeadline.PENDING_NOTICE_MS);
76
+ }
77
+ try {
78
+ unsubscribeAttention = (0, import_hostAttention.onHostAttentionChange)((next) => {
79
+ const wasAwaiting = attention?.awaiting ?? false;
80
+ attention = next;
81
+ deadline?.setAwaiting(next.awaiting);
82
+ if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();
83
+ });
84
+ } catch {
85
+ }
86
+ if (signal) {
87
+ onAbort = () => reject(new import_protocolDeadline.ProtocolCancelledError(call));
88
+ signal.addEventListener("abort", onAbort, { once: true });
89
+ }
90
+ });
91
+ } finally {
92
+ deadline?.dispose();
93
+ if (notice !== void 0) clearTimeout(notice);
94
+ unsubscribeAttention?.();
95
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
96
+ work.catch(() => void 0);
32
97
  }
33
- const t = (0, import_hostRuntime.getHostRuntime)()?.transport;
34
- if (t && typeof t.sendMessage === "function") return t;
35
- throw new Error("immediately.run: no host transport (neither injected nor __immediatelyRun__)");
36
98
  }
37
- const sendMessage = (type, data = {}) => {
38
- transport().sendMessage(type, data);
39
- };
40
- const protocolRequest = (protocolName, method, params) => transport().protocolRequest(protocolName, method, params);
41
- const addListener = (msgType, handler, event) => {
42
- const onMessage = event ?? transport().onMessage;
43
- const disposable = onMessage((msg) => {
44
- if (msg.type === msgType) {
45
- handler(msg);
46
- }
47
- });
48
- return () => disposable.dispose();
49
- };
50
99
  // Annotate the CommonJS export names for ESM import in node:
51
100
  0 && (module.exports = {
52
101
  addListener,
53
102
  protocolRequest,
54
- sendMessage
103
+ sendMessage,
104
+ withDeadline
55
105
  });
56
106
  //# sourceMappingURL=sandboxUtils.cjs.map