@storystick/preview 0.0.0-stage → 0.2.0-rc.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 (245) hide show
  1. package/LICENSE +112 -0
  2. package/PUBLIC-API.md +55 -0
  3. package/README.md +42 -2
  4. package/dist/.source-hash +1 -0
  5. package/dist/authoring/runtime-contract.d.ts +89 -0
  6. package/dist/authoring/runtime-contract.js +28 -0
  7. package/dist/channel-taxonomy.d.ts +17 -0
  8. package/dist/channel-taxonomy.js +46 -0
  9. package/dist/context-command.d.ts +18 -0
  10. package/dist/context-command.js +38 -0
  11. package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
  12. package/dist/dev/suspicious-codeword-detector.js +220 -0
  13. package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
  14. package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
  15. package/dist/framework/browser-context.d.ts +2 -0
  16. package/dist/framework/browser-context.js +2 -0
  17. package/dist/framework/generation-refresh.d.ts +23 -0
  18. package/dist/framework/generation-refresh.js +82 -0
  19. package/dist/framework/generation-signal.d.ts +8 -0
  20. package/dist/framework/generation-signal.js +17 -0
  21. package/dist/framework/projection-generation-watch.d.ts +5 -0
  22. package/dist/framework/projection-generation-watch.js +53 -0
  23. package/dist/framework/render-receipt.d.ts +8 -0
  24. package/dist/framework/render-receipt.js +10 -0
  25. package/dist/framework/runtime-adapter-store.d.ts +30 -0
  26. package/dist/framework/runtime-adapter-store.js +81 -0
  27. package/dist/grant-coordinator.d.ts +53 -0
  28. package/dist/grant-coordinator.js +329 -0
  29. package/dist/grant-lifecycle.d.ts +52 -0
  30. package/dist/grant-lifecycle.js +74 -0
  31. package/dist/identity/__test-adapter.d.ts +34 -0
  32. package/dist/identity/__test-adapter.js +59 -0
  33. package/dist/identity/browser-context-gate.d.ts +4 -0
  34. package/dist/identity/browser-context-gate.js +22 -0
  35. package/dist/identity/browser-context.d.ts +13 -0
  36. package/dist/identity/browser-context.js +22 -0
  37. package/dist/identity/carriers/postmessage.d.ts +84 -0
  38. package/dist/identity/carriers/postmessage.js +177 -0
  39. package/dist/identity/carriers/storage-state.d.ts +41 -0
  40. package/dist/identity/carriers/storage-state.js +99 -0
  41. package/dist/identity/carriers/storage.d.ts +39 -0
  42. package/dist/identity/carriers/storage.js +182 -0
  43. package/dist/identity/carriers/url.d.ts +19 -0
  44. package/dist/identity/carriers/url.js +38 -0
  45. package/dist/identity/identity-state-machine.d.ts +70 -0
  46. package/dist/identity/identity-state-machine.js +244 -0
  47. package/dist/identity/identity-types.d.ts +192 -0
  48. package/dist/identity/identity-types.js +22 -0
  49. package/dist/identity/index.d.ts +18 -0
  50. package/dist/identity/index.js +16 -0
  51. package/dist/identity/preview-cookie.d.ts +9 -0
  52. package/dist/identity/preview-cookie.js +12 -0
  53. package/dist/identity/preview-draft.d.ts +6 -0
  54. package/dist/identity/preview-draft.js +25 -0
  55. package/dist/identity/preview-identity.d.ts +28 -0
  56. package/dist/identity/preview-identity.js +84 -0
  57. package/dist/identity/preview-scope.d.ts +28 -0
  58. package/dist/identity/preview-scope.js +47 -0
  59. package/dist/identity/resolver.d.ts +110 -0
  60. package/dist/identity/resolver.js +334 -0
  61. package/dist/identity/session-query.d.ts +4 -0
  62. package/dist/identity/session-query.js +19 -0
  63. package/dist/index.d.ts +90 -0
  64. package/dist/index.js +788 -0
  65. package/dist/interaction-mode.d.ts +58 -0
  66. package/dist/interaction-mode.js +93 -0
  67. package/dist/nav-handler.d.ts +95 -0
  68. package/dist/nav-handler.js +84 -0
  69. package/dist/overlay-label.d.ts +79 -0
  70. package/dist/overlay-label.js +73 -0
  71. package/dist/provenance/anchor-map.d.ts +95 -0
  72. package/dist/provenance/anchor-map.js +177 -0
  73. package/dist/provenance/carrier-plan.d.ts +51 -0
  74. package/dist/provenance/carrier-plan.js +90 -0
  75. package/dist/provenance/coded-text.d.ts +20 -0
  76. package/dist/provenance/coded-text.js +302 -0
  77. package/dist/provenance/direct-edit-locator.d.ts +29 -0
  78. package/dist/provenance/direct-edit-locator.js +218 -0
  79. package/dist/provenance/editable-leaf.d.ts +134 -0
  80. package/dist/provenance/editable-leaf.js +483 -0
  81. package/dist/provenance/editor-target.d.ts +29 -0
  82. package/dist/provenance/editor-target.js +46 -0
  83. package/dist/provenance/field-leaf-walker.d.ts +21 -0
  84. package/dist/provenance/field-leaf-walker.js +98 -0
  85. package/dist/provenance/frame-dependencies.d.ts +212 -0
  86. package/dist/provenance/frame-dependencies.js +550 -0
  87. package/dist/provenance/index.d.ts +26 -0
  88. package/dist/provenance/index.js +26 -0
  89. package/dist/provenance/local-events.d.ts +1 -0
  90. package/dist/provenance/local-events.js +1 -0
  91. package/dist/provenance/markers.d.ts +21 -0
  92. package/dist/provenance/markers.js +70 -0
  93. package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
  94. package/dist/provenance/multi-frame-anchor-map.js +109 -0
  95. package/dist/provenance/normalize.d.ts +2 -0
  96. package/dist/provenance/normalize.js +19 -0
  97. package/dist/provenance/overlay-focus-echo.d.ts +17 -0
  98. package/dist/provenance/overlay-focus-echo.js +16 -0
  99. package/dist/provenance/overlay-projection.d.ts +65 -0
  100. package/dist/provenance/overlay-projection.js +201 -0
  101. package/dist/provenance/owner-map.d.ts +55 -0
  102. package/dist/provenance/owner-map.js +239 -0
  103. package/dist/provenance/read-carrier.d.ts +22 -0
  104. package/dist/provenance/read-carrier.js +92 -0
  105. package/dist/provenance/recorder.d.ts +38 -0
  106. package/dist/provenance/recorder.js +82 -0
  107. package/dist/provenance/render-context.d.ts +106 -0
  108. package/dist/provenance/render-context.js +338 -0
  109. package/dist/provenance/reveal-contract.d.ts +44 -0
  110. package/dist/provenance/reveal-contract.js +1 -0
  111. package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
  112. package/dist/provenance/richtext-slice-embed-address.js +35 -0
  113. package/dist/provenance/runtime-script.d.ts +1 -0
  114. package/dist/provenance/runtime-script.js +2217 -0
  115. package/dist/provenance/sidecar-store.d.ts +63 -0
  116. package/dist/provenance/sidecar-store.js +151 -0
  117. package/dist/provenance/sidecar.d.ts +34 -0
  118. package/dist/provenance/sidecar.js +227 -0
  119. package/dist/provenance/slice-model.d.ts +28 -0
  120. package/dist/provenance/slice-model.js +86 -0
  121. package/dist/provenance/source-identity.d.ts +3 -0
  122. package/dist/provenance/source-identity.js +7 -0
  123. package/dist/provenance/source-map-materializer.d.ts +76 -0
  124. package/dist/provenance/source-map-materializer.js +286 -0
  125. package/dist/provenance/source-registry.d.ts +103 -0
  126. package/dist/provenance/source-registry.js +562 -0
  127. package/dist/provenance/transformed-output-gate.d.ts +161 -0
  128. package/dist/provenance/transformed-output-gate.js +171 -0
  129. package/dist/provenance/types.d.ts +102 -0
  130. package/dist/provenance/types.js +1 -0
  131. package/dist/refresh-policy.d.ts +11 -0
  132. package/dist/refresh-policy.js +14 -0
  133. package/dist/reveal-target.d.ts +8 -0
  134. package/dist/reveal-target.js +47 -0
  135. package/dist/route-paths.d.ts +11 -0
  136. package/dist/route-paths.js +14 -0
  137. package/dist/router-subscriber.d.ts +92 -0
  138. package/dist/router-subscriber.js +44 -0
  139. package/dist/runtime/anchor-types.d.ts +5 -0
  140. package/dist/runtime/anchor-types.js +1 -0
  141. package/dist/runtime/authority-store.d.ts +31 -0
  142. package/dist/runtime/authority-store.js +56 -0
  143. package/dist/runtime/boot.d.ts +52 -0
  144. package/dist/runtime/boot.js +379 -0
  145. package/dist/runtime/content-refresh.d.ts +30 -0
  146. package/dist/runtime/content-refresh.js +68 -0
  147. package/dist/runtime/emit-runtime-error.d.ts +51 -0
  148. package/dist/runtime/emit-runtime-error.js +64 -0
  149. package/dist/runtime/field-proxy.d.ts +112 -0
  150. package/dist/runtime/field-proxy.js +471 -0
  151. package/dist/runtime/form-contract-reporter.d.ts +99 -0
  152. package/dist/runtime/form-contract-reporter.js +391 -0
  153. package/dist/runtime/invalidation-adapter.d.ts +17 -0
  154. package/dist/runtime/invalidation-adapter.js +42 -0
  155. package/dist/runtime/live-query-overlay.d.ts +39 -0
  156. package/dist/runtime/live-query-overlay.js +345 -0
  157. package/dist/runtime/momentary-navigate.d.ts +16 -0
  158. package/dist/runtime/momentary-navigate.js +106 -0
  159. package/dist/runtime/observation-scheduler.d.ts +15 -0
  160. package/dist/runtime/observation-scheduler.js +42 -0
  161. package/dist/runtime/perf-metrics.d.ts +28 -0
  162. package/dist/runtime/perf-metrics.js +90 -0
  163. package/dist/runtime/preview-interaction-policy.d.ts +1 -0
  164. package/dist/runtime/preview-interaction-policy.js +103 -0
  165. package/dist/runtime/preview-overlay-painter.d.ts +27 -0
  166. package/dist/runtime/preview-overlay-painter.js +1732 -0
  167. package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
  168. package/dist/runtime/preview-overlay-sidecar.js +45 -0
  169. package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
  170. package/dist/runtime/preview-perspective-alignment.js +40 -0
  171. package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
  172. package/dist/runtime/preview-perspective-intent-state.js +5 -0
  173. package/dist/runtime/preview-transition-state.d.ts +142 -0
  174. package/dist/runtime/preview-transition-state.js +257 -0
  175. package/dist/runtime/render-mode-state.d.ts +51 -0
  176. package/dist/runtime/render-mode-state.js +76 -0
  177. package/dist/runtime/request-id.d.ts +4 -0
  178. package/dist/runtime/request-id.js +8 -0
  179. package/dist/runtime/resolve-schema-field.d.ts +99 -0
  180. package/dist/runtime/resolve-schema-field.js +356 -0
  181. package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
  182. package/dist/runtime/runtime-error-dispatch.js +75 -0
  183. package/dist/runtime/schema-store.d.ts +121 -0
  184. package/dist/runtime/schema-store.js +667 -0
  185. package/dist/runtime/snapshot-assembly.d.ts +104 -0
  186. package/dist/runtime/snapshot-assembly.js +553 -0
  187. package/dist/runtime/snapshot-reporter.d.ts +55 -0
  188. package/dist/runtime/snapshot-reporter.js +375 -0
  189. package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
  190. package/dist/runtime/standalone-overlay-controller.js +281 -0
  191. package/dist/runtime/store.d.ts +307 -0
  192. package/dist/runtime/store.js +1026 -0
  193. package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
  194. package/dist/runtime/windowed-minimap-projection.js +189 -0
  195. package/dist/runtime/workspace-shortcuts.d.ts +30 -0
  196. package/dist/runtime/workspace-shortcuts.js +188 -0
  197. package/dist/sdk-error-copy.d.ts +48 -0
  198. package/dist/sdk-error-copy.js +182 -0
  199. package/dist/select-cursor.d.ts +2 -0
  200. package/dist/select-cursor.js +23 -0
  201. package/dist/server/admission-cookie.d.ts +38 -0
  202. package/dist/server/admission-cookie.js +122 -0
  203. package/dist/server/control-client.d.ts +29 -0
  204. package/dist/server/control-client.js +22 -0
  205. package/dist/server/control-errors.d.ts +22 -0
  206. package/dist/server/control-errors.js +165 -0
  207. package/dist/server/draft-route.d.ts +12 -0
  208. package/dist/server/draft-route.js +45 -0
  209. package/dist/server/extract-token.d.ts +30 -0
  210. package/dist/server/extract-token.js +98 -0
  211. package/dist/server/grant.d.ts +128 -0
  212. package/dist/server/grant.js +395 -0
  213. package/dist/server/ordinary-read-provenance.d.ts +11 -0
  214. package/dist/server/ordinary-read-provenance.js +124 -0
  215. package/dist/server/preview-read-metadata.d.ts +10 -0
  216. package/dist/server/preview-read-metadata.js +27 -0
  217. package/dist/server/provenance-frame-builder.d.ts +53 -0
  218. package/dist/server/provenance-frame-builder.js +102 -0
  219. package/dist/server/provenance-runtime.d.ts +70 -0
  220. package/dist/server/provenance-runtime.js +46 -0
  221. package/dist/server/read-context-command.d.ts +11 -0
  222. package/dist/server/read-context-command.js +53 -0
  223. package/dist/server/read-identity.d.ts +3 -0
  224. package/dist/server/read-identity.js +63 -0
  225. package/dist/server/read-lifecycle.d.ts +17 -0
  226. package/dist/server/read-lifecycle.js +52 -0
  227. package/dist/server/render-frame-provenance.d.ts +133 -0
  228. package/dist/server/render-frame-provenance.js +676 -0
  229. package/dist/server/render-id.d.ts +15 -0
  230. package/dist/server/render-id.js +22 -0
  231. package/dist/server/route-document-provenance.d.ts +77 -0
  232. package/dist/server/route-document-provenance.js +1327 -0
  233. package/dist/server/route-session-cookie.d.ts +33 -0
  234. package/dist/server/route-session-cookie.js +54 -0
  235. package/dist/session-projections.d.ts +33 -0
  236. package/dist/session-projections.js +43 -0
  237. package/dist/session-subscriber.d.ts +124 -0
  238. package/dist/session-subscriber.js +79 -0
  239. package/dist/sha256-fallback.d.ts +30 -0
  240. package/dist/sha256-fallback.js +126 -0
  241. package/dist/types.d.ts +446 -0
  242. package/dist/types.js +85 -0
  243. package/dist/validators.d.ts +61 -0
  244. package/dist/validators.js +110 -0
  245. package/package.json +206 -3
@@ -0,0 +1,44 @@
1
+ const NOOP = () => {
2
+ /* default — no adapter registered */
3
+ };
4
+ function subscriberCell() {
5
+ const existing = globalThis.__storystick_preview_router_subscriber__;
6
+ if (existing)
7
+ return existing;
8
+ const created = { subscriber: null };
9
+ globalThis.__storystick_preview_router_subscriber__ = created;
10
+ return created;
11
+ }
12
+ /**
13
+ * Register the router subscriber. Replaces any previously registered
14
+ * subscriber. Pass `null` to reset to the default no-op.
15
+ */
16
+ export function setRouterSubscriber(fn) {
17
+ subscriberCell().subscriber = fn;
18
+ }
19
+ /**
20
+ * Read the currently-registered subscriber. Returns `null` before any
21
+ * adapter has called `setRouterSubscriber`. Consumers gate their
22
+ * invocation on the non-null return.
23
+ */
24
+ export function getRouterSubscriber() {
25
+ return subscriberCell().subscriber;
26
+ }
27
+ /**
28
+ * Convenience for `activate()`'s router-transition path: invoke the
29
+ * currently-registered subscriber if any, no-op otherwise. Centralises
30
+ * the null-check so callers don't repeat it.
31
+ */
32
+ export function notifyRouterTransition(transition) {
33
+ if (typeof window !== 'undefined')
34
+ window.dispatchEvent(new CustomEvent('storystick.preview.location-committed', { detail: transition }));
35
+ const fn = subscriberCell().subscriber ?? NOOP;
36
+ fn(transition);
37
+ }
38
+ /**
39
+ * Test-only reset. Vitest isolates modules per file but multiple
40
+ * cases inside one file would otherwise share state.
41
+ */
42
+ export function _resetRouterSubscriberForTests() {
43
+ subscriberCell().subscriber = null;
44
+ }
@@ -0,0 +1,5 @@
1
+ /** Conserved deliberately: PathAnchor carries keyed annotation-model hops. */
2
+ export interface PathAnchor {
3
+ pathPrefix: string;
4
+ key: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { Client } from "@storystick/client";
2
+ import type { ConvexReactClient } from "convex/react";
3
+ import type { PreviewReadContext } from "@storystick/types/preview-query-targeting";
4
+ /**
5
+ * The narrow Preview authority projection consumed by query runtimes.
6
+ *
7
+ * Keep this module value-import free: production query hooks can depend on it
8
+ * without pulling the Preview overlay, identity resolver, or grant coordinator
9
+ * into their bundle.
10
+ */
11
+ export interface PreviewAuthoritySnapshot {
12
+ readonly client: ConvexReactClient | null;
13
+ readonly httpClient: Client | null;
14
+ readonly sessionId: string | null;
15
+ readonly previewToken: string | null;
16
+ readonly variantId: string | null;
17
+ readonly releaseId: string | null;
18
+ readonly readContext: PreviewReadContext | null;
19
+ }
20
+ interface PreviewAuthorityCell {
21
+ snapshot: PreviewAuthoritySnapshot;
22
+ listeners: Set<() => void>;
23
+ }
24
+ declare global {
25
+ var __storystick_preview_authority__: PreviewAuthorityCell | undefined;
26
+ }
27
+ export declare function getPreviewAuthority(): PreviewAuthoritySnapshot;
28
+ export declare function subscribePreviewAuthority(listener: () => void): () => void;
29
+ /** Runtime bridge. Publish a complete projection to avoid mixed authority. */
30
+ export declare function publishPreviewAuthority(next: PreviewAuthoritySnapshot): void;
31
+ export {};
@@ -0,0 +1,56 @@
1
+ const EMPTY_AUTHORITY = Object.freeze({
2
+ client: null,
3
+ httpClient: null,
4
+ sessionId: null,
5
+ previewToken: null,
6
+ variantId: null,
7
+ releaseId: null,
8
+ readContext: null,
9
+ });
10
+ function authorityCell() {
11
+ const existing = globalThis.__storystick_preview_authority__;
12
+ if (existing) {
13
+ if (!(existing.listeners instanceof Set))
14
+ existing.listeners = new Set();
15
+ return existing;
16
+ }
17
+ const created = {
18
+ snapshot: EMPTY_AUTHORITY,
19
+ listeners: new Set(),
20
+ };
21
+ globalThis.__storystick_preview_authority__ = created;
22
+ return created;
23
+ }
24
+ export function getPreviewAuthority() {
25
+ return authorityCell().snapshot;
26
+ }
27
+ export function subscribePreviewAuthority(listener) {
28
+ const cell = authorityCell();
29
+ cell.listeners.add(listener);
30
+ return () => {
31
+ cell.listeners.delete(listener);
32
+ };
33
+ }
34
+ /** Runtime bridge. Publish a complete projection to avoid mixed authority. */
35
+ export function publishPreviewAuthority(next) {
36
+ const cell = authorityCell();
37
+ const current = cell.snapshot;
38
+ if (current.client === next.client &&
39
+ current.httpClient === next.httpClient &&
40
+ current.sessionId === next.sessionId &&
41
+ current.previewToken === next.previewToken &&
42
+ current.variantId === next.variantId &&
43
+ current.releaseId === next.releaseId &&
44
+ current.readContext === next.readContext) {
45
+ return;
46
+ }
47
+ cell.snapshot = Object.freeze({ ...next });
48
+ for (const listener of cell.listeners) {
49
+ try {
50
+ listener();
51
+ }
52
+ catch {
53
+ /* defensive — one observer must not break the authority fan-out */
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ADR-131 Amendment 002 + chantier-decomposition.md §3 — iframe runtime
3
+ * boot orchestrator.
4
+ *
5
+ * Wires schema delivery and preview interaction listeners into a coherent
6
+ * runtime. Called
7
+ * by the iframe-side `<Storystick />` adapter (`@storystick/next`) in
8
+ * preview mode.
9
+ *
10
+ * ┌────────────────────────────────────────────────────────────────┐
11
+ * │ bootIframeRuntime(opts) — once per <Storystick /> mount │
12
+ * │ │
13
+ * │ bootSchema + retry loop │
14
+ * │ │
15
+ * │ window.message → overlay / v2 handlers │
16
+ * │ │
17
+ * │ Returns teardown() — caller invokes on HMR / unmount. │
18
+ * └────────────────────────────────────────────────────────────────┘
19
+ *
20
+ * Idempotent : a second `bootIframeRuntime()` call is a noop while the
21
+ * first is still active (returns the same teardown).
22
+ */
23
+ declare global {
24
+ interface Window {
25
+ __storystickPreviewOverlayMapOwner?: "boot";
26
+ }
27
+ }
28
+ export interface BootIframeRuntimeOpts {
29
+ /** Document under observation. Defaults to `window.document`. */
30
+ document?: Document;
31
+ }
32
+ export interface IframeRuntimeHandle {
33
+ /** Tear down the runtime and release listeners. */
34
+ teardown(): void;
35
+ }
36
+ /**
37
+ * Boot the iframe runtime. Idempotent — calling a second time before
38
+ * `teardown()` returns the existing handle. Concurrent first-time
39
+ * callers share a single boot promise (StrictMode double-mount safe).
40
+ *
41
+ * Returns `null` if any prerequisite is genuinely missing (no Document,
42
+ * no window). The `<Storystick />` adapter's preview mode is gated
43
+ * server-side, so this path only fires in environments where the
44
+ * window exists.
45
+ */
46
+ export declare function bootIframeRuntime(opts?: BootIframeRuntimeOpts): Promise<IframeRuntimeHandle | null>;
47
+ /**
48
+ * Test hook — clear the active handle, invoking teardown if present.
49
+ * Required when tests open + tear down many runtimes in sequence and
50
+ * need a clean module state.
51
+ */
52
+ export declare function _resetBootForTestsOnly(): void;
@@ -0,0 +1,379 @@
1
+ /**
2
+ * ADR-131 Amendment 002 + chantier-decomposition.md §3 — iframe runtime
3
+ * boot orchestrator.
4
+ *
5
+ * Wires schema delivery and preview interaction listeners into a coherent
6
+ * runtime. Called
7
+ * by the iframe-side `<Storystick />` adapter (`@storystick/next`) in
8
+ * preview mode.
9
+ *
10
+ * ┌────────────────────────────────────────────────────────────────┐
11
+ * │ bootIframeRuntime(opts) — once per <Storystick /> mount │
12
+ * │ │
13
+ * │ bootSchema + retry loop │
14
+ * │ │
15
+ * │ window.message → overlay / v2 handlers │
16
+ * │ │
17
+ * │ Returns teardown() — caller invokes on HMR / unmount. │
18
+ * └────────────────────────────────────────────────────────────────┘
19
+ *
20
+ * Idempotent : a second `bootIframeRuntime()` call is a noop while the
21
+ * first is still active (returns the same teardown).
22
+ */
23
+ import { bootSchema, startSchemaRetryLoop } from "./schema-store.js";
24
+ import { previewRuntime } from "./store.js";
25
+ import { applyPreviewOverlayMap, clearPreviewOverlayPaint, installPreviewEditInteractionGuard, revealPreviewV2SourceInIframe, } from "./preview-overlay-painter.js";
26
+ import { installPreviewWorkspaceShortcuts } from "./workspace-shortcuts.js";
27
+ import { installMomentaryNavigateKeyRelay } from "./momentary-navigate.js";
28
+ import { validateOverlayClear, validateOverlayMap, validatePreviewV2RevealSource, } from "../validators.js";
29
+ import { setInteractionMode } from "../interaction-mode.js";
30
+ import { getReloadHandler } from "../nav-handler.js";
31
+ import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, validatePreviewEnvelope, } from "@storystick/transport-allowlist";
32
+ // ── Module-scoped singletons (forward-compat with HMR + StrictMode) ──
33
+ let active = null;
34
+ // Boot in-flight slot. Set SYNCHRONOUSLY at the start of bootIframeRuntime
35
+ // (before the first await) so concurrent callers — StrictMode double-mount,
36
+ // HMR re-mount — observe the same boot promise instead of racing to open
37
+ // duplicate listeners. Released once boot resolves
38
+ // (whether to a handle or null).
39
+ let bootInFlight = null;
40
+ function cryptoRandomHex() {
41
+ const bytes = new Uint8Array(16);
42
+ if (typeof crypto !== "undefined" &&
43
+ typeof crypto.getRandomValues === "function") {
44
+ crypto.getRandomValues(bytes);
45
+ }
46
+ else {
47
+ for (let index = 0; index < bytes.length; index++) {
48
+ bytes[index] = Math.floor(Math.random() * 256);
49
+ }
50
+ }
51
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
52
+ }
53
+ function emitOverlayReady(view, payload) {
54
+ const identity = previewRuntime.getState().identity;
55
+ if (identity === null || identity.parentOrigin === null)
56
+ return;
57
+ const envelope = {
58
+ namespace: PREVIEW_NAMESPACE,
59
+ version: PREVIEW_PROTOCOL_VERSION,
60
+ type: "storystick.preview.overlay-ready",
61
+ sessionId: identity.sessionId,
62
+ projectId: identity.projectId,
63
+ requestId: cryptoRandomHex(),
64
+ nonce: cryptoRandomHex(),
65
+ sentAt: Date.now(),
66
+ payload,
67
+ };
68
+ view.parent.postMessage(envelope, identity.parentOrigin);
69
+ }
70
+ function emitOverlayClick(view, payload) {
71
+ const identity = previewRuntime.getState().identity;
72
+ if (identity === null || identity.parentOrigin === null)
73
+ return;
74
+ const envelope = {
75
+ namespace: PREVIEW_NAMESPACE,
76
+ version: PREVIEW_PROTOCOL_VERSION,
77
+ type: "storystick.preview.overlay-click",
78
+ sessionId: identity.sessionId,
79
+ projectId: identity.projectId,
80
+ requestId: cryptoRandomHex(),
81
+ nonce: cryptoRandomHex(),
82
+ sentAt: Date.now(),
83
+ payload,
84
+ };
85
+ view.parent.postMessage(envelope, identity.parentOrigin);
86
+ }
87
+ function emitIframeInteraction(view, payload) {
88
+ const identity = previewRuntime.getState().identity;
89
+ if (identity === null || identity.parentOrigin === null)
90
+ return;
91
+ const envelope = {
92
+ namespace: PREVIEW_NAMESPACE,
93
+ version: PREVIEW_PROTOCOL_VERSION,
94
+ type: "storystick.preview.iframe-interaction",
95
+ sessionId: identity.sessionId,
96
+ projectId: identity.projectId,
97
+ requestId: cryptoRandomHex(),
98
+ nonce: cryptoRandomHex(),
99
+ sentAt: Date.now(),
100
+ payload,
101
+ };
102
+ view.parent.postMessage(envelope, identity.parentOrigin);
103
+ }
104
+ function emitPreviewV2RevealResult(view, payload) {
105
+ const identity = previewRuntime.getState().identity;
106
+ if (identity === null || identity.parentOrigin === null)
107
+ return;
108
+ const envelope = {
109
+ namespace: PREVIEW_NAMESPACE,
110
+ version: PREVIEW_PROTOCOL_VERSION,
111
+ type: "storystick.preview.v2.reveal-result",
112
+ sessionId: identity.sessionId,
113
+ projectId: identity.projectId,
114
+ requestId: cryptoRandomHex(),
115
+ nonce: cryptoRandomHex(),
116
+ sentAt: Date.now(),
117
+ payload,
118
+ };
119
+ view.parent.postMessage(envelope, identity.parentOrigin);
120
+ }
121
+ /**
122
+ * Boot the iframe runtime. Idempotent — calling a second time before
123
+ * `teardown()` returns the existing handle. Concurrent first-time
124
+ * callers share a single boot promise (StrictMode double-mount safe).
125
+ *
126
+ * Returns `null` if any prerequisite is genuinely missing (no Document,
127
+ * no window). The `<Storystick />` adapter's preview mode is gated
128
+ * server-side, so this path only fires in environments where the
129
+ * window exists.
130
+ */
131
+ export function bootIframeRuntime(opts) {
132
+ // Codex R1 fix — set the in-flight slot SYNCHRONOUSLY before any
133
+ // await so concurrent callers (StrictMode double-mount, HMR
134
+ // re-mount) share a single boot instead of racing to open duplicate
135
+ // caches / channels / listeners.
136
+ if (active !== null)
137
+ return Promise.resolve(active);
138
+ if (bootInFlight !== null)
139
+ return bootInFlight;
140
+ bootInFlight = (async () => {
141
+ // Preserve the in-flight slot's ordering now that boot has no I/O await:
142
+ // the caller must assign this promise before the finally block clears it.
143
+ await Promise.resolve();
144
+ let keepOverlayMapOwner = false;
145
+ let releaseOverlayMapOwner = () => {
146
+ /* unset until a Window is available */
147
+ };
148
+ try {
149
+ const doc = opts?.document ?? (typeof document !== "undefined" ? document : null);
150
+ if (doc === null)
151
+ return null;
152
+ const view = doc.defaultView ?? (typeof window !== "undefined" ? window : null);
153
+ if (view !== null) {
154
+ const previousOverlayMapOwner = view.__storystickPreviewOverlayMapOwner;
155
+ let released = false;
156
+ view.__storystickPreviewOverlayMapOwner = "boot";
157
+ releaseOverlayMapOwner = () => {
158
+ if (released)
159
+ return;
160
+ released = true;
161
+ if (previousOverlayMapOwner === undefined) {
162
+ delete view.__storystickPreviewOverlayMapOwner;
163
+ }
164
+ else {
165
+ view.__storystickPreviewOverlayMapOwner = previousOverlayMapOwner;
166
+ }
167
+ };
168
+ }
169
+ // 1. Boot schema. Two cooperating mechanisms :
170
+ // - An EAGER `bootSchema()` call : on a fresh iframe with an
171
+ // already-fetched IDB cache (or a fast network), the schema
172
+ // lands in the in-memory record within microseconds. The
173
+ // retry orchestrator's attempt 1 at t=1s then sees the
174
+ // cache hit and bails without any UI signal — fast path
175
+ // preserved (codex R2 P2 fix).
176
+ // - The retry orchestrator owns the full attempt cycle
177
+ // (§9.T7) : 5 attempts at exponential backoff (1s, 2s,
178
+ // 4s, 8s, 16s — attempt 1 at t=1s, attempt 5 at t=31s),
179
+ // with one warning emit at attempt 1 fail and one error
180
+ // emit at attempt 5 fail. Counter resets on httpClient
181
+ // or projectId change.
182
+ void bootSchema();
183
+ const teardownSchemaRetry = startSchemaRetryLoop();
184
+ let teardownEditInteractionGuard = null;
185
+ const ensureEditInteractionGuard = () => {
186
+ if (view === null || teardownEditInteractionGuard !== null)
187
+ return;
188
+ teardownEditInteractionGuard = installPreviewEditInteractionGuard(view, {
189
+ onInteraction: (payload) => emitIframeInteraction(view, payload),
190
+ });
191
+ };
192
+ if (previewRuntime.getState().identity?.parentOrigin !== null) {
193
+ ensureEditInteractionGuard();
194
+ }
195
+ const teardownWorkspaceShortcuts = view === null
196
+ ? () => { }
197
+ : installPreviewWorkspaceShortcuts(view, () => {
198
+ const state = previewRuntime.getState();
199
+ return state.previewEnded ? null : state.identity;
200
+ }, undefined, previewRuntime.subscribeIdentity);
201
+ const teardownMomentaryNavigateKeyRelay = view === null
202
+ ? null
203
+ : installMomentaryNavigateKeyRelay(view, {
204
+ getTargetOrigin: () => previewRuntime.getState().identity?.parentOrigin ?? null,
205
+ });
206
+ // 2. Studio → iframe postMessage path.
207
+ const onMessage = (ev) => {
208
+ if (view === null)
209
+ return;
210
+ const data = ev.data;
211
+ const rawType = data !== null && typeof data === "object"
212
+ ? data.type
213
+ : null;
214
+ if (rawType === "storystick.preview.interaction-mode") {
215
+ const identity = previewRuntime.getState().identity;
216
+ if (identity === null || identity.parentOrigin === null)
217
+ return;
218
+ const base = validatePreviewEnvelope(data);
219
+ if (!base.ok ||
220
+ base.message.type !== "storystick.preview.interaction-mode") {
221
+ return;
222
+ }
223
+ if (ev.origin !== identity.parentOrigin)
224
+ return;
225
+ if (base.message.sessionId !== identity.sessionId)
226
+ return;
227
+ if (base.message.projectId !== identity.projectId)
228
+ return;
229
+ ensureEditInteractionGuard();
230
+ setInteractionMode(base.message.payload.mode);
231
+ return;
232
+ }
233
+ if (rawType === "storystick.preview.reload") {
234
+ const identity = previewRuntime.getState().identity;
235
+ if (identity === null || identity.parentOrigin === null)
236
+ return;
237
+ const base = validatePreviewEnvelope(data);
238
+ if (!base.ok || base.message.type !== "storystick.preview.reload") {
239
+ return;
240
+ }
241
+ if (ev.origin !== identity.parentOrigin)
242
+ return;
243
+ if (base.message.sessionId !== identity.sessionId)
244
+ return;
245
+ if (base.message.projectId !== identity.projectId)
246
+ return;
247
+ getReloadHandler()(base.message.payload.mode, {
248
+ commandId: base.message.payload.commandId,
249
+ });
250
+ return;
251
+ }
252
+ if (rawType === "storystick.preview.overlay-map") {
253
+ const identity = previewRuntime.getState().identity;
254
+ if (identity === null || identity.parentOrigin === null)
255
+ return;
256
+ const overlayMap = validateOverlayMap(data, ev.origin, identity.sessionId, identity.projectId, identity.parentOrigin);
257
+ if (!overlayMap.ok)
258
+ return;
259
+ ensureEditInteractionGuard();
260
+ const result = applyPreviewOverlayMap(overlayMap.envelope.payload, view, {
261
+ onClick: (payload) => emitOverlayClick(view, payload),
262
+ });
263
+ emitOverlayReady(view, {
264
+ generation: result.generation,
265
+ paintedCount: result.paintedCount,
266
+ missingSnapshotItemKeys: result.missingSnapshotItemKeys,
267
+ routeHash: overlayMap.envelope.payload.routeHash,
268
+ codeVersion: overlayMap.envelope.payload.codeVersion,
269
+ renderId: overlayMap.envelope.payload.renderId,
270
+ });
271
+ return;
272
+ }
273
+ if (rawType === "storystick.preview.overlay-clear") {
274
+ const identity = previewRuntime.getState().identity;
275
+ if (identity === null || identity.parentOrigin === null)
276
+ return;
277
+ const overlayClear = validateOverlayClear(data, ev.origin, identity.sessionId, identity.projectId, identity.parentOrigin);
278
+ if (!overlayClear.ok)
279
+ return;
280
+ ensureEditInteractionGuard();
281
+ clearPreviewOverlayPaint();
282
+ return;
283
+ }
284
+ if (rawType === "storystick.preview.v2.reveal-source") {
285
+ const identity = previewRuntime.getState().identity;
286
+ if (identity === null || identity.parentOrigin === null)
287
+ return;
288
+ const revealSource = validatePreviewV2RevealSource(data, ev.origin, identity.sessionId, identity.projectId, identity.parentOrigin);
289
+ if (!revealSource.ok)
290
+ return;
291
+ const result = revealPreviewV2SourceInIframe(revealSource.envelope.payload, view);
292
+ emitPreviewV2RevealResult(view, {
293
+ requestId: revealSource.envelope.payload.requestId,
294
+ sourceKey: revealSource.envelope.payload.sourceKey,
295
+ targetSelection: revealSource.envelope.payload.targetSelection,
296
+ status: result.status,
297
+ revealedCount: result.revealedCount,
298
+ });
299
+ return;
300
+ }
301
+ return;
302
+ };
303
+ if (view !== null) {
304
+ view.addEventListener("message", onMessage);
305
+ }
306
+ // Deliberate annotation seam: the iframe runtime never observes
307
+ // customer DOM output to infer ownership. It reads the annotations
308
+ // authored by SDK resolution functions.
309
+ const handle = {
310
+ teardown() {
311
+ if (active !== handle)
312
+ return;
313
+ try {
314
+ if (view !== null)
315
+ view.removeEventListener("message", onMessage);
316
+ }
317
+ catch {
318
+ /* defensive */
319
+ }
320
+ try {
321
+ teardownEditInteractionGuard?.();
322
+ teardownEditInteractionGuard = null;
323
+ }
324
+ catch {
325
+ /* defensive */
326
+ }
327
+ try {
328
+ teardownWorkspaceShortcuts();
329
+ teardownMomentaryNavigateKeyRelay?.();
330
+ }
331
+ catch {
332
+ /* defensive */
333
+ }
334
+ try {
335
+ clearPreviewOverlayPaint();
336
+ }
337
+ catch {
338
+ /* defensive */
339
+ }
340
+ releaseOverlayMapOwner();
341
+ try {
342
+ teardownSchemaRetry();
343
+ }
344
+ catch {
345
+ /* defensive */
346
+ }
347
+ active = null;
348
+ },
349
+ };
350
+ active = handle;
351
+ keepOverlayMapOwner = true;
352
+ return handle;
353
+ }
354
+ finally {
355
+ if (!keepOverlayMapOwner) {
356
+ releaseOverlayMapOwner();
357
+ }
358
+ bootInFlight = null;
359
+ }
360
+ })();
361
+ return bootInFlight;
362
+ }
363
+ /**
364
+ * Test hook — clear the active handle, invoking teardown if present.
365
+ * Required when tests open + tear down many runtimes in sequence and
366
+ * need a clean module state.
367
+ */
368
+ export function _resetBootForTestsOnly() {
369
+ if (active !== null) {
370
+ try {
371
+ active.teardown();
372
+ }
373
+ catch {
374
+ /* defensive */
375
+ }
376
+ }
377
+ active = null;
378
+ bootInFlight = null;
379
+ }
@@ -0,0 +1,30 @@
1
+ import { type PreviewContentRefreshDecision } from '../provenance/index.js';
2
+ import type { PreviewQueryTargetingSignalV1 } from '@storystick/types/preview-query-targeting';
3
+ export declare const PREVIEW_CONTENT_REFRESH_SOURCE = "preview-session-content-revision";
4
+ export type PreviewContentRefreshReason = 'missed-initial-revision' | 'forward-revision-jump' | 'reactive-dependency-change';
5
+ export interface PreviewContentRefreshEvent {
6
+ /**
7
+ * Contract owner for content refreshes. This intentionally names the
8
+ * Convex preview-session row, not postMessage: content refresh is a
9
+ * data-subscription signal consumed by the customer router adapter.
10
+ */
11
+ source: typeof PREVIEW_CONTENT_REFRESH_SOURCE;
12
+ revision: number;
13
+ previousRevision: number | null;
14
+ reason: PreviewContentRefreshReason;
15
+ refresh: boolean;
16
+ affectedFrameIds: readonly string[];
17
+ decisionReason: PreviewContentRefreshDecision['reason'];
18
+ /**
19
+ * Exact Content Plane source sequence committed before this invalidation.
20
+ * Framework adapters wait until the public projection covers it before
21
+ * replacing a valid server-rendered tree.
22
+ */
23
+ requiredProjectedThroughSeq?: number;
24
+ queryTargeting?: PreviewQueryTargetingSignalV1;
25
+ }
26
+ export interface ContentRevisionObservation {
27
+ observedRevision: number;
28
+ event: PreviewContentRefreshEvent | null;
29
+ }
30
+ export declare function observeContentRevision(previousRevision: number | null, nextRevision: number, decision?: PreviewContentRefreshDecision, requiredProjectedThroughSeq?: number, queryTargetingChanged?: boolean, queryTargeting?: PreviewQueryTargetingSignalV1): ContentRevisionObservation;
@@ -0,0 +1,68 @@
1
+ import { createPreviewContentRefreshDecision, } from '../provenance/index.js';
2
+ export const PREVIEW_CONTENT_REFRESH_SOURCE = 'preview-session-content-revision';
3
+ export function observeContentRevision(previousRevision, nextRevision, decision, requiredProjectedThroughSeq, queryTargetingChanged = false, queryTargeting) {
4
+ const refreshDecision = decision ?? createPreviewContentRefreshDecision({
5
+ refresh: true,
6
+ affectedFrameIds: [],
7
+ reason: 'legacy-refresh',
8
+ });
9
+ if (previousRevision === null) {
10
+ return {
11
+ observedRevision: nextRevision,
12
+ event: nextRevision > 0
13
+ ? {
14
+ source: PREVIEW_CONTENT_REFRESH_SOURCE,
15
+ revision: nextRevision,
16
+ previousRevision: null,
17
+ reason: 'missed-initial-revision',
18
+ refresh: refreshDecision.refresh,
19
+ affectedFrameIds: refreshDecision.affectedFrameIds,
20
+ decisionReason: refreshDecision.reason,
21
+ ...queryTargetingRequirement(queryTargeting),
22
+ ...projectionRequirement(requiredProjectedThroughSeq),
23
+ }
24
+ : null,
25
+ };
26
+ }
27
+ if (nextRevision <= previousRevision) {
28
+ return {
29
+ observedRevision: previousRevision,
30
+ event: decision !== undefined &&
31
+ (refreshDecision.refresh || queryTargetingChanged)
32
+ ? {
33
+ source: PREVIEW_CONTENT_REFRESH_SOURCE,
34
+ revision: previousRevision,
35
+ previousRevision,
36
+ reason: 'reactive-dependency-change',
37
+ refresh: refreshDecision.refresh,
38
+ affectedFrameIds: refreshDecision.affectedFrameIds,
39
+ decisionReason: refreshDecision.reason,
40
+ ...queryTargetingRequirement(queryTargeting),
41
+ ...projectionRequirement(requiredProjectedThroughSeq),
42
+ }
43
+ : null,
44
+ };
45
+ }
46
+ return {
47
+ observedRevision: nextRevision,
48
+ event: {
49
+ source: PREVIEW_CONTENT_REFRESH_SOURCE,
50
+ revision: nextRevision,
51
+ previousRevision,
52
+ reason: 'forward-revision-jump',
53
+ refresh: refreshDecision.refresh,
54
+ affectedFrameIds: refreshDecision.affectedFrameIds,
55
+ decisionReason: refreshDecision.reason,
56
+ ...queryTargetingRequirement(queryTargeting),
57
+ ...projectionRequirement(requiredProjectedThroughSeq),
58
+ },
59
+ };
60
+ }
61
+ function queryTargetingRequirement(value) {
62
+ return value === undefined ? {} : { queryTargeting: value };
63
+ }
64
+ function projectionRequirement(value) {
65
+ return Number.isSafeInteger(value) && value >= 0
66
+ ? { requiredProjectedThroughSeq: value }
67
+ : {};
68
+ }