@immediately-run/sdk 0.13.0 → 0.16.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 (118) hide show
  1. package/README.md +27 -3
  2. package/dist/MDXProvider.cjs.map +1 -1
  3. package/dist/MDXProvider.d.cts +4 -0
  4. package/dist/MDXProvider.d.ts +4 -0
  5. package/dist/MDXProvider.js.map +1 -1
  6. package/dist/RoutingSpec.cjs.map +1 -1
  7. package/dist/RoutingSpec.d.cts +20 -3
  8. package/dist/RoutingSpec.d.ts +20 -3
  9. package/dist/auth.cjs.map +1 -1
  10. package/dist/auth.d.cts +2 -0
  11. package/dist/auth.d.ts +2 -0
  12. package/dist/auth.js.map +1 -1
  13. package/dist/boot.cjs +17 -7
  14. package/dist/boot.cjs.map +1 -1
  15. package/dist/boot.d.cts +28 -4
  16. package/dist/boot.d.ts +28 -4
  17. package/dist/boot.js +16 -7
  18. package/dist/boot.js.map +1 -1
  19. package/dist/components/Include.cjs.map +1 -1
  20. package/dist/components/Include.d.cts +7 -0
  21. package/dist/components/Include.d.ts +7 -0
  22. package/dist/components/Include.js.map +1 -1
  23. package/dist/components/MDXComponents.cjs.map +1 -1
  24. package/dist/components/MDXComponents.d.cts +6 -0
  25. package/dist/components/MDXComponents.d.ts +6 -0
  26. package/dist/components/MDXComponents.js.map +1 -1
  27. package/dist/components/Routes.cjs +59 -0
  28. package/dist/components/Routes.cjs.map +1 -0
  29. package/dist/components/Routes.d.cts +34 -0
  30. package/dist/components/Routes.d.ts +34 -0
  31. package/dist/components/Routes.js +34 -0
  32. package/dist/components/Routes.js.map +1 -0
  33. package/dist/contribute.cjs.map +1 -1
  34. package/dist/contribute.d.cts +2 -0
  35. package/dist/contribute.d.ts +2 -0
  36. package/dist/contribute.js.map +1 -1
  37. package/dist/diagnostics.cjs.map +1 -1
  38. package/dist/diagnostics.d.cts +3 -0
  39. package/dist/diagnostics.d.ts +3 -0
  40. package/dist/diagnostics.js.map +1 -1
  41. package/dist/formFactor.cjs.map +1 -1
  42. package/dist/formFactor.d.cts +2 -0
  43. package/dist/formFactor.d.ts +2 -0
  44. package/dist/formFactor.js.map +1 -1
  45. package/dist/hooks.cjs +27 -28
  46. package/dist/hooks.cjs.map +1 -1
  47. package/dist/hooks.d.cts +39 -4
  48. package/dist/hooks.d.ts +39 -4
  49. package/dist/hooks.js +27 -29
  50. package/dist/hooks.js.map +1 -1
  51. package/dist/index.cjs +6 -0
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +7 -4
  54. package/dist/index.d.ts +7 -4
  55. package/dist/index.js +3 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/irMarkers.cjs.map +1 -1
  58. package/dist/irMarkers.d.cts +1 -0
  59. package/dist/irMarkers.d.ts +1 -0
  60. package/dist/irMarkers.js.map +1 -1
  61. package/dist/llm.cjs.map +1 -1
  62. package/dist/llm.d.cts +5 -0
  63. package/dist/llm.d.ts +5 -0
  64. package/dist/llm.js.map +1 -1
  65. package/dist/loading.cjs +186 -0
  66. package/dist/loading.cjs.map +1 -0
  67. package/dist/loading.d.cts +48 -0
  68. package/dist/loading.d.ts +48 -0
  69. package/dist/loading.js +162 -0
  70. package/dist/loading.js.map +1 -0
  71. package/dist/mounts.cjs.map +1 -1
  72. package/dist/mounts.d.cts +14 -2
  73. package/dist/mounts.d.ts +14 -2
  74. package/dist/mounts.js.map +1 -1
  75. package/dist/netFetch.cjs.map +1 -1
  76. package/dist/netFetch.d.cts +2 -0
  77. package/dist/netFetch.d.ts +2 -0
  78. package/dist/netFetch.js.map +1 -1
  79. package/dist/onFsChange.cjs +42 -0
  80. package/dist/onFsChange.cjs.map +1 -0
  81. package/dist/onFsChange.d.cts +23 -0
  82. package/dist/onFsChange.d.ts +23 -0
  83. package/dist/onFsChange.js +16 -0
  84. package/dist/onFsChange.js.map +1 -0
  85. package/dist/protocolStream.cjs.map +1 -1
  86. package/dist/protocolStream.d.cts +3 -0
  87. package/dist/protocolStream.d.ts +3 -0
  88. package/dist/protocolStream.js.map +1 -1
  89. package/dist/ready.cjs.map +1 -1
  90. package/dist/ready.d.cts +7 -0
  91. package/dist/ready.d.ts +7 -0
  92. package/dist/ready.js.map +1 -1
  93. package/dist/routeMatch.cjs +72 -0
  94. package/dist/routeMatch.cjs.map +1 -0
  95. package/dist/routeMatch.d.cts +19 -0
  96. package/dist/routeMatch.d.ts +19 -0
  97. package/dist/routeMatch.js +46 -0
  98. package/dist/routeMatch.js.map +1 -0
  99. package/dist/routing.cjs +35 -14
  100. package/dist/routing.cjs.map +1 -1
  101. package/dist/routing.d.cts +33 -4
  102. package/dist/routing.d.ts +33 -4
  103. package/dist/routing.js +32 -14
  104. package/dist/routing.js.map +1 -1
  105. package/dist/runtime.cjs.map +1 -1
  106. package/dist/runtime.d.cts +1 -0
  107. package/dist/runtime.d.ts +1 -0
  108. package/dist/runtime.js.map +1 -1
  109. package/dist/sandboxTypes.cjs.map +1 -1
  110. package/dist/sandboxTypes.d.cts +30 -7
  111. package/dist/sandboxTypes.d.ts +30 -7
  112. package/dist/version.cjs +1 -1
  113. package/dist/version.cjs.map +1 -1
  114. package/dist/version.d.cts +1 -1
  115. package/dist/version.d.ts +1 -1
  116. package/dist/version.js +1 -1
  117. package/dist/version.js.map +1 -1
  118. package/package.json +6 -2
@@ -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\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\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}\n\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 */\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): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\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 try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\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 return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\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};\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 */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AAkBlC,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;AASA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GACE;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAChC,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;AAED,MAAI;AACF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,WAAO,MAAM;AACX,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,eAAO,MAAM;AAAA,MACf,OAAO;AACL,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAGA,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;AACvF;AAOO,SAAS,eACd,cACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,MAAM;AAC3E;","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';\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}\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 */\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): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\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 try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\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 return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\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};\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 */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AAqBlC,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;AASA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GACE;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAChC,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;AAED,MAAI;AACF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,WAAO,MAAM;AACX,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,eAAO,MAAM;AAAA,MACf,OAAO;AACL,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAGA,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;AACvF;AAOO,SAAS,eACd,cACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,MAAM;AAC3E;","names":[]}
@@ -1,3 +1,4 @@
1
+ /** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */
1
2
  type StreamFrame = {
2
3
  kind: 'event';
3
4
  value: unknown;
@@ -9,6 +10,7 @@ type StreamFrame = {
9
10
  code: string;
10
11
  message: string;
11
12
  };
13
+ /** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */
12
14
  interface StreamTransport {
13
15
  send: (msg: {
14
16
  type: string;
@@ -22,6 +24,7 @@ interface StreamTransport {
22
24
  stream?: StreamFrame;
23
25
  }) => void) => () => void;
24
26
  }
27
+ /** Thrown when a stream ends in an `error` frame; carries the host's `code`. */
25
28
  declare class StreamError extends Error {
26
29
  code: string;
27
30
  constructor(code: string, message: string);
@@ -1,3 +1,4 @@
1
+ /** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */
1
2
  type StreamFrame = {
2
3
  kind: 'event';
3
4
  value: unknown;
@@ -9,6 +10,7 @@ type StreamFrame = {
9
10
  code: string;
10
11
  message: string;
11
12
  };
13
+ /** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */
12
14
  interface StreamTransport {
13
15
  send: (msg: {
14
16
  type: string;
@@ -22,6 +24,7 @@ interface StreamTransport {
22
24
  stream?: StreamFrame;
23
25
  }) => void) => () => void;
24
26
  }
27
+ /** Thrown when a stream ends in an `error` frame; carries the host's `code`. */
25
28
  declare class StreamError extends Error {
26
29
  code: string;
27
30
  constructor(code: string, message: string);
@@ -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\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\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}\n\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 */\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): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\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 try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\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 return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\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};\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 */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":"AAUA,SAAS,aAAa,mBAAmB;AAkBlC,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;AASA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GACE;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAChC,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;AAED,MAAI;AACF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,WAAO,MAAM;AACX,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,eAAO,MAAM;AAAA,MACf,OAAO;AACL,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAGA,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;AACvF;AAOO,SAAS,eACd,cACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,MAAM;AAC3E;","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';\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}\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 */\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): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\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 try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\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 return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\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};\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 */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":"AAUA,SAAS,aAAa,mBAAmB;AAqBlC,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;AASA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GACE;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAChC,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;AAED,MAAI;AACF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,WAAO,MAAM;AACX,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,eAAO,MAAM;AAAA,MACf,OAAO;AACL,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AAAA,EACd;AACF;AAGA,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;AACvF;AAOO,SAAS,eACd,cACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,MAAM;AAC3E;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAA2C;AAc3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,oBAAAA,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAS5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":["defaultSend"]}
1
+ {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAA2C;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,oBAAAA,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":["defaultSend"]}
package/dist/ready.d.cts CHANGED
@@ -1,3 +1,4 @@
1
+ /** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */
1
2
  interface ReadyState {
2
3
  /** Whether the app has called `reportReady()`. */
3
4
  reported: boolean;
@@ -14,6 +15,12 @@ interface ReadyDeps {
14
15
  * report to the runtime (`ir-report-ready`) so the host can resolve
15
16
  * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before
16
17
  * the root render commits can only delay the signal, never advance it.
18
+ *
19
+ * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *"keep your
20
+ * loading skeleton up; I am not done yet"* — the host holds the §3 reveal until
21
+ * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive
22
+ * frame is on screen — not at mount, and not after every async settle. An app that
23
+ * never calls it reveals automatically at the root-render commit (the default path).
17
24
  */
18
25
  declare function reportReady(): void;
19
26
  /** Pollable snapshot of the report state. */
package/dist/ready.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */
1
2
  interface ReadyState {
2
3
  /** Whether the app has called `reportReady()`. */
3
4
  reported: boolean;
@@ -14,6 +15,12 @@ interface ReadyDeps {
14
15
  * report to the runtime (`ir-report-ready`) so the host can resolve
15
16
  * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before
16
17
  * the root render commits can only delay the signal, never advance it.
18
+ *
19
+ * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *"keep your
20
+ * loading skeleton up; I am not done yet"* — the host holds the §3 reveal until
21
+ * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive
22
+ * frame is on screen — not at mount, and not after every async settle. An app that
23
+ * never calls it reveals automatically at the root-render commit (the default path).
17
24
  */
18
25
  declare function reportReady(): void;
19
26
  /** Pollable snapshot of the report state. */
package/dist/ready.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":"AAaA,SAAS,eAAe,mBAAmB;AAc3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAS5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":[]}
1
+ {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":"AAaA,SAAS,eAAe,mBAAmB;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":[]}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var routeMatch_exports = {};
20
+ __export(routeMatch_exports, {
21
+ compileTemplate: () => compileTemplate,
22
+ matchRoute: () => matchRoute,
23
+ toRegExp: () => toRegExp
24
+ });
25
+ module.exports = __toCommonJS(routeMatch_exports);
26
+ const escapeForRegexp = (str) => str.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
27
+ const WILDCARD_GROUP = "wild";
28
+ const compileTemplate = (template) => {
29
+ const token = /(:[A-Za-z_][A-Za-z0-9_]*)|\*/g;
30
+ let src = "";
31
+ let last = 0;
32
+ let m;
33
+ while ((m = token.exec(template)) !== null) {
34
+ src += escapeForRegexp(template.slice(last, m.index));
35
+ src += m[1] ? `(?<${m[1].slice(1)}>[^/]+)` : `(?<${WILDCARD_GROUP}>.*)`;
36
+ last = m.index + m[0].length;
37
+ }
38
+ src += escapeForRegexp(template.slice(last));
39
+ return new RegExp(`^${src}$`);
40
+ };
41
+ const templateCache = /* @__PURE__ */ new Map();
42
+ const toRegExp = (pattern) => {
43
+ if (pattern instanceof RegExp) {
44
+ return pattern;
45
+ }
46
+ let compiled = templateCache.get(pattern);
47
+ if (!compiled) {
48
+ compiled = compileTemplate(pattern);
49
+ templateCache.set(pattern, compiled);
50
+ }
51
+ return compiled;
52
+ };
53
+ const matchRoute = (pattern, path) => {
54
+ const match = path.match(toRegExp(pattern));
55
+ if (!match) {
56
+ return null;
57
+ }
58
+ const params = {};
59
+ for (const [key, value] of Object.entries(match.groups ?? {})) {
60
+ if (value !== void 0) {
61
+ params[key === WILDCARD_GROUP ? "*" : key] = value;
62
+ }
63
+ }
64
+ return params;
65
+ };
66
+ // Annotate the CommonJS export names for ESM import in node:
67
+ 0 && (module.exports = {
68
+ compileTemplate,
69
+ matchRoute,
70
+ toRegExp
71
+ });
72
+ //# sourceMappingURL=routeMatch.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/routeMatch.ts"],"sourcesContent":["import type { RouteParams } from './RoutingSpec';\n\n// from: https://stackoverflow.com/a/63838890\nconst escapeForRegexp = (str: string): string => str.replace(/[.*+\\-?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// Internal group name standing in for a `*` wildcard (which is not a valid JS\n// regex group identifier); remapped to the `*` param key after matching.\nconst WILDCARD_GROUP = 'wild';\n\n/**\n * Compile a path template to an anchored RegExp with named groups:\n * `:name` → one non-slash segment `*` → the rest (greedy)\n * A template with neither token is a literal exact match. Raw RegExp patterns\n * never reach here — they are the escape hatch, used as authored.\n */\nexport const compileTemplate = (template: string): RegExp => {\n const token = /(:[A-Za-z_][A-Za-z0-9_]*)|\\*/g;\n let src = '';\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = token.exec(template)) !== null) {\n src += escapeForRegexp(template.slice(last, m.index));\n src += m[1] ? `(?<${m[1].slice(1)}>[^/]+)` : `(?<${WILDCARD_GROUP}>.*)`;\n last = m.index + m[0].length;\n }\n src += escapeForRegexp(template.slice(last));\n return new RegExp(`^${src}$`);\n};\n\nconst templateCache = new Map<string, RegExp>();\n\n/** Resolve a pattern to a RegExp: templates are compiled (and cached), RegExp passes through. */\nexport const toRegExp = (pattern: string | RegExp): RegExp => {\n if (pattern instanceof RegExp) {\n return pattern;\n }\n let compiled = templateCache.get(pattern);\n if (!compiled) {\n compiled = compileTemplate(pattern);\n templateCache.set(pattern, compiled);\n }\n return compiled;\n};\n\n/**\n * Match a `sandboxPath` against a route pattern. Returns the named params on a\n * match (the `*` wildcard surfaces under the `'*'` key), or `null` otherwise.\n */\nexport const matchRoute = (pattern: string | RegExp, path: string): RouteParams | null => {\n const match = path.match(toRegExp(pattern));\n if (!match) {\n return null;\n }\n const params: RouteParams = {};\n for (const [key, value] of Object.entries(match.groups ?? {})) {\n if (value !== undefined) {\n params[key === WILDCARD_GROUP ? '*' : key] = value;\n }\n }\n return params;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,MAAM,kBAAkB,CAAC,QAAwB,IAAI,QAAQ,yBAAyB,MAAM;AAI5F,MAAM,iBAAiB;AAQhB,MAAM,kBAAkB,CAAC,aAA6B;AAC3D,QAAM,QAAQ;AACd,MAAI,MAAM;AACV,MAAI,OAAO;AACX,MAAI;AACJ,UAAQ,IAAI,MAAM,KAAK,QAAQ,OAAO,MAAM;AAC1C,WAAO,gBAAgB,SAAS,MAAM,MAAM,EAAE,KAAK,CAAC;AACpD,WAAO,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,YAAY,MAAM,cAAc;AACjE,WAAO,EAAE,QAAQ,EAAE,CAAC,EAAE;AAAA,EACxB;AACA,SAAO,gBAAgB,SAAS,MAAM,IAAI,CAAC;AAC3C,SAAO,IAAI,OAAO,IAAI,GAAG,GAAG;AAC9B;AAEA,MAAM,gBAAgB,oBAAI,IAAoB;AAGvC,MAAM,WAAW,CAAC,YAAqC;AAC5D,MAAI,mBAAmB,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,WAAW,cAAc,IAAI,OAAO;AACxC,MAAI,CAAC,UAAU;AACb,eAAW,gBAAgB,OAAO;AAClC,kBAAc,IAAI,SAAS,QAAQ;AAAA,EACrC;AACA,SAAO;AACT;AAMO,MAAM,aAAa,CAAC,SAA0B,SAAqC;AACxF,QAAM,QAAQ,KAAK,MAAM,SAAS,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,SAAsB,CAAC;AAC7B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,UAAU,CAAC,CAAC,GAAG;AAC7D,QAAI,UAAU,QAAW;AACvB,aAAO,QAAQ,iBAAiB,MAAM,GAAG,IAAI;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,19 @@
1
+ import { RouteParams } from './RoutingSpec.cjs';
2
+ import 'react';
3
+
4
+ /**
5
+ * Compile a path template to an anchored RegExp with named groups:
6
+ * `:name` → one non-slash segment `*` → the rest (greedy)
7
+ * A template with neither token is a literal exact match. Raw RegExp patterns
8
+ * never reach here — they are the escape hatch, used as authored.
9
+ */
10
+ declare const compileTemplate: (template: string) => RegExp;
11
+ /** Resolve a pattern to a RegExp: templates are compiled (and cached), RegExp passes through. */
12
+ declare const toRegExp: (pattern: string | RegExp) => RegExp;
13
+ /**
14
+ * Match a `sandboxPath` against a route pattern. Returns the named params on a
15
+ * match (the `*` wildcard surfaces under the `'*'` key), or `null` otherwise.
16
+ */
17
+ declare const matchRoute: (pattern: string | RegExp, path: string) => RouteParams | null;
18
+
19
+ export { compileTemplate, matchRoute, toRegExp };
@@ -0,0 +1,19 @@
1
+ import { RouteParams } from './RoutingSpec.js';
2
+ import 'react';
3
+
4
+ /**
5
+ * Compile a path template to an anchored RegExp with named groups:
6
+ * `:name` → one non-slash segment `*` → the rest (greedy)
7
+ * A template with neither token is a literal exact match. Raw RegExp patterns
8
+ * never reach here — they are the escape hatch, used as authored.
9
+ */
10
+ declare const compileTemplate: (template: string) => RegExp;
11
+ /** Resolve a pattern to a RegExp: templates are compiled (and cached), RegExp passes through. */
12
+ declare const toRegExp: (pattern: string | RegExp) => RegExp;
13
+ /**
14
+ * Match a `sandboxPath` against a route pattern. Returns the named params on a
15
+ * match (the `*` wildcard surfaces under the `'*'` key), or `null` otherwise.
16
+ */
17
+ declare const matchRoute: (pattern: string | RegExp, path: string) => RouteParams | null;
18
+
19
+ export { compileTemplate, matchRoute, toRegExp };
@@ -0,0 +1,46 @@
1
+ const escapeForRegexp = (str) => str.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
2
+ const WILDCARD_GROUP = "wild";
3
+ const compileTemplate = (template) => {
4
+ const token = /(:[A-Za-z_][A-Za-z0-9_]*)|\*/g;
5
+ let src = "";
6
+ let last = 0;
7
+ let m;
8
+ while ((m = token.exec(template)) !== null) {
9
+ src += escapeForRegexp(template.slice(last, m.index));
10
+ src += m[1] ? `(?<${m[1].slice(1)}>[^/]+)` : `(?<${WILDCARD_GROUP}>.*)`;
11
+ last = m.index + m[0].length;
12
+ }
13
+ src += escapeForRegexp(template.slice(last));
14
+ return new RegExp(`^${src}$`);
15
+ };
16
+ const templateCache = /* @__PURE__ */ new Map();
17
+ const toRegExp = (pattern) => {
18
+ if (pattern instanceof RegExp) {
19
+ return pattern;
20
+ }
21
+ let compiled = templateCache.get(pattern);
22
+ if (!compiled) {
23
+ compiled = compileTemplate(pattern);
24
+ templateCache.set(pattern, compiled);
25
+ }
26
+ return compiled;
27
+ };
28
+ const matchRoute = (pattern, path) => {
29
+ const match = path.match(toRegExp(pattern));
30
+ if (!match) {
31
+ return null;
32
+ }
33
+ const params = {};
34
+ for (const [key, value] of Object.entries(match.groups ?? {})) {
35
+ if (value !== void 0) {
36
+ params[key === WILDCARD_GROUP ? "*" : key] = value;
37
+ }
38
+ }
39
+ return params;
40
+ };
41
+ export {
42
+ compileTemplate,
43
+ matchRoute,
44
+ toRegExp
45
+ };
46
+ //# sourceMappingURL=routeMatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/routeMatch.ts"],"sourcesContent":["import type { RouteParams } from './RoutingSpec';\n\n// from: https://stackoverflow.com/a/63838890\nconst escapeForRegexp = (str: string): string => str.replace(/[.*+\\-?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n// Internal group name standing in for a `*` wildcard (which is not a valid JS\n// regex group identifier); remapped to the `*` param key after matching.\nconst WILDCARD_GROUP = 'wild';\n\n/**\n * Compile a path template to an anchored RegExp with named groups:\n * `:name` → one non-slash segment `*` → the rest (greedy)\n * A template with neither token is a literal exact match. Raw RegExp patterns\n * never reach here — they are the escape hatch, used as authored.\n */\nexport const compileTemplate = (template: string): RegExp => {\n const token = /(:[A-Za-z_][A-Za-z0-9_]*)|\\*/g;\n let src = '';\n let last = 0;\n let m: RegExpExecArray | null;\n while ((m = token.exec(template)) !== null) {\n src += escapeForRegexp(template.slice(last, m.index));\n src += m[1] ? `(?<${m[1].slice(1)}>[^/]+)` : `(?<${WILDCARD_GROUP}>.*)`;\n last = m.index + m[0].length;\n }\n src += escapeForRegexp(template.slice(last));\n return new RegExp(`^${src}$`);\n};\n\nconst templateCache = new Map<string, RegExp>();\n\n/** Resolve a pattern to a RegExp: templates are compiled (and cached), RegExp passes through. */\nexport const toRegExp = (pattern: string | RegExp): RegExp => {\n if (pattern instanceof RegExp) {\n return pattern;\n }\n let compiled = templateCache.get(pattern);\n if (!compiled) {\n compiled = compileTemplate(pattern);\n templateCache.set(pattern, compiled);\n }\n return compiled;\n};\n\n/**\n * Match a `sandboxPath` against a route pattern. Returns the named params on a\n * match (the `*` wildcard surfaces under the `'*'` key), or `null` otherwise.\n */\nexport const matchRoute = (pattern: string | RegExp, path: string): RouteParams | null => {\n const match = path.match(toRegExp(pattern));\n if (!match) {\n return null;\n }\n const params: RouteParams = {};\n for (const [key, value] of Object.entries(match.groups ?? {})) {\n if (value !== undefined) {\n params[key === WILDCARD_GROUP ? '*' : key] = value;\n }\n }\n return params;\n};\n"],"mappings":"AAGA,MAAM,kBAAkB,CAAC,QAAwB,IAAI,QAAQ,yBAAyB,MAAM;AAI5F,MAAM,iBAAiB;AAQhB,MAAM,kBAAkB,CAAC,aAA6B;AAC3D,QAAM,QAAQ;AACd,MAAI,MAAM;AACV,MAAI,OAAO;AACX,MAAI;AACJ,UAAQ,IAAI,MAAM,KAAK,QAAQ,OAAO,MAAM;AAC1C,WAAO,gBAAgB,SAAS,MAAM,MAAM,EAAE,KAAK,CAAC;AACpD,WAAO,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,YAAY,MAAM,cAAc;AACjE,WAAO,EAAE,QAAQ,EAAE,CAAC,EAAE;AAAA,EACxB;AACA,SAAO,gBAAgB,SAAS,MAAM,IAAI,CAAC;AAC3C,SAAO,IAAI,OAAO,IAAI,GAAG,GAAG;AAC9B;AAEA,MAAM,gBAAgB,oBAAI,IAAoB;AAGvC,MAAM,WAAW,CAAC,YAAqC;AAC5D,MAAI,mBAAmB,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,WAAW,cAAc,IAAI,OAAO;AACxC,MAAI,CAAC,UAAU;AACb,eAAW,gBAAgB,OAAO;AAClC,kBAAc,IAAI,SAAS,QAAQ;AAAA,EACrC;AACA,SAAO;AACT;AAMO,MAAM,aAAa,CAAC,SAA0B,SAAqC;AACxF,QAAM,QAAQ,KAAK,MAAM,SAAS,OAAO,CAAC;AAC1C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,SAAsB,CAAC;AAC7B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,UAAU,CAAC,CAAC,GAAG;AAC7D,QAAI,UAAU,QAAW;AACvB,aAAO,QAAQ,iBAAiB,MAAM,GAAG,IAAI;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
package/dist/routing.cjs CHANGED
@@ -21,12 +21,17 @@ __export(routing_exports, {
21
21
  Router: () => Router,
22
22
  applyRoutingRule: () => applyRoutingRule,
23
23
  navigate: () => navigate,
24
+ renderRoute: () => renderRoute,
25
+ useRoute: () => useRoute,
26
+ useRouteParams: () => useRouteParams,
24
27
  useTinkerableLink: () => useTinkerableLink
25
28
  });
26
29
  module.exports = __toCommonJS(routing_exports);
30
+ var import_jsx_runtime = require("react/jsx-runtime");
27
31
  var import_react = require("react");
28
32
  var import_sandboxUtils = require("./sandboxUtils");
29
33
  var import_TinkerableContext = require("./TinkerableContext");
34
+ var import_routeMatch = require("./routeMatch");
30
35
  var import_urlUtils = require("./urlUtils");
31
36
  var import_pathUtils = require("./pathUtils");
32
37
  const useTinkerableLink = (newSandboxLocation) => {
@@ -42,29 +47,42 @@ const useTinkerableLink = (newSandboxLocation) => {
42
47
  const applyRoutingRule = (routingSpec, navigationState) => {
43
48
  const { sandboxPath } = navigationState;
44
49
  for (const routingRule of routingSpec.routes) {
45
- if (typeof routingRule.pattern === "string") {
46
- if (routingRule.pattern === sandboxPath) {
47
- return { routingRule };
48
- }
49
- } else {
50
- const match = sandboxPath.match(routingRule.pattern);
51
- if (routingRule.pattern.test(sandboxPath)) {
52
- return {
53
- routingRule,
54
- pathParameters: match?.groups
55
- };
56
- }
50
+ const pathParameters = (0, import_routeMatch.matchRoute)(routingRule.pattern, sandboxPath);
51
+ if (pathParameters) {
52
+ return { routingRule, pathParameters };
57
53
  }
58
54
  }
59
55
  return void 0;
60
56
  };
57
+ const renderRoute = (routingRule, params) => {
58
+ if (routingRule.component) {
59
+ const Component = routingRule.component;
60
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { params });
61
+ }
62
+ return routingRule.element ?? routingRule.reactNode ?? null;
63
+ };
61
64
  const Router = () => {
62
65
  const context = (0, import_react.useContext)(import_TinkerableContext.TinkerableContext);
63
- const { navigationState: { routingRule } } = context;
66
+ const { navigationState: { routingRule, pathParameters } } = context;
64
67
  if (!routingRule) {
65
68
  throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);
66
69
  }
67
- return routingRule.reactNode;
70
+ return renderRoute(routingRule, pathParameters ?? {});
71
+ };
72
+ const useRouteParams = () => (0, import_react.use)(import_TinkerableContext.TinkerableContext).navigationState.pathParameters ?? {};
73
+ const useRoute = () => {
74
+ const { navigationState } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
75
+ const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;
76
+ return {
77
+ name: routingRule?.name,
78
+ params: pathParameters ?? {},
79
+ sandboxPath,
80
+ mode,
81
+ provider,
82
+ namespace,
83
+ repository,
84
+ ref
85
+ };
68
86
  };
69
87
  const navigate = (target) => {
70
88
  console.log(`[Sandbox] Navigating to ${target}`);
@@ -79,6 +97,9 @@ const navigate = (target) => {
79
97
  Router,
80
98
  applyRoutingRule,
81
99
  navigate,
100
+ renderRoute,
101
+ useRoute,
102
+ useRouteParams,
82
103
  useTinkerableLink
83
104
  });
84
105
  //# sourceMappingURL=routing.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\n\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule,\n pathParameters?: Record<string, string>;\n}\n\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation)\n } else {\n newNavigationState.sandboxPath = newSandboxLocation\n }\n return constructUrl(outerHref, newNavigationState);\n}\n\nexport const applyRoutingRule = (routingSpec:RoutingSpec, navigationState: NavigationState): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n if (typeof routingRule.pattern === 'string') {\n if (routingRule.pattern === sandboxPath) {\n return {routingRule};\n }\n } else {\n const match = sandboxPath.match(routingRule.pattern);\n if (routingRule.pattern.test(sandboxPath)) {\n return {\n routingRule,\n pathParameters: match?.groups\n }\n }\n }\n }\n return undefined;\n}\n\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {navigationState: {routingRule}} = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return routingRule.reactNode;\n};\n\n\n// Perform in-site navigation.\n// Top level frame is messaged to updated URL, after which a message will be\n// sent wit the new href, triggering the actual navigation.\nexport const navigate = (target: string) => {\n console.log(`[Sandbox] Navigating to ${target}`)\n sendMessage('urlchange', {\n url: target,\n back: false,\n forward: false,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,sBAA0D;AAC1D,uBAA0B;AAOnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAEO,MAAM,mBAAmB,CAAC,aAAyB,oBAAqE;AAC7H,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,QAAI,OAAO,YAAY,YAAY,UAAU;AAC3C,UAAI,YAAY,YAAY,aAAa;AACvC,eAAO,EAAC,YAAW;AAAA,MACrB;AAAA,IACF,OAAO;AACL,YAAM,QAAQ,YAAY,MAAM,YAAY,OAAO;AACnD,UAAI,YAAY,QAAQ,KAAK,WAAW,GAAG;AACzC,eAAO;AAAA,UACL;AAAA,UACA,gBAAgB,OAAO;AAAA,QACzB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM,EAAC,iBAAiB,EAAC,YAAW,EAAC,IAAI;AACzC,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY;AACrB;AAMO,MAAM,WAAW,CAAC,WAAmB;AAC1C,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAC/C,uCAAY,aAAa;AAAA,IACvB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,EACX,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule,\n pathParameters?: Record<string, string>;\n}\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation)\n } else {\n newNavigationState.sandboxPath = newSandboxLocation\n }\n return constructUrl(outerHref, newNavigationState);\n}\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (routingSpec:RoutingSpec, navigationState: NavigationState): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n}\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {navigationState: {routingRule, pathParameters}} = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n */\nexport const navigate = (target: string) => {\n console.log(`[Sandbox] Navigating to ${target}`)\n sendMessage('urlchange', {\n url: target,\n back: false,\n forward: false,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CW;AA7CX,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,wBAA2B;AAC3B,sBAA0D;AAC1D,uBAA0B;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAAC,aAAyB,oBAAqE;AAC7H,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,qBAAiB,8BAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,4CAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM,EAAC,iBAAiB,EAAC,aAAa,eAAc,EAAC,IAAI;AACzD,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,UAC3B,kBAAI,0CAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAOO,MAAM,WAAW,CAAC,WAAmB;AAC1C,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAC/C,uCAAY,aAAa;AAAA,IACvB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,EACX,CAAC;AACH;","names":[]}
@@ -1,15 +1,44 @@
1
- import * as react from 'react';
1
+ import { ReactNode } from 'react';
2
2
  import { NavigationState } from './TinkerableContext.cjs';
3
- import { RoutingRule, RoutingSpec } from './RoutingSpec.cjs';
3
+ import { RoutingRule, RoutingSpec, RouteParams } from './RoutingSpec.cjs';
4
4
  import './sandboxTypes.cjs';
5
5
 
6
+ /** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */
6
7
  type AppliedRoutingRule = {
7
8
  routingRule: RoutingRule;
8
9
  pathParameters?: Record<string, string>;
9
10
  };
11
+ /** Build the full outer href for an in-app target (absolute `sandboxPath` or a
12
+ * path relative to the current route), e.g. for an `href` attribute. */
10
13
  declare const useTinkerableLink: (newSandboxLocation: string) => string;
14
+ /** Find the first rule in `routingSpec` whose pattern matches the current
15
+ * `sandboxPath`, returning it with the captured params (or `undefined`). */
11
16
  declare const applyRoutingRule: (routingSpec: RoutingSpec, navigationState: NavigationState) => AppliedRoutingRule | undefined;
12
- declare const Router: () => react.ReactNode;
17
+ /** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */
18
+ declare const renderRoute: (routingRule: RoutingRule, params: RouteParams) => ReactNode;
19
+ /** Render the route matched for the current location (set up by `boot`'s route table). */
20
+ declare const Router: () => ReactNode;
21
+ /** Read the current route's matched params (`:name` segments and the `*` wildcard). */
22
+ declare const useRouteParams: <T extends RouteParams = RouteParams>() => T;
23
+ /**
24
+ * Read the current route: the matched rule's `name`, its `params`, the app-owned
25
+ * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,
26
+ * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.
27
+ */
28
+ declare const useRoute: () => {
29
+ name: string | undefined;
30
+ params: RouteParams;
31
+ sandboxPath: string;
32
+ mode: string;
33
+ provider: string;
34
+ namespace: string;
35
+ repository: string;
36
+ ref: string;
37
+ };
38
+ /**
39
+ * Navigate within the app. Messages the host to update the URL; the host then
40
+ * pushes the new href back, which drives the actual route change.
41
+ */
13
42
  declare const navigate: (target: string) => void;
14
43
 
15
- export { type AppliedRoutingRule, Router, applyRoutingRule, navigate, useTinkerableLink };
44
+ export { type AppliedRoutingRule, Router, applyRoutingRule, navigate, renderRoute, useRoute, useRouteParams, useTinkerableLink };
package/dist/routing.d.ts CHANGED
@@ -1,15 +1,44 @@
1
- import * as react from 'react';
1
+ import { ReactNode } from 'react';
2
2
  import { NavigationState } from './TinkerableContext.js';
3
- import { RoutingRule, RoutingSpec } from './RoutingSpec.js';
3
+ import { RoutingRule, RoutingSpec, RouteParams } from './RoutingSpec.js';
4
4
  import './sandboxTypes.js';
5
5
 
6
+ /** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */
6
7
  type AppliedRoutingRule = {
7
8
  routingRule: RoutingRule;
8
9
  pathParameters?: Record<string, string>;
9
10
  };
11
+ /** Build the full outer href for an in-app target (absolute `sandboxPath` or a
12
+ * path relative to the current route), e.g. for an `href` attribute. */
10
13
  declare const useTinkerableLink: (newSandboxLocation: string) => string;
14
+ /** Find the first rule in `routingSpec` whose pattern matches the current
15
+ * `sandboxPath`, returning it with the captured params (or `undefined`). */
11
16
  declare const applyRoutingRule: (routingSpec: RoutingSpec, navigationState: NavigationState) => AppliedRoutingRule | undefined;
12
- declare const Router: () => react.ReactNode;
17
+ /** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */
18
+ declare const renderRoute: (routingRule: RoutingRule, params: RouteParams) => ReactNode;
19
+ /** Render the route matched for the current location (set up by `boot`'s route table). */
20
+ declare const Router: () => ReactNode;
21
+ /** Read the current route's matched params (`:name` segments and the `*` wildcard). */
22
+ declare const useRouteParams: <T extends RouteParams = RouteParams>() => T;
23
+ /**
24
+ * Read the current route: the matched rule's `name`, its `params`, the app-owned
25
+ * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,
26
+ * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.
27
+ */
28
+ declare const useRoute: () => {
29
+ name: string | undefined;
30
+ params: RouteParams;
31
+ sandboxPath: string;
32
+ mode: string;
33
+ provider: string;
34
+ namespace: string;
35
+ repository: string;
36
+ ref: string;
37
+ };
38
+ /**
39
+ * Navigate within the app. Messages the host to update the URL; the host then
40
+ * pushes the new href back, which drives the actual route change.
41
+ */
13
42
  declare const navigate: (target: string) => void;
14
43
 
15
- export { type AppliedRoutingRule, Router, applyRoutingRule, navigate, useTinkerableLink };
44
+ export { type AppliedRoutingRule, Router, applyRoutingRule, navigate, renderRoute, useRoute, useRouteParams, useTinkerableLink };