@storystick/preview 0.0.0-stage → 0.2.0-rc.1
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.
- package/LICENSE +112 -0
- package/PUBLIC-API.md +56 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +92 -0
- package/dist/authoring/runtime-contract.js +28 -0
- package/dist/channel-taxonomy.d.ts +17 -0
- package/dist/channel-taxonomy.js +46 -0
- package/dist/context-command.d.ts +18 -0
- package/dist/context-command.js +38 -0
- package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
- package/dist/dev/suspicious-codeword-detector.js +220 -0
- package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
- package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
- package/dist/framework/browser-context.d.ts +2 -0
- package/dist/framework/browser-context.js +2 -0
- package/dist/framework/generation-refresh.d.ts +23 -0
- package/dist/framework/generation-refresh.js +82 -0
- package/dist/framework/generation-signal.d.ts +8 -0
- package/dist/framework/generation-signal.js +17 -0
- package/dist/framework/projection-generation-watch.d.ts +5 -0
- package/dist/framework/projection-generation-watch.js +53 -0
- package/dist/framework/render-receipt.d.ts +8 -0
- package/dist/framework/render-receipt.js +10 -0
- package/dist/framework/runtime-adapter-store.d.ts +30 -0
- package/dist/framework/runtime-adapter-store.js +81 -0
- package/dist/grant-coordinator.d.ts +53 -0
- package/dist/grant-coordinator.js +329 -0
- package/dist/grant-lifecycle.d.ts +52 -0
- package/dist/grant-lifecycle.js +74 -0
- package/dist/identity/__test-adapter.d.ts +34 -0
- package/dist/identity/__test-adapter.js +59 -0
- package/dist/identity/browser-context-gate.d.ts +4 -0
- package/dist/identity/browser-context-gate.js +22 -0
- package/dist/identity/browser-context.d.ts +13 -0
- package/dist/identity/browser-context.js +22 -0
- package/dist/identity/carriers/postmessage.d.ts +84 -0
- package/dist/identity/carriers/postmessage.js +177 -0
- package/dist/identity/carriers/storage-state.d.ts +41 -0
- package/dist/identity/carriers/storage-state.js +99 -0
- package/dist/identity/carriers/storage.d.ts +39 -0
- package/dist/identity/carriers/storage.js +182 -0
- package/dist/identity/carriers/url.d.ts +19 -0
- package/dist/identity/carriers/url.js +38 -0
- package/dist/identity/identity-state-machine.d.ts +70 -0
- package/dist/identity/identity-state-machine.js +244 -0
- package/dist/identity/identity-types.d.ts +192 -0
- package/dist/identity/identity-types.js +22 -0
- package/dist/identity/index.d.ts +18 -0
- package/dist/identity/index.js +16 -0
- package/dist/identity/preview-cookie.d.ts +9 -0
- package/dist/identity/preview-cookie.js +12 -0
- package/dist/identity/preview-draft.d.ts +6 -0
- package/dist/identity/preview-draft.js +25 -0
- package/dist/identity/preview-identity.d.ts +28 -0
- package/dist/identity/preview-identity.js +84 -0
- package/dist/identity/preview-scope.d.ts +28 -0
- package/dist/identity/preview-scope.js +47 -0
- package/dist/identity/resolver.d.ts +110 -0
- package/dist/identity/resolver.js +334 -0
- package/dist/identity/session-query.d.ts +4 -0
- package/dist/identity/session-query.js +19 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +788 -0
- package/dist/interaction-mode.d.ts +58 -0
- package/dist/interaction-mode.js +93 -0
- package/dist/nav-handler.d.ts +95 -0
- package/dist/nav-handler.js +84 -0
- package/dist/overlay-label.d.ts +79 -0
- package/dist/overlay-label.js +73 -0
- package/dist/page-presentation.d.ts +24 -0
- package/dist/page-presentation.js +32 -0
- package/dist/provenance/anchor-map.d.ts +95 -0
- package/dist/provenance/anchor-map.js +177 -0
- package/dist/provenance/carrier-plan.d.ts +51 -0
- package/dist/provenance/carrier-plan.js +90 -0
- package/dist/provenance/coded-text.d.ts +20 -0
- package/dist/provenance/coded-text.js +302 -0
- package/dist/provenance/direct-edit-locator.d.ts +29 -0
- package/dist/provenance/direct-edit-locator.js +218 -0
- package/dist/provenance/editable-leaf.d.ts +134 -0
- package/dist/provenance/editable-leaf.js +483 -0
- package/dist/provenance/editor-target.d.ts +29 -0
- package/dist/provenance/editor-target.js +46 -0
- package/dist/provenance/field-leaf-walker.d.ts +21 -0
- package/dist/provenance/field-leaf-walker.js +98 -0
- package/dist/provenance/frame-dependencies.d.ts +212 -0
- package/dist/provenance/frame-dependencies.js +550 -0
- package/dist/provenance/index.d.ts +26 -0
- package/dist/provenance/index.js +26 -0
- package/dist/provenance/local-events.d.ts +1 -0
- package/dist/provenance/local-events.js +1 -0
- package/dist/provenance/markers.d.ts +21 -0
- package/dist/provenance/markers.js +70 -0
- package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
- package/dist/provenance/multi-frame-anchor-map.js +109 -0
- package/dist/provenance/normalize.d.ts +2 -0
- package/dist/provenance/normalize.js +19 -0
- package/dist/provenance/overlay-focus-echo.d.ts +17 -0
- package/dist/provenance/overlay-focus-echo.js +16 -0
- package/dist/provenance/overlay-projection.d.ts +65 -0
- package/dist/provenance/overlay-projection.js +201 -0
- package/dist/provenance/owner-map.d.ts +55 -0
- package/dist/provenance/owner-map.js +239 -0
- package/dist/provenance/read-carrier.d.ts +22 -0
- package/dist/provenance/read-carrier.js +92 -0
- package/dist/provenance/recorder.d.ts +38 -0
- package/dist/provenance/recorder.js +82 -0
- package/dist/provenance/render-context.d.ts +106 -0
- package/dist/provenance/render-context.js +338 -0
- package/dist/provenance/reveal-contract.d.ts +44 -0
- package/dist/provenance/reveal-contract.js +1 -0
- package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
- package/dist/provenance/richtext-slice-embed-address.js +35 -0
- package/dist/provenance/runtime-script.d.ts +1 -0
- package/dist/provenance/runtime-script.js +2217 -0
- package/dist/provenance/sidecar-store.d.ts +63 -0
- package/dist/provenance/sidecar-store.js +151 -0
- package/dist/provenance/sidecar.d.ts +34 -0
- package/dist/provenance/sidecar.js +227 -0
- package/dist/provenance/slice-model.d.ts +28 -0
- package/dist/provenance/slice-model.js +86 -0
- package/dist/provenance/source-identity.d.ts +3 -0
- package/dist/provenance/source-identity.js +7 -0
- package/dist/provenance/source-map-materializer.d.ts +76 -0
- package/dist/provenance/source-map-materializer.js +286 -0
- package/dist/provenance/source-registry.d.ts +103 -0
- package/dist/provenance/source-registry.js +562 -0
- package/dist/provenance/transformed-output-gate.d.ts +161 -0
- package/dist/provenance/transformed-output-gate.js +171 -0
- package/dist/provenance/types.d.ts +102 -0
- package/dist/provenance/types.js +1 -0
- package/dist/refresh-policy.d.ts +11 -0
- package/dist/refresh-policy.js +14 -0
- package/dist/reveal-target.d.ts +8 -0
- package/dist/reveal-target.js +47 -0
- package/dist/route-paths.d.ts +11 -0
- package/dist/route-paths.js +14 -0
- package/dist/router-subscriber.d.ts +92 -0
- package/dist/router-subscriber.js +44 -0
- package/dist/runtime/anchor-types.d.ts +5 -0
- package/dist/runtime/anchor-types.js +1 -0
- package/dist/runtime/authority-store.d.ts +31 -0
- package/dist/runtime/authority-store.js +56 -0
- package/dist/runtime/boot.d.ts +52 -0
- package/dist/runtime/boot.js +379 -0
- package/dist/runtime/content-refresh.d.ts +30 -0
- package/dist/runtime/content-refresh.js +68 -0
- package/dist/runtime/emit-runtime-error.d.ts +51 -0
- package/dist/runtime/emit-runtime-error.js +64 -0
- package/dist/runtime/field-proxy.d.ts +112 -0
- package/dist/runtime/field-proxy.js +471 -0
- package/dist/runtime/form-contract-reporter.d.ts +99 -0
- package/dist/runtime/form-contract-reporter.js +391 -0
- package/dist/runtime/invalidation-adapter.d.ts +17 -0
- package/dist/runtime/invalidation-adapter.js +42 -0
- package/dist/runtime/live-query-overlay.d.ts +39 -0
- package/dist/runtime/live-query-overlay.js +345 -0
- package/dist/runtime/momentary-navigate.d.ts +16 -0
- package/dist/runtime/momentary-navigate.js +106 -0
- package/dist/runtime/observation-scheduler.d.ts +15 -0
- package/dist/runtime/observation-scheduler.js +42 -0
- package/dist/runtime/perf-metrics.d.ts +28 -0
- package/dist/runtime/perf-metrics.js +90 -0
- package/dist/runtime/preview-interaction-policy.d.ts +1 -0
- package/dist/runtime/preview-interaction-policy.js +103 -0
- package/dist/runtime/preview-overlay-painter.d.ts +27 -0
- package/dist/runtime/preview-overlay-painter.js +1732 -0
- package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
- package/dist/runtime/preview-overlay-sidecar.js +45 -0
- package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
- package/dist/runtime/preview-perspective-alignment.js +40 -0
- package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
- package/dist/runtime/preview-perspective-intent-state.js +5 -0
- package/dist/runtime/preview-transition-state.d.ts +142 -0
- package/dist/runtime/preview-transition-state.js +257 -0
- package/dist/runtime/render-mode-state.d.ts +51 -0
- package/dist/runtime/render-mode-state.js +76 -0
- package/dist/runtime/request-id.d.ts +4 -0
- package/dist/runtime/request-id.js +8 -0
- package/dist/runtime/resolve-schema-field.d.ts +99 -0
- package/dist/runtime/resolve-schema-field.js +356 -0
- package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
- package/dist/runtime/runtime-error-dispatch.js +75 -0
- package/dist/runtime/schema-store.d.ts +121 -0
- package/dist/runtime/schema-store.js +667 -0
- package/dist/runtime/snapshot-assembly.d.ts +104 -0
- package/dist/runtime/snapshot-assembly.js +553 -0
- package/dist/runtime/snapshot-reporter.d.ts +55 -0
- package/dist/runtime/snapshot-reporter.js +375 -0
- package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
- package/dist/runtime/standalone-overlay-controller.js +281 -0
- package/dist/runtime/store.d.ts +307 -0
- package/dist/runtime/store.js +1026 -0
- package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
- package/dist/runtime/windowed-minimap-projection.js +189 -0
- package/dist/runtime/workspace-shortcuts.d.ts +30 -0
- package/dist/runtime/workspace-shortcuts.js +188 -0
- package/dist/sdk-error-copy.d.ts +48 -0
- package/dist/sdk-error-copy.js +182 -0
- package/dist/select-cursor.d.ts +2 -0
- package/dist/select-cursor.js +23 -0
- package/dist/server/admission-cookie.d.ts +38 -0
- package/dist/server/admission-cookie.js +122 -0
- package/dist/server/control-client.d.ts +29 -0
- package/dist/server/control-client.js +22 -0
- package/dist/server/control-errors.d.ts +22 -0
- package/dist/server/control-errors.js +182 -0
- package/dist/server/draft-route.d.ts +12 -0
- package/dist/server/draft-route.js +45 -0
- package/dist/server/extract-token.d.ts +30 -0
- package/dist/server/extract-token.js +98 -0
- package/dist/server/grant.d.ts +128 -0
- package/dist/server/grant.js +395 -0
- package/dist/server/ordinary-read-provenance.d.ts +11 -0
- package/dist/server/ordinary-read-provenance.js +124 -0
- package/dist/server/preview-read-metadata.d.ts +10 -0
- package/dist/server/preview-read-metadata.js +27 -0
- package/dist/server/provenance-frame-builder.d.ts +53 -0
- package/dist/server/provenance-frame-builder.js +102 -0
- package/dist/server/provenance-runtime.d.ts +70 -0
- package/dist/server/provenance-runtime.js +46 -0
- package/dist/server/read-context-command.d.ts +11 -0
- package/dist/server/read-context-command.js +53 -0
- package/dist/server/read-identity.d.ts +3 -0
- package/dist/server/read-identity.js +63 -0
- package/dist/server/read-lifecycle.d.ts +17 -0
- package/dist/server/read-lifecycle.js +52 -0
- package/dist/server/render-frame-provenance.d.ts +133 -0
- package/dist/server/render-frame-provenance.js +676 -0
- package/dist/server/render-id.d.ts +15 -0
- package/dist/server/render-id.js +22 -0
- package/dist/server/route-document-provenance.d.ts +77 -0
- package/dist/server/route-document-provenance.js +1327 -0
- package/dist/server/route-session-cookie.d.ts +33 -0
- package/dist/server/route-session-cookie.js +54 -0
- package/dist/session-projections.d.ts +33 -0
- package/dist/session-projections.js +43 -0
- package/dist/session-subscriber.d.ts +124 -0
- package/dist/session-subscriber.js +79 -0
- package/dist/sha256-fallback.d.ts +30 -0
- package/dist/sha256-fallback.js +126 -0
- package/dist/types.d.ts +446 -0
- package/dist/types.js +85 -0
- package/dist/validators.d.ts +61 -0
- package/dist/validators.js +110 -0
- package/package.json +210 -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
|
+
};
|