{"version":3,"sources":["../src/entryState.ts"],"sourcesContent":["// Per-history-entry scratch (R3-627): a small opaque value an app attaches to the\n// history entry it is LEAVING, and reads back when the browser returns to that entry.\n//\n// Why this exists. The app frame's realm is replaced by an in-app navigation, so\n// nothing in app memory survives it; the host owns the history stack but is at an\n// opaque origin and cannot look inside the frame. So the only way an app can leave\n// itself a bookmark is to hand the host a value to hold against the entry. The host\n// never parses it — it is the app's own value coming back to the app, which is why\n// this does not weaken `EDITOR_AS_APP_SPEC §2` (\"view state … never crosses the\n// boundary\"): the host is a courier, not a reader.\n//\n// Collected at navigation time, not continuously. The app is the one calling\n// `navigate()`, so the scratch is gathered synchronously inside that call — no scroll\n// reporting channel, no throttling, no traffic for a value needed at one instant.\n\n/** How the browser reached the current entry. `push` is an ordinary forward\n *  navigation; `back`/`forward` are traversals, and only a traversal restores. */\nexport type NavigationDirection = 'push' | 'back' | 'forward';\n\n/** Serialized cap for the whole scratch. Past this the scratch is dropped with a\n *  dev-time warning rather than silently truncated: a half-written bookmark that\n *  restores to the wrong place is worse than no bookmark. */\nexport const ENTRY_STATE_MAX_BYTES = 4096;\n\nexport interface ArrivedNavigation {\n  /** The scratch the leaving app left on this entry, if any. */\n  readonly state: Readonly<Record<string, unknown>> | undefined;\n  /** How this entry was reached. */\n  readonly direction: NavigationDirection;\n}\n\ntype Collector = () => unknown;\n\nconst collectors = new Map<string, Collector>();\nlet queued: Record<string, unknown> = {};\nlet arrived: ArrivedNavigation = { state: undefined, direction: 'push' };\nconst listeners = new Set<() => void>();\n\nconst notify = (): void => {\n  for (const l of [...listeners]) l();\n};\n\n/**\n * Queue a value for the entry being left. The last call before a navigation wins.\n * Prefer {@link registerEntryStateCollector} for values that are only knowable at the\n * instant of navigating (a scroll offset is the motivating case).\n */\nexport const saveEntryState = (key: string, value: unknown): void => {\n  queued[key] = value;\n};\n\n/** Register a callback asked for its value at navigation time. Returns its remover.\n *  A second registration for the same key replaces the first — one owner per key. */\nexport const registerEntryStateCollector = (key: string, collect: Collector): (() => void) => {\n  collectors.set(key, collect);\n  return () => {\n    if (collectors.get(key) === collect) collectors.delete(key);\n  };\n};\n\n/**\n * Gather the scratch for the entry being left and clear the queue. Called by\n * `navigate()` — not part of the app-facing surface.\n *\n * A collector that throws is skipped: a bookmark is a convenience, and it must never\n * be able to break a navigation.\n */\nexport const takeQueuedEntryState = (): Record<string, unknown> | undefined => {\n  const out: Record<string, unknown> = { ...queued };\n  queued = {};\n  for (const [key, collect] of collectors) {\n    try {\n      const value = collect();\n      if (value !== undefined) out[key] = value;\n    } catch {\n      /* a collector must not break navigation */\n    }\n  }\n  if (Object.keys(out).length === 0) return undefined;\n  let size = 0;\n  try {\n    size = JSON.stringify(out).length;\n  } catch {\n    return undefined; // not serializable → nothing to hand the host\n  }\n  if (size > ENTRY_STATE_MAX_BYTES) {\n    console.warn(\n      `[Sandbox] entry state is ${size} bytes, over the ${ENTRY_STATE_MAX_BYTES}-byte cap — dropped. ` +\n        `Keep per-entry scratch small (a scroll offset, a few ids), not a cache.`,\n    );\n    return undefined;\n  }\n  return out;\n};\n\n/** Record what the host said about the entry just arrived at. Called by the boot\n *  shell's `urlchange` listener — not part of the app-facing surface. */\nexport const receiveNavigation = (next: ArrivedNavigation): void => {\n  arrived = next;\n  notify();\n};\n\n/** The current arrival, as one stable object so `useSyncExternalStore` can compare\n *  by identity. */\nexport const getArrivedNavigation = (): ArrivedNavigation => arrived;\n\nexport const subscribeNavigation = (listener: () => void): (() => void) => {\n  listeners.add(listener);\n  return () => listeners.delete(listener);\n};\n\n/** Test seam: forget every collector, queued value and arrival. */\nexport const resetEntryState = (): void => {\n  collectors.clear();\n  queued = {};\n  arrived = { state: undefined, direction: 'push' };\n  listeners.clear();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBO,MAAM,wBAAwB;AAWrC,MAAM,aAAa,oBAAI,IAAuB;AAC9C,IAAI,SAAkC,CAAC;AACvC,IAAI,UAA6B,EAAE,OAAO,QAAW,WAAW,OAAO;AACvE,MAAM,YAAY,oBAAI,IAAgB;AAEtC,MAAM,SAAS,MAAY;AACzB,aAAW,KAAK,CAAC,GAAG,SAAS,EAAG,GAAE;AACpC;AAOO,MAAM,iBAAiB,CAAC,KAAa,UAAyB;AACnE,SAAO,GAAG,IAAI;AAChB;AAIO,MAAM,8BAA8B,CAAC,KAAa,YAAqC;AAC5F,aAAW,IAAI,KAAK,OAAO;AAC3B,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,GAAG,MAAM,QAAS,YAAW,OAAO,GAAG;AAAA,EAC5D;AACF;AASO,MAAM,uBAAuB,MAA2C;AAC7E,QAAM,MAA+B,EAAE,GAAG,OAAO;AACjD,WAAS,CAAC;AACV,aAAW,CAAC,KAAK,OAAO,KAAK,YAAY;AACvC,QAAI;AACF,YAAM,QAAQ,QAAQ;AACtB,UAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,MAAI,OAAO;AACX,MAAI;AACF,WAAO,KAAK,UAAU,GAAG,EAAE;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,uBAAuB;AAChC,YAAQ;AAAA,MACN,4BAA4B,IAAI,oBAAoB,qBAAqB;AAAA,IAE3E;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAIO,MAAM,oBAAoB,CAAC,SAAkC;AAClE,YAAU;AACV,SAAO;AACT;AAIO,MAAM,uBAAuB,MAAyB;AAEtD,MAAM,sBAAsB,CAAC,aAAuC;AACzE,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM,UAAU,OAAO,QAAQ;AACxC;AAGO,MAAM,kBAAkB,MAAY;AACzC,aAAW,MAAM;AACjB,WAAS,CAAC;AACV,YAAU,EAAE,OAAO,QAAW,WAAW,OAAO;AAChD,YAAU,MAAM;AAClB;","names":[]}