{"version":3,"sources":["../src/sandboxUtils.ts"],"sourcesContent":["// Bounded host calls (R3-298) + the host-attention-aware deadline (R3-307).\n//\n// The transport primitives this builds on moved to `hostTransport.ts` — see that file for\n// why. `sendMessage` and `addListener` are re-exported here unchanged, so every existing\n// import site (and this module's api-snapshot entry) is exactly as it was.\nimport { transport } from './hostTransport';\nimport { onHostAttentionChange, type HostAttention } from './hostAttention';\n\nimport {\n  attendanceOf,\n  attendanceReason,\n  boundsFor,\n  createSuspendableDeadline,\n  PENDING_NOTICE_MS,\n  ProtocolCancelledError,\n  ProtocolTimeoutError,\n  type BoundedCallOptions,\n  type CallBounds,\n} from './protocolDeadline';\n\nexport { sendMessage, addListener } from './hostTransport';\n\n/**\n * One host protocol request, BOUNDED (R3-298).\n *\n * The bound comes from the call's classification (`protocolDeadline.ts`): an unattended\n * channel round-trip gets tens of seconds, a call that may draw host chrome and wait for a\n * person gets minutes. Nothing is unbounded — an unbounded wait is the failure this fixes.\n *\n * Since R3-307 a call whose prompts the host actually announces runs on the SHORT bound and\n * is suspended only while the host says a person is being asked, so the common\n * grant-already-held path reports a fault in seconds instead of minutes. The absolute\n * ceiling still applies: the signal may extend a deadline, never remove it.\n *\n * The host's own work is NOT cancelled by `signal` or by the deadline. The one-shot\n * transport allocates its `msgId` internally, so the SDK has no handle to send the host a\n * cancel for (streams do, and `consumeStream` uses it). What the caller gets back is\n * control: it stops waiting and gets a typed error instead of hanging. A host prompt that\n * is already on screen stays there until the user dismisses it, which is correct — the SDK\n * must not be able to tear down host chrome the user is looking at.\n */\nexport const protocolRequest = (\n  protocolName: string,\n  method: string,\n  params: Array<any>,\n  opts?: BoundedCallOptions,\n): Promise<any> =>\n  withDeadline(protocolName, method, () => transport().protocolRequest(protocolName, method, params), opts);\n\n/**\n * Race a host call against its deadline, a caller abort, and a pending notice.\n *\n * Kept separate from the transport so it is unit-testable against a promise that simply\n * never settles — which is the whole scenario, and one no live transport reproduces on\n * demand.\n *\n * An explicit `opts.timeoutMs` is the WHOLE bound and is never suspended: a caller that\n * names a number owns the wait, and silently stretching it past what they asked for would\n * be the same class of surprise this machinery exists to remove.\n */\nexport async function withDeadline<T>(\n  scheme: string,\n  method: string,\n  start: () => Promise<T>,\n  opts?: BoundedCallOptions,\n): Promise<T> {\n  const call = `${scheme}:${method}`;\n  const attendance = attendanceOf(scheme, method);\n  const bounds: CallBounds =\n    opts?.timeoutMs !== undefined ? { idleMs: opts.timeoutMs, ceilingMs: opts.timeoutMs } : boundsFor(scheme, method);\n  const signal = opts?.signal;\n\n  if (signal?.aborted) throw new ProtocolCancelledError(call);\n\n  const work = start();\n  // No bound and no abort wanted: hand back the untouched promise rather than wrapping it\n  // in timers that would never fire.\n  if (!Number.isFinite(bounds.ceilingMs) && !signal && !opts?.onPending) return work;\n\n  let deadline: { setAwaiting: (a: boolean) => void; dispose: () => void } | undefined;\n  let notice: ReturnType<typeof setTimeout> | undefined;\n  let unsubscribeAttention: (() => void) | undefined;\n  let onAbort: (() => void) | undefined;\n  const started = Date.now();\n\n  try {\n    return await new Promise<T>((resolve, reject) => {\n      // `work` settling always wins — a call that answered a millisecond before its\n      // deadline must not be reported as a timeout.\n      work.then(resolve, reject);\n\n      // The host's live \"a person is being asked\" signal. Read defensively: a host that\n      // never pushes the channel leaves this at \"not awaiting\", which is exactly the\n      // pre-R3-307 behaviour for a call on its idle bound.\n      let attention: HostAttention | undefined;\n      let noticeFired = false;\n      const fireNotice = () => {\n        try {\n          opts?.onPending?.({\n            call,\n            attendance,\n            elapsedMs: Date.now() - started,\n            ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n            ...(attention?.awaiting ? { awaiting: { kind: attention.kind, since: attention.since } } : {}),\n          });\n        } catch {\n          /* a caller's render callback must never break the call it describes */\n        }\n      };\n\n      deadline = createSuspendableDeadline({\n        bounds,\n        onExpire: (bound, elapsedBoundMs) => reject(new ProtocolTimeoutError(call, elapsedBoundMs, attendance, bound)),\n      });\n\n      if (opts?.onPending) {\n        notice = setTimeout(() => {\n          noticeFired = true;\n          fireNotice();\n        }, PENDING_NOTICE_MS);\n      }\n\n      // Subscribing invokes the listener immediately with the current value, so the\n      // deadline starts in the right state even if a prompt was already up.\n      //\n      // Guarded because this sits on the path of EVERY host call: a fault in the attention\n      // channel must degrade to \"no signal\" (the pre-R3-307 behaviour, bounds unsuspended),\n      // never take down every request in the SDK.\n      try {\n        unsubscribeAttention = onHostAttentionChange((next) => {\n          const wasAwaiting = attention?.awaiting ?? false;\n          attention = next;\n          deadline?.setAwaiting(next.awaiting);\n          // Re-notify once the notice has fired: \"still waiting\" → \"tap your passkey\" is\n          // the whole point, and a caller rendering a waiting state wants the live\n          // sentence, not the one that was true three seconds in.\n          if (noticeFired && next.awaiting !== wasAwaiting) fireNotice();\n        });\n      } catch {\n        /* no attention signal available — the bounds simply never suspend */\n      }\n\n      if (signal) {\n        onAbort = () => reject(new ProtocolCancelledError(call));\n        signal.addEventListener('abort', onAbort, { once: true });\n      }\n    });\n  } finally {\n    deadline?.dispose();\n    if (notice !== undefined) clearTimeout(notice);\n    unsubscribeAttention?.();\n    if (signal && onAbort) signal.removeEventListener('abort', onAbort);\n    // The host may still answer after we stopped waiting; swallow it so a late rejection\n    // does not surface as an unhandled promise rejection in the app's console.\n    work.catch(() => undefined);\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,2BAA0B;AAC1B,2BAA0D;AAE1D,8BAUO;AAEP,IAAAA,wBAAyC;AAqBlC,MAAM,kBAAkB,CAC7B,cACA,QACA,QACA,SAEA,aAAa,cAAc,QAAQ,UAAM,gCAAU,EAAE,gBAAgB,cAAc,QAAQ,MAAM,GAAG,IAAI;AAa1G,eAAsB,aACpB,QACA,QACA,OACA,MACY;AACZ,QAAM,OAAO,GAAG,MAAM,IAAI,MAAM;AAChC,QAAM,iBAAa,sCAAa,QAAQ,MAAM;AAC9C,QAAM,SACJ,MAAM,cAAc,SAAY,EAAE,QAAQ,KAAK,WAAW,WAAW,KAAK,UAAU,QAAI,mCAAU,QAAQ,MAAM;AAClH,QAAM,SAAS,MAAM;AAErB,MAAI,QAAQ,QAAS,OAAM,IAAI,+CAAuB,IAAI;AAE1D,QAAM,OAAO,MAAM;AAGnB,MAAI,CAAC,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,UAAU,CAAC,MAAM,UAAW,QAAO;AAE9E,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,KAAK,IAAI;AAEzB,MAAI;AACF,WAAO,MAAM,IAAI,QAAW,CAAC,SAAS,WAAW;AAG/C,WAAK,KAAK,SAAS,MAAM;AAKzB,UAAI;AACJ,UAAI,cAAc;AAClB,YAAM,aAAa,MAAM;AACvB,YAAI;AACF,gBAAM,YAAY;AAAA,YAChB;AAAA,YACA;AAAA,YACA,WAAW,KAAK,IAAI,IAAI;AAAA,YACxB,OAAI,0CAAiB,QAAQ,MAAM,IAAI,EAAE,YAAQ,0CAAiB,QAAQ,MAAM,EAAY,IAAI,CAAC;AAAA,YACjG,GAAI,WAAW,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,UAAU,MAAM,EAAE,IAAI,CAAC;AAAA,UAC9F,CAAC;AAAA,QACH,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,qBAAW,mDAA0B;AAAA,QACnC;AAAA,QACA,UAAU,CAAC,OAAO,mBAAmB,OAAO,IAAI,6CAAqB,MAAM,gBAAgB,YAAY,KAAK,CAAC;AAAA,MAC/G,CAAC;AAED,UAAI,MAAM,WAAW;AACnB,iBAAS,WAAW,MAAM;AACxB,wBAAc;AACd,qBAAW;AAAA,QACb,GAAG,yCAAiB;AAAA,MACtB;AAQA,UAAI;AACF,mCAAuB,4CAAsB,CAAC,SAAS;AACrD,gBAAM,cAAc,WAAW,YAAY;AAC3C,sBAAY;AACZ,oBAAU,YAAY,KAAK,QAAQ;AAInC,cAAI,eAAe,KAAK,aAAa,YAAa,YAAW;AAAA,QAC/D,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAEA,UAAI,QAAQ;AACV,kBAAU,MAAM,OAAO,IAAI,+CAAuB,IAAI,CAAC;AACvD,eAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH,UAAE;AACA,cAAU,QAAQ;AAClB,QAAI,WAAW,OAAW,cAAa,MAAM;AAC7C,2BAAuB;AACvB,QAAI,UAAU,QAAS,QAAO,oBAAoB,SAAS,OAAO;AAGlE,SAAK,MAAM,MAAM,MAAS;AAAA,EAC5B;AACF;","names":["import_hostTransport"]}