@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,1026 @@
1
+ import { sendPreviewPerspectiveCommand } from '../context-command.js';
2
+ import { isPreviewReadContext } from '@storystick/types/preview-query-targeting';
3
+ /**
4
+ * `previewRuntime` — module-level singleton (ADR-099 D11 + ADR-116).
5
+ *
6
+ * Five concerns the Provider used to handle, ported to imperative
7
+ * methods on this singleton (D11.1) :
8
+ *
9
+ * 1. Convergent grant lifecycle with a compatibility projection for
10
+ * framework adapters that still consume the historical state field.
11
+ *
12
+ * 2. `httpClient` (ConvexReactClient) lifecycle. Built on first JWT
13
+ * arrival ; disposed via `dispose()`. HMR resets on module reload.
14
+ *
15
+ * 3. In-place Convex auth fetcher fed only by the same-origin grant route.
16
+ *
17
+ * 4. Session subscription gated on token presence (NOT
18
+ * lifecycle state). Survives the `authenticated → refreshing →
19
+ * authenticated` cycle. (Provider's codex Lane D round 10 P1.)
20
+ *
21
+ * 5. `contentRevision` driver. Listeners observe forward jumps and
22
+ * fire `router.refresh()` (the `<Storystick />` Client Component
23
+ * holds the `useRouter()` reference ; the singleton stays
24
+ * framework-agnostic).
25
+ *
26
+ * ADR-116 (PR4) — the identity resolver replaces `defaultDetectPreview`.
27
+ * Boot path :
28
+ *
29
+ * init(opts)
30
+ * → compare-and-set (D5 invariant 1 — set inFlightInitPromise
31
+ * BEFORE notify so a synchronous-reentrant init() observes the
32
+ * slot populated)
33
+ * → resolver.start() resolves the identity via carrier chain
34
+ * (broadcast > storage > URL — ADR-116 D3)
35
+ * → on first identity (provisional or live), start the same-origin grant
36
+ * coordinator and secret-free control plane for that sessionId
37
+ *
38
+ * Full runtime cell persists on `globalThis.__storystick_preview_runtime__`
39
+ * (D5 invariant 2 — codex R6 #3). Persisting only `state` is INSUFFICIENT :
40
+ * Next Fast Refresh re-evaluates the module body but does NOT remount
41
+ * every `useSyncExternalStore` consumer ; old consumers stay subscribed
42
+ * to the OLD module's `listeners` Set. The full cell (state + listeners +
43
+ * revisionListeners + inFlightInitPromise + cleanups + identity resolver
44
+ * handles) on `globalThis` keeps emissions and subscriptions aligned
45
+ * across HMR.
46
+ *
47
+ * Per CSO-PREVIEW-11 — per-tab in-memory only. Module-level `let` state
48
+ * (and the `globalThis` cell) ; no SharedWorker / ServiceWorker /
49
+ * BroadcastChannel / localStorage. The new sessionStorage carrier (PR2)
50
+ * is tier-2 identity persistence within the iframe origin partition —
51
+ * distinct from cross-tab/process IPC.
52
+ */
53
+ import { ConvexReactClient } from "convex/react";
54
+ import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, } from "@storystick/transport-allowlist";
55
+ import { activate } from "../index.js";
56
+ import { createPreviewGrantCoordinator, } from "../grant-coordinator.js";
57
+ import { legacyPreviewAuthState, } from "../grant-lifecycle.js";
58
+ import { isTerminalPreviewSessionErrorCode, subscribePreviewContentSignal, subscribePreviewSession, } from "../session-subscriber.js";
59
+ import { createIdentityResolver, } from "../identity/index.js";
60
+ import { createUrlCarrier } from "../identity/carriers/url.js";
61
+ import { createStorageCarrier, clearStored, clearStandaloneStored, writeStandaloneStored, rememberStandaloneStudioUrl } from "../identity/carriers/storage.js";
62
+ import { observeContentRevision, } from "./content-refresh.js";
63
+ import { publishPreviewAuthority } from "./authority-store.js";
64
+ import { recordPreviewPerfMetric } from "./perf-metrics.js";
65
+ const INITIAL_STATE = {
66
+ state: "idle",
67
+ lifecycle: null,
68
+ client: null,
69
+ previewToken: null,
70
+ cookieLifecycle: null,
71
+ error: null,
72
+ contentRevision: null,
73
+ sessionId: null,
74
+ session: null,
75
+ renderedReadContext: null,
76
+ variantId: null,
77
+ releaseId: null,
78
+ identity: null,
79
+ httpClient: null,
80
+ };
81
+ /**
82
+ * ADR-116 R7 fix — hydrate the cell defensively when an older SDK
83
+ * version (or an HMR intermediate that wrote a partial shape) already
84
+ * populated `globalThis.__storystick_preview_runtime__`. The default-
85
+ * value pattern with `||` only triggered when the slot was undefined ;
86
+ * a partial cell (e.g., one that predates `identityListeners` being
87
+ * added in PR4) would surface as runtime crashes on `.add()` of a
88
+ * `Set` that does not exist. Always backfill missing fields against
89
+ * the current shape before using the cell.
90
+ */
91
+ function hydrateCell(existing) {
92
+ const defaults = {
93
+ state: { ...INITIAL_STATE },
94
+ listeners: new Set(),
95
+ revisionListeners: new Set(),
96
+ contentRefreshListeners: new Set(),
97
+ identityListeners: new Set(),
98
+ inFlightInitPromise: null,
99
+ cleanupActivate: null,
100
+ grantCoordinator: null,
101
+ grantGeneration: 0,
102
+ sessionSnapshotReady: false,
103
+ signalSnapshotReady: false,
104
+ cleanupSubscription: null,
105
+ subscriptionSessionId: null,
106
+ cleanupResolver: null,
107
+ lastObservedRevision: null,
108
+ lastQueryTargetingFingerprint: null,
109
+ pendingRevisionNotification: null,
110
+ pendingContentRefreshEvent: null,
111
+ terminal: false,
112
+ resolver: null,
113
+ authedSessionId: null,
114
+ };
115
+ if (!existing)
116
+ return defaults;
117
+ return {
118
+ state: { ...defaults.state, ...(existing.state ?? {}) },
119
+ listeners: existing.listeners instanceof Set ? existing.listeners : new Set(),
120
+ revisionListeners: existing.revisionListeners instanceof Set
121
+ ? existing.revisionListeners
122
+ : new Set(),
123
+ contentRefreshListeners: existing.contentRefreshListeners instanceof Set
124
+ ? existing.contentRefreshListeners
125
+ : new Set(),
126
+ identityListeners: existing.identityListeners instanceof Set
127
+ ? existing.identityListeners
128
+ : new Set(),
129
+ inFlightInitPromise: existing.inFlightInitPromise ?? null,
130
+ cleanupActivate: existing.cleanupActivate ?? null,
131
+ grantCoordinator: existing.grantCoordinator ?? null,
132
+ grantGeneration: existing.grantGeneration ?? 0,
133
+ sessionSnapshotReady: existing.sessionSnapshotReady ?? false,
134
+ signalSnapshotReady: existing.signalSnapshotReady ?? false,
135
+ cleanupSubscription: existing.cleanupSubscription ?? null,
136
+ cleanupRenderObservation: existing.cleanupRenderObservation,
137
+ subscriptionSessionId: existing.subscriptionSessionId ?? null,
138
+ cleanupResolver: existing.cleanupResolver ?? null,
139
+ lastObservedRevision: existing.lastObservedRevision ?? null,
140
+ lastQueryTargetingFingerprint: existing.lastQueryTargetingFingerprint ?? null,
141
+ pendingRevisionNotification: existing.pendingRevisionNotification ?? null,
142
+ pendingContentRefreshEvent: existing.pendingContentRefreshEvent ?? null,
143
+ terminal: existing.terminal ?? false,
144
+ resolver: existing.resolver ?? null,
145
+ authedSessionId: existing.authedSessionId ?? null,
146
+ };
147
+ }
148
+ function scopeFields(scope) {
149
+ return scope.kind === "variant"
150
+ ? { variantId: scope.variantId, releaseId: null }
151
+ : scope.kind === "release"
152
+ ? { variantId: null, releaseId: scope.releaseId }
153
+ : { variantId: null, releaseId: null };
154
+ }
155
+ const cell = hydrateCell(typeof globalThis !== "undefined"
156
+ ? globalThis.__storystick_preview_runtime__
157
+ : undefined);
158
+ if (typeof globalThis !== "undefined") {
159
+ globalThis.__storystick_preview_runtime__ = cell;
160
+ }
161
+ function publishAuthorityFromState() {
162
+ publishPreviewAuthority({
163
+ client: cell.state.client,
164
+ httpClient: cell.state.httpClient,
165
+ sessionId: cell.state.sessionId,
166
+ previewToken: cell.state.previewToken,
167
+ variantId: cell.state.variantId,
168
+ releaseId: cell.state.releaseId,
169
+ readContext: cell.state.session?.readContext ?? null,
170
+ });
171
+ }
172
+ publishAuthorityFromState();
173
+ let lastReadContextNotification = "";
174
+ function notify() {
175
+ const notification = JSON.stringify([cell.state.sessionId, cell.state.session?.readContext,
176
+ cell.state.session?.readContextStatus, cell.state.state]);
177
+ if (notification !== lastReadContextNotification && typeof window !== "undefined") {
178
+ lastReadContextNotification = notification;
179
+ cell.state = { ...cell.state, renderedReadContext: null };
180
+ window.dispatchEvent(new CustomEvent("storystick.preview.read-context-changed"));
181
+ }
182
+ for (const l of cell.listeners) {
183
+ try {
184
+ l();
185
+ }
186
+ catch {
187
+ /* defensive — listener exceptions never break the fan-out */
188
+ }
189
+ }
190
+ }
191
+ function notifyIdentity(identity) {
192
+ for (const l of cell.identityListeners) {
193
+ try {
194
+ l(identity);
195
+ }
196
+ catch {
197
+ /* defensive */
198
+ }
199
+ }
200
+ }
201
+ /**
202
+ * Sticky-error double-defense (`provider.tsx:278-300` port).
203
+ * - Layer (a) : synchronous flag flipped the moment `'error'` is
204
+ * requested (BEFORE applying the partial).
205
+ * - Layer (b) : assignment short-circuits when current
206
+ * `state.state === 'error'`.
207
+ * Both layers MUST stay in place. (a) alone races on batched updates ;
208
+ * (b) alone misses callbacks captured before state reaches the
209
+ * listener fan-out.
210
+ */
211
+ function _setStatePartial(partial) {
212
+ cell.state = { ...cell.state, ...partial };
213
+ cell.terminal = partial.state === "error";
214
+ publishAuthorityFromState();
215
+ notify();
216
+ }
217
+ function emitContentRefresh(event) {
218
+ if (cell.contentRefreshListeners.size === 0) {
219
+ cell.pendingContentRefreshEvent = event;
220
+ }
221
+ else {
222
+ cell.pendingContentRefreshEvent = null;
223
+ for (const cb of cell.contentRefreshListeners) {
224
+ try {
225
+ cb(event);
226
+ }
227
+ catch {
228
+ /* defensive */
229
+ }
230
+ }
231
+ }
232
+ const revision = event.revision;
233
+ if (cell.revisionListeners.size === 0) {
234
+ cell.pendingRevisionNotification = revision;
235
+ return;
236
+ }
237
+ cell.pendingRevisionNotification = null;
238
+ for (const cb of cell.revisionListeners) {
239
+ try {
240
+ cb(revision);
241
+ }
242
+ catch {
243
+ /* defensive */
244
+ }
245
+ }
246
+ }
247
+ function previewQueryTargetingFingerprint(value) {
248
+ if (!value)
249
+ return 'none';
250
+ return [
251
+ value.version,
252
+ value.service,
253
+ [...value.coveredQueryReceiptIds].sort().join(','),
254
+ [...value.affectedQueryReceiptIds].sort().join(','),
255
+ ].join('|');
256
+ }
257
+ // ─────────────────────────────────────────────
258
+ // Identity resolver wiring
259
+ // ─────────────────────────────────────────────
260
+ function buildResolver(opts) {
261
+ return createIdentityResolver({
262
+ expectedProjectId: opts.projectId,
263
+ carriers: {
264
+ storage: createStorageCarrier(),
265
+ url: createUrlCarrier(),
266
+ },
267
+ });
268
+ }
269
+ /**
270
+ * React to resolver state transitions. Maps `IdentitySnapshot` to
271
+ * `PreviewRuntimeState` and kicks off `activate()` when identity
272
+ * first resolves (or a Replace transition produces a new sessionId).
273
+ */
274
+ function randomEnvelopeId() {
275
+ const bytes = new Uint8Array(16);
276
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
277
+ crypto.getRandomValues(bytes);
278
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
279
+ }
280
+ return `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
281
+ }
282
+ function getLifecycleSnapshot() {
283
+ const lifecycle = cell.state.lifecycle;
284
+ if (!lifecycle)
285
+ return null;
286
+ return {
287
+ ...(cell.state.session?.readContext ? { readContext: cell.state.session.readContext } : {}),
288
+ ...(cell.state.contextCommand ? { contextCommand: cell.state.contextCommand } : {}),
289
+ ...(cell.state.renderedReadContext ? { renderedReadContext: cell.state.renderedReadContext } : {}),
290
+ phase: lifecycle.phase,
291
+ attempt: lifecycle.attempt,
292
+ since: lifecycle.since,
293
+ ...(lifecycle.phase === "ready"
294
+ ? { lastReadyAt: lifecycle.readyAt }
295
+ : lifecycle.phase === "reconnecting"
296
+ ? { lastReadyAt: lifecycle.lastReadyAt }
297
+ : {}),
298
+ ...(lifecycle.phase === "blocked" ? { code: lifecycle.code } : {}),
299
+ };
300
+ }
301
+ function emitLifecycleSnapshot() {
302
+ const lifecycle = cell.state.lifecycle;
303
+ const identity = cell.state.identity;
304
+ if (lifecycle === null ||
305
+ identity === null ||
306
+ identity.parentOrigin === null ||
307
+ typeof window === "undefined" ||
308
+ window.parent === window) {
309
+ return;
310
+ }
311
+ const envelope = {
312
+ namespace: PREVIEW_NAMESPACE,
313
+ version: PREVIEW_PROTOCOL_VERSION,
314
+ type: "storystick.preview.lifecycle",
315
+ sessionId: lifecycle.sessionId,
316
+ projectId: identity.projectId,
317
+ requestId: randomEnvelopeId(),
318
+ nonce: randomEnvelopeId(),
319
+ sentAt: Date.now(),
320
+ payload: getLifecycleSnapshot(),
321
+ };
322
+ window.parent.postMessage(envelope, identity.parentOrigin);
323
+ }
324
+ function setLifecycle(lifecycle) {
325
+ _setStatePartial({
326
+ lifecycle,
327
+ state: legacyPreviewAuthState(lifecycle),
328
+ error: lifecycle.phase === "blocked"
329
+ ? { code: lifecycle.code, message: lifecycle.message }
330
+ : null,
331
+ });
332
+ if (lifecycle.phase === "ready") {
333
+ cell.resolver?.notifyAuthSucceeded();
334
+ }
335
+ else if (lifecycle.phase === "blocked") {
336
+ cell.resolver?.notifyAuthFailed({
337
+ code: lifecycle.code,
338
+ message: lifecycle.message,
339
+ });
340
+ }
341
+ emitLifecycleSnapshot();
342
+ }
343
+ function restartControlPlane(identity, opts) {
344
+ cell.cleanupActivate?.();
345
+ cell.cleanupActivate = activate({
346
+ endpoint: opts.gatewaySyncUrl,
347
+ sessionId: identity.sessionId,
348
+ projectId: opts.projectId,
349
+ parentOrigin: identity.parentOrigin,
350
+ ...(isStandalonePreviewContext() && cell.state.previewToken !== null
351
+ ? { standaloneCapability: true }
352
+ : {}),
353
+ onSessionBroadcast: emitLifecycleSnapshot,
354
+ });
355
+ emitLifecycleSnapshot();
356
+ }
357
+ function isStandalonePreviewContext() {
358
+ if (typeof window === "undefined")
359
+ return false;
360
+ try {
361
+ return window.parent === window;
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ }
367
+ function installGrant(grant, identity, opts, generation) {
368
+ if (cell.grantGeneration !== generation ||
369
+ cell.state.sessionId !== identity.sessionId) {
370
+ return;
371
+ }
372
+ let client = cell.state.client;
373
+ if (client === null) {
374
+ client = new ConvexReactClient(opts.gatewaySyncUrl, {
375
+ skipConvexDeploymentUrlCheck: true,
376
+ expectAuth: true,
377
+ });
378
+ }
379
+ if (isStandalonePreviewContext())
380
+ writeStandaloneStored(identity);
381
+ _setStatePartial({ client, previewToken: grant.token });
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ client.setAuth(async () => cell.state.previewToken);
384
+ // A public `_ss_session` selector is not sufficient to paint overlays in a
385
+ // standalone tab. Start the local control plane only after the origin's
386
+ // HttpOnly preview capability has yielded a valid access token.
387
+ if (isStandalonePreviewContext() && cell.cleanupActivate === null) {
388
+ restartControlPlane(identity, opts);
389
+ }
390
+ wireSessionSubscription(identity.sessionId, client);
391
+ }
392
+ function startGrant(identity, opts) {
393
+ cell.grantCoordinator?.stop();
394
+ cell.grantCoordinator = null;
395
+ cell.cleanupSubscription?.();
396
+ cell.cleanupSubscription = null;
397
+ cell.subscriptionSessionId = null;
398
+ if (cell.state.client !== null) {
399
+ try {
400
+ void cell.state.client.close();
401
+ }
402
+ catch {
403
+ /* defensive */
404
+ }
405
+ }
406
+ cell.grantGeneration += 1;
407
+ const generation = cell.grantGeneration;
408
+ cell.authedSessionId = identity.sessionId;
409
+ cell.sessionSnapshotReady = false;
410
+ cell.signalSnapshotReady = false;
411
+ cell.lastObservedRevision = null;
412
+ cell.lastQueryTargetingFingerprint = null;
413
+ cell.pendingRevisionNotification = null;
414
+ cell.pendingContentRefreshEvent = null;
415
+ _setStatePartial({
416
+ client: null,
417
+ previewToken: null,
418
+ session: null,
419
+ contentRevision: null,
420
+ sessionId: identity.sessionId,
421
+ variantId: null,
422
+ releaseId: null,
423
+ contextCommand: undefined,
424
+ });
425
+ if (isStandalonePreviewContext()) {
426
+ cell.cleanupActivate?.();
427
+ cell.cleanupActivate = null;
428
+ }
429
+ else {
430
+ restartControlPlane(identity, opts);
431
+ }
432
+ const coordinator = createPreviewGrantCoordinator({
433
+ sessionId: identity.sessionId,
434
+ projectId: opts.projectId,
435
+ ...(opts.tokenRoute !== undefined ? { tokenRoute: opts.tokenRoute } : {}),
436
+ generation,
437
+ onGrant: (grant) => installGrant(grant, identity, opts, generation),
438
+ onLifecycle: (lifecycle) => {
439
+ if (cell.grantGeneration !== generation)
440
+ return;
441
+ setLifecycle(lifecycle);
442
+ },
443
+ });
444
+ cell.grantCoordinator = coordinator;
445
+ coordinator.start();
446
+ }
447
+ function onResolverSnapshot(snap, opts) {
448
+ const identity = snap.identity;
449
+ const previousIdentity = cell.state.identity;
450
+ if (identity !== previousIdentity) {
451
+ _setStatePartial({ identity, sessionId: identity?.sessionId ?? null });
452
+ notifyIdentity(identity);
453
+ }
454
+ if (snap.state === "error" && snap.error) {
455
+ _setStatePartial({ state: "error", error: snap.error });
456
+ return;
457
+ }
458
+ if (identity === null)
459
+ return;
460
+ if (cell.authedSessionId !== identity.sessionId) {
461
+ startGrant(identity, opts);
462
+ return;
463
+ }
464
+ if (previousIdentity?.parentOrigin !== identity.parentOrigin) {
465
+ if (!isStandalonePreviewContext() || cell.state.previewToken !== null) {
466
+ restartControlPlane(identity, opts);
467
+ }
468
+ }
469
+ }
470
+ async function init(opts) {
471
+ if (cell.state.previewEnded)
472
+ return;
473
+ if (!cell.cleanupRenderObservation && typeof window !== 'undefined') {
474
+ // Editable query provenance and the framework's server-render receipt
475
+ // attest the same context. An empty page may have only the latter.
476
+ let queryContext = null;
477
+ let frameworkContext = null;
478
+ const clear = () => { queryContext = null; frameworkContext = null; };
479
+ const observe = (event) => {
480
+ const detail = event.detail;
481
+ if (!detail || typeof detail !== 'object' || !('sessionId' in detail) ||
482
+ detail.sessionId !== cell.state.sessionId || !('readContext' in detail))
483
+ return;
484
+ const rendered = detail.readContext;
485
+ const accepted = cell.state.session?.readContext;
486
+ if (rendered !== null && (!isPreviewReadContext(rendered) ||
487
+ cell.state.state !== 'authenticated' || cell.state.session?.readContextStatus !== 'active' ||
488
+ rendered.contextId !== accepted?.contextId || rendered.revision !== accepted?.revision))
489
+ return;
490
+ if ('source' in detail && detail.source === 'framework') {
491
+ frameworkContext = rendered;
492
+ }
493
+ else {
494
+ queryContext = rendered;
495
+ }
496
+ _setStatePartial({ renderedReadContext: frameworkContext ?? queryContext });
497
+ emitLifecycleSnapshot();
498
+ };
499
+ window.addEventListener('storystick.preview.read-context-changed', clear);
500
+ window.addEventListener('storystick.preview.rendered-context', observe);
501
+ cell.cleanupRenderObservation = () => {
502
+ window.removeEventListener('storystick.preview.read-context-changed', clear);
503
+ window.removeEventListener('storystick.preview.rendered-context', observe);
504
+ };
505
+ }
506
+ // ADR-116 D5 invariant 1 — compare-and-set atomicity. Only the first
507
+ // caller transitions out of idle/error. Concurrent callers (StrictMode
508
+ // double-mount, HMR re-mount, two adapter mounts) observe
509
+ // `inFlightInitPromise` and await it.
510
+ const currentState = cell.state.state;
511
+ if (currentState !== "idle" && currentState !== "error") {
512
+ // Already past idle — return any in-flight promise OR resolved.
513
+ return cell.inFlightInitPromise ?? Promise.resolve();
514
+ }
515
+ // CRITICAL — assign inFlightInitPromise BEFORE any _setStatePartial
516
+ // → notify (codex R6 #2). A synchronous-reentrant listener observing
517
+ // `state !== 'idle'` must find the promise slot populated, not null,
518
+ // so it awaits the real work instead of returning Promise.resolve().
519
+ let resolveOuter = () => { };
520
+ const flight = new Promise((resolve) => {
521
+ resolveOuter = resolve;
522
+ });
523
+ cell.inFlightInitPromise = flight;
524
+ // Build resolver lazily (once per cell — survives HMR + StrictMode).
525
+ if (cell.resolver === null) {
526
+ cell.resolver = buildResolver(opts);
527
+ }
528
+ if (cell.cleanupResolver === null && cell.resolver !== null) {
529
+ const r = cell.resolver;
530
+ cell.cleanupResolver = r.subscribe((snap) => onResolverSnapshot(snap, opts));
531
+ }
532
+ // Resolve `flight` when state machine settles at `authenticated` or
533
+ // `error`. Auto-unsubscribes on completion.
534
+ const unsub = subscribe(() => {
535
+ const s = cell.state.state;
536
+ if (s === "authenticated" || s === "error") {
537
+ unsub();
538
+ cell.inFlightInitPromise = null;
539
+ resolveOuter();
540
+ }
541
+ });
542
+ // Drive the resolver. start() resolves when the resolver leaves
543
+ // resolving-identity (to handshaking or error). Carrier hits +
544
+ // synchronous broadcasts both flow through onResolverSnapshot.
545
+ void cell.resolver.start();
546
+ return flight;
547
+ }
548
+ function markGrantReadyWhenLive() {
549
+ if (!cell.sessionSnapshotReady || !cell.signalSnapshotReady)
550
+ return;
551
+ cell.grantCoordinator?.markReady();
552
+ }
553
+ function wireSessionSubscription(sessionId, client) {
554
+ if (cell.cleanupSubscription !== null &&
555
+ cell.subscriptionSessionId === sessionId) {
556
+ return;
557
+ }
558
+ const previousSubscriptionSessionId = cell.subscriptionSessionId;
559
+ const previousSessionId = cell.state.session?.sessionId ?? null;
560
+ if (cell.cleanupSubscription !== null) {
561
+ try {
562
+ cell.cleanupSubscription();
563
+ }
564
+ catch {
565
+ /* defensive */
566
+ }
567
+ cell.cleanupSubscription = null;
568
+ cell.subscriptionSessionId = null;
569
+ }
570
+ if (previousSubscriptionSessionId !== sessionId ||
571
+ (previousSessionId !== null && previousSessionId !== sessionId)) {
572
+ cell.lastObservedRevision = null;
573
+ cell.lastQueryTargetingFingerprint = null;
574
+ cell.pendingRevisionNotification = null;
575
+ cell.pendingContentRefreshEvent = null;
576
+ _setStatePartial({ contentRevision: null, session: null });
577
+ }
578
+ const subscribeToQuery = (queryName, args, onUpdate, onError) => {
579
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
580
+ const watch = client.watchQuery?.(
581
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
582
+ queryName, args);
583
+ if (!watch || typeof watch.onUpdate !== "function") {
584
+ return () => { };
585
+ }
586
+ const stop = watch.onUpdate(() => {
587
+ try {
588
+ const value = watch.localQueryResult();
589
+ if (value !== undefined)
590
+ onUpdate(value);
591
+ }
592
+ catch (err) {
593
+ onError?.(err);
594
+ }
595
+ });
596
+ return typeof stop === "function" ? stop : () => { };
597
+ };
598
+ const subscriptionGeneration = cell.grantGeneration;
599
+ const subscriptionIsCurrent = () => cell.grantGeneration === subscriptionGeneration &&
600
+ cell.subscriptionSessionId === sessionId;
601
+ const cleanupSession = subscribePreviewSession({
602
+ sessionId,
603
+ mode: "routing-only",
604
+ subscribe: (args, onUpdate, onError) => {
605
+ return subscribeToQuery("sdkQueries:getPreviewSession", args, onUpdate, onError);
606
+ },
607
+ onUpdate: (data) => {
608
+ if (!subscriptionIsCurrent())
609
+ return;
610
+ const previous = cell.state.session?.readContext;
611
+ if (previous?.contextId === data.readContext.contextId && previous.revision > data.readContext.revision)
612
+ return;
613
+ if (cell.state.identity && data.standaloneStudioUrl)
614
+ rememberStandaloneStudioUrl(cell.state.identity, data.standaloneStudioUrl);
615
+ _setStatePartial({ session: data, ...scopeFields(data.readContext.perspective) });
616
+ cell.sessionSnapshotReady = true;
617
+ if (data.readContextStatus === 'ended')
618
+ return;
619
+ markGrantReadyWhenLive();
620
+ emitLifecycleSnapshot();
621
+ },
622
+ onError: (code, message) => {
623
+ if (!subscriptionIsCurrent())
624
+ return;
625
+ if (!isTerminalPreviewSessionErrorCode(code)) {
626
+ cell.sessionSnapshotReady = false;
627
+ cell.grantCoordinator?.reportTransientFailure("subscription");
628
+ return;
629
+ }
630
+ cell.grantCoordinator?.reportBlocked(code, message);
631
+ },
632
+ });
633
+ const cleanupSignal = subscribePreviewContentSignal({
634
+ sessionId,
635
+ subscribe: (args, onUpdate, onError) => {
636
+ return subscribeToQuery("sdkQueries:getPreviewContentSignal", args, onUpdate, onError);
637
+ },
638
+ onUpdate: (data) => {
639
+ if (!subscriptionIsCurrent())
640
+ return;
641
+ const targetingFingerprint = previewQueryTargetingFingerprint(data.queryTargeting);
642
+ const queryTargetingChanged = targetingFingerprint !== cell.lastQueryTargetingFingerprint;
643
+ const observation = observeContentRevision(cell.lastObservedRevision, data.contentRevision, data.contentRefreshDecision, data.requiredProjectedThroughSeq, queryTargetingChanged, data.queryTargeting);
644
+ cell.lastQueryTargetingFingerprint = targetingFingerprint;
645
+ if (observation.observedRevision !== cell.lastObservedRevision) {
646
+ cell.lastObservedRevision = observation.observedRevision;
647
+ _setStatePartial({ contentRevision: observation.observedRevision });
648
+ // C1: record observed revision for e2e latency stitching (correlates
649
+ // Studio saveCommit/revision bump to downstream refresh+ paint).
650
+ if (typeof window !== "undefined") {
651
+ recordPreviewPerfMetric(window, {
652
+ name: "preview.contentRevisionObserved",
653
+ detail: {
654
+ revision: observation.observedRevision,
655
+ requiredProjectedThroughSeq: data.requiredProjectedThroughSeq ?? null,
656
+ },
657
+ });
658
+ }
659
+ }
660
+ if (observation.event !== null) {
661
+ // The first subscription value can arrive after Studio already
662
+ // patched the preview session. Since createSession starts at
663
+ // revision 0, a first observed value > 0 is a missed forward
664
+ // jump, not a harmless baseline.
665
+ emitContentRefresh(observation.event);
666
+ }
667
+ cell.signalSnapshotReady = true;
668
+ markGrantReadyWhenLive();
669
+ },
670
+ onError: (code, message) => {
671
+ if (!subscriptionIsCurrent())
672
+ return;
673
+ if (!isTerminalPreviewSessionErrorCode(code)) {
674
+ cell.signalSnapshotReady = false;
675
+ cell.grantCoordinator?.reportTransientFailure("subscription");
676
+ return;
677
+ }
678
+ cell.grantCoordinator?.reportBlocked(code, message);
679
+ },
680
+ });
681
+ cell.subscriptionSessionId = sessionId;
682
+ cell.cleanupSubscription = () => {
683
+ cleanupSignal();
684
+ cleanupSession();
685
+ if (cell.subscriptionSessionId === sessionId) {
686
+ cell.subscriptionSessionId = null;
687
+ }
688
+ };
689
+ }
690
+ function dispose(preserveSubscribers = false) {
691
+ cell.grantGeneration += 1;
692
+ cell.grantCoordinator?.stop();
693
+ cell.grantCoordinator = null;
694
+ cell.cleanupActivate?.();
695
+ cell.cleanupActivate = null;
696
+ cell.cleanupSubscription?.();
697
+ cell.cleanupSubscription = null;
698
+ cell.subscriptionSessionId = null;
699
+ cell.cleanupResolver?.();
700
+ cell.cleanupResolver = null;
701
+ cell.cleanupRenderObservation?.();
702
+ cell.cleanupRenderObservation = undefined;
703
+ if (cell.resolver) {
704
+ try {
705
+ cell.resolver.stop();
706
+ }
707
+ catch {
708
+ /* defensive */
709
+ }
710
+ cell.resolver = null;
711
+ }
712
+ if (cell.state.client) {
713
+ try {
714
+ void cell.state.client.close();
715
+ }
716
+ catch {
717
+ /* defensive */
718
+ }
719
+ }
720
+ cell.state = { ...INITIAL_STATE };
721
+ publishAuthorityFromState();
722
+ cell.terminal = false;
723
+ cell.lastObservedRevision = null;
724
+ cell.lastQueryTargetingFingerprint = null;
725
+ cell.pendingRevisionNotification = null;
726
+ cell.pendingContentRefreshEvent = null;
727
+ cell.inFlightInitPromise = null;
728
+ cell.authedSessionId = null;
729
+ cell.sessionSnapshotReady = false;
730
+ cell.signalSnapshotReady = false;
731
+ if (preserveSubscribers)
732
+ return;
733
+ cell.listeners.clear();
734
+ cell.revisionListeners.clear();
735
+ cell.contentRefreshListeners.clear();
736
+ cell.identityListeners.clear();
737
+ }
738
+ function deactivate() {
739
+ dispose(true);
740
+ if (isStandalonePreviewContext())
741
+ clearStandaloneStored();
742
+ else
743
+ clearStored();
744
+ cell.state = { ...cell.state, previewEnded: true };
745
+ notifyIdentity(null);
746
+ notify();
747
+ }
748
+ function reportEndedContextLifecycle(context, error) {
749
+ const current = cell.state.session;
750
+ if (current?.readContextStatus !== 'ended' ||
751
+ current.readContext.contextId !== context.contextId ||
752
+ current.readContext.revision !== context.revision)
753
+ return false;
754
+ if (cell.grantCoordinator === null) {
755
+ _setStatePartial({ state: 'error', error });
756
+ return false;
757
+ }
758
+ cell.grantCoordinator.reportBlocked(error.code, error.message);
759
+ return true;
760
+ }
761
+ function confirmContextEnded(context) {
762
+ return reportEndedContextLifecycle(context, {
763
+ code: 'PREVIEW_CONTEXT_ENDED',
764
+ message: 'The shared Preview context ended.',
765
+ });
766
+ }
767
+ function reportContextExitUnavailable(context) {
768
+ return reportEndedContextLifecycle(context, {
769
+ code: 'PREVIEW_CONTEXT_EXIT_UNAVAILABLE',
770
+ message: 'The ended Preview context could not be cleared locally.',
771
+ });
772
+ }
773
+ function getState() {
774
+ return cell.state;
775
+ }
776
+ function subscribe(listener) {
777
+ cell.listeners.add(listener);
778
+ return () => {
779
+ cell.listeners.delete(listener);
780
+ };
781
+ }
782
+ function reactiveClient() {
783
+ return cell.state.client;
784
+ }
785
+ function subscribeContentRevision(cb) {
786
+ cell.revisionListeners.add(cb);
787
+ const pendingRevision = cell.pendingRevisionNotification;
788
+ if (pendingRevision !== null) {
789
+ cell.pendingRevisionNotification = null;
790
+ try {
791
+ cb(pendingRevision);
792
+ }
793
+ catch {
794
+ /* defensive */
795
+ }
796
+ }
797
+ return () => {
798
+ cell.revisionListeners.delete(cb);
799
+ };
800
+ }
801
+ function subscribeContentRefresh(cb) {
802
+ cell.contentRefreshListeners.add(cb);
803
+ const pendingEvent = cell.pendingContentRefreshEvent;
804
+ if (pendingEvent !== null) {
805
+ cell.pendingContentRefreshEvent = null;
806
+ try {
807
+ cb(pendingEvent);
808
+ }
809
+ catch {
810
+ /* defensive */
811
+ }
812
+ }
813
+ return () => {
814
+ cell.contentRefreshListeners.delete(cb);
815
+ };
816
+ }
817
+ function subscribeIdentity(cb) {
818
+ cell.identityListeners.add(cb);
819
+ return () => {
820
+ cell.identityListeners.delete(cb);
821
+ };
822
+ }
823
+ function getIdentity() {
824
+ return cell.state.identity;
825
+ }
826
+ function getVariantId() {
827
+ return cell.state.variantId;
828
+ }
829
+ async function changePerspective(command, route) {
830
+ const generation = cell.grantGeneration;
831
+ try {
832
+ const result = await sendPreviewPerspectiveCommand(route, command);
833
+ if (generation !== cell.grantGeneration)
834
+ return null;
835
+ const current = cell.state.session;
836
+ const observed = current?.readContext;
837
+ // An older HTTP response must not roll back a newer subscription result.
838
+ if (observed && observed.contextId !== result.context.contextId) {
839
+ cell.sessionSnapshotReady = false;
840
+ cell.grantCoordinator?.retryNow();
841
+ }
842
+ else if (current && (!observed || observed.revision <= result.context.revision)) {
843
+ _setStatePartial({ session: { ...current, readContext: result.context,
844
+ readContextStatus: result.ended ? 'ended' : 'active',
845
+ activeVariantId: result.context.perspective.kind === 'variant' ? result.context.perspective.variantId : null,
846
+ activeReleaseId: result.context.perspective.kind === 'release' ? result.context.perspective.releaseId : null,
847
+ }, ...scopeFields(result.context.perspective) });
848
+ }
849
+ _setStatePartial({ contextCommand: result.status === 'applied'
850
+ ? { commandId: command.commandId, status: result.status, contextId: result.context.contextId, appliedRevision: result.appliedRevision }
851
+ : { commandId: command.commandId, status: result.status } });
852
+ emitLifecycleSnapshot();
853
+ const accepted = cell.state.session?.readContext;
854
+ return result.status === 'applied' && !result.ended && accepted &&
855
+ accepted.contextId === result.context.contextId && accepted.revision === result.appliedRevision
856
+ ? accepted : null;
857
+ }
858
+ catch {
859
+ if (generation !== cell.grantGeneration)
860
+ return null;
861
+ _setStatePartial({ contextCommand: { commandId: command.commandId, status: 'unavailable' } });
862
+ emitLifecycleSnapshot();
863
+ return null;
864
+ }
865
+ }
866
+ function setHttpClient(client) {
867
+ // Field-only update — see PreviewRuntime.setHttpClient docstring for
868
+ // why this bypasses the sticky-error gate. We notify listeners so any
869
+ // useSyncExternalStore consumers that read `httpClient` re-render
870
+ // (today : the iframe runtime boot orchestrator's bootSchema retry
871
+ // loop ; tomorrow : any tier-2/3 consumer that observes the SDK
872
+ // arrival).
873
+ if (cell.state.httpClient === client)
874
+ return;
875
+ cell.state = { ...cell.state, httpClient: client };
876
+ publishAuthorityFromState();
877
+ notify();
878
+ }
879
+ function observeCookieLifecycle(event) {
880
+ if (event.type === "refresh-failed") {
881
+ const current = cell.state.cookieLifecycle;
882
+ if (current === null || !Number.isFinite(event.at))
883
+ return;
884
+ _setStatePartial({
885
+ cookieLifecycle: {
886
+ ...current,
887
+ lastRefreshOutcome: {
888
+ status: "failure",
889
+ at: event.at,
890
+ code: event.code,
891
+ },
892
+ },
893
+ });
894
+ return;
895
+ }
896
+ if (!Number.isFinite(event.mintedAt) ||
897
+ !Number.isFinite(event.expiresAt) ||
898
+ event.expiresAt <= event.mintedAt) {
899
+ return;
900
+ }
901
+ _setStatePartial({
902
+ cookieLifecycle: {
903
+ mintedAt: event.mintedAt,
904
+ expiresAt: event.expiresAt,
905
+ lastRefreshOutcome: event.type === "refresh-succeeded"
906
+ ? { status: "success", at: event.mintedAt }
907
+ : null,
908
+ },
909
+ });
910
+ }
911
+ function reportCookieTerminalFailure(error) {
912
+ if (cell.grantCoordinator !== null) {
913
+ // The same-origin grant route is now the authority for both the browser
914
+ // grant and the server cookie. A legacy adapter scheduler failure is a
915
+ // reconnect signal, never a sticky terminal state.
916
+ cell.grantCoordinator.reportTransientFailure("grant");
917
+ return;
918
+ }
919
+ _setStatePartial({
920
+ state: "error",
921
+ error: { code: error.code, message: error.message },
922
+ });
923
+ }
924
+ const __test = {
925
+ reset() {
926
+ cell.grantCoordinator?.stop();
927
+ cell.grantCoordinator = null;
928
+ cell.cleanupActivate?.();
929
+ cell.cleanupActivate = null;
930
+ cell.cleanupSubscription?.();
931
+ cell.cleanupSubscription = null;
932
+ cell.subscriptionSessionId = null;
933
+ cell.cleanupResolver?.();
934
+ cell.cleanupResolver = null;
935
+ cell.cleanupRenderObservation?.();
936
+ cell.cleanupRenderObservation = undefined;
937
+ if (cell.resolver) {
938
+ try {
939
+ cell.resolver.stop();
940
+ }
941
+ catch {
942
+ /* defensive */
943
+ }
944
+ cell.resolver = null;
945
+ }
946
+ if (cell.state.client) {
947
+ try {
948
+ void cell.state.client.close();
949
+ }
950
+ catch {
951
+ /* defensive */
952
+ }
953
+ }
954
+ cell.state = { ...INITIAL_STATE };
955
+ publishAuthorityFromState();
956
+ cell.terminal = false;
957
+ cell.lastObservedRevision = null;
958
+ cell.lastQueryTargetingFingerprint = null;
959
+ cell.pendingRevisionNotification = null;
960
+ cell.pendingContentRefreshEvent = null;
961
+ cell.inFlightInitPromise = null;
962
+ cell.authedSessionId = null;
963
+ cell.grantGeneration += 1;
964
+ cell.sessionSnapshotReady = false;
965
+ cell.signalSnapshotReady = false;
966
+ cell.listeners.clear();
967
+ cell.revisionListeners.clear();
968
+ cell.contentRefreshListeners.clear();
969
+ cell.identityListeners.clear();
970
+ },
971
+ set(partial) {
972
+ cell.state = { ...cell.state, ...partial };
973
+ publishAuthorityFromState();
974
+ if (partial.state === "error")
975
+ cell.terminal = true;
976
+ if (partial.state !== undefined && partial.state !== "error") {
977
+ cell.terminal = false;
978
+ }
979
+ notify();
980
+ if (partial.identity !== undefined)
981
+ notifyIdentity(partial.identity);
982
+ },
983
+ setResolver(resolver) {
984
+ if (cell.cleanupResolver) {
985
+ cell.cleanupResolver();
986
+ cell.cleanupResolver = null;
987
+ }
988
+ if (cell.resolver && cell.resolver !== resolver) {
989
+ try {
990
+ cell.resolver.stop();
991
+ }
992
+ catch {
993
+ /* defensive */
994
+ }
995
+ }
996
+ cell.resolver = resolver;
997
+ },
998
+ wireSessionSubscription(sessionId, client) {
999
+ wireSessionSubscription(sessionId, client);
1000
+ },
1001
+ getSubscriptionSessionId() {
1002
+ return cell.subscriptionSessionId;
1003
+ },
1004
+ };
1005
+ export const previewRuntime = {
1006
+ init,
1007
+ dispose,
1008
+ deactivate,
1009
+ confirmContextEnded,
1010
+ reportContextExitUnavailable,
1011
+ refreshGrant: () => { cell.sessionSnapshotReady = false; cell.grantCoordinator?.retryNow(); },
1012
+ getState,
1013
+ getLifecycleSnapshot,
1014
+ subscribe,
1015
+ reactiveClient,
1016
+ subscribeContentRevision,
1017
+ subscribeContentRefresh,
1018
+ subscribeIdentity,
1019
+ getIdentity,
1020
+ getVariantId,
1021
+ changePerspective,
1022
+ setHttpClient,
1023
+ observeCookieLifecycle,
1024
+ reportCookieTerminalFailure,
1025
+ __test,
1026
+ };