@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.
- package/LICENSE +112 -0
- package/PUBLIC-API.md +55 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +89 -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/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 +165 -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 +206 -3
|
@@ -0,0 +1,2217 @@
|
|
|
1
|
+
import { installPreviewWorkspaceShortcuts, previewWorkspaceCommandIds, } from '../runtime/workspace-shortcuts.js';
|
|
2
|
+
import { decodePreviewReadCarrier, selectPreviewPrimaryPage } from './read-carrier.js';
|
|
3
|
+
import { PREVIEW_PROVENANCE_ANCHOR_MAP_EVENT } from './local-events.js';
|
|
4
|
+
// Utility consumers of the provenance entry point must not retain the injected
|
|
5
|
+
// runtime. Constructing this string is pure; only its explicit consumer runs it.
|
|
6
|
+
export const previewProvenanceRuntimeScript = /* @__PURE__ */ (() => String.raw `
|
|
7
|
+
(() => {
|
|
8
|
+
const decodeReadCarrier = ${decodePreviewReadCarrier.toString()}
|
|
9
|
+
const selectPrimaryPage = ${selectPreviewPrimaryPage.toString()}
|
|
10
|
+
const LOCAL_ANCHOR_MAP_EVENT = ${JSON.stringify(PREVIEW_PROVENANCE_ANCHOR_MAP_EVENT)}
|
|
11
|
+
// This script is served by the SDK endpoint, including the framework basePath.
|
|
12
|
+
const runtimePath = document.currentScript?.src ? new URL(document.currentScript.src).pathname : ''
|
|
13
|
+
const runtimeBasePath = runtimePath.endsWith('/api/storystick/provenance-runtime')
|
|
14
|
+
? runtimePath.slice(0, -'/api/storystick/provenance-runtime'.length) : ''
|
|
15
|
+
if (window.__ssPreviewProvenanceRuntimeInstalled) return
|
|
16
|
+
window.__ssPreviewProvenanceRuntimeInstalled = true
|
|
17
|
+
const installWorkspaceShortcuts = ${installPreviewWorkspaceShortcuts.toString()}
|
|
18
|
+
installWorkspaceShortcuts(window, () => {
|
|
19
|
+
const state = window.__storystick_preview_runtime__?.state
|
|
20
|
+
return state && !state.previewEnded ? state.identity : null
|
|
21
|
+
}, ${JSON.stringify(previewWorkspaceCommandIds)})
|
|
22
|
+
// Missing required anchors remain invalid. Poll slowly between real DOM
|
|
23
|
+
// invalidations, with a wall-clock deadline independent of display refresh.
|
|
24
|
+
const READY_DEADLINE_MS = 4000
|
|
25
|
+
const READY_RETRY_MS = 100
|
|
26
|
+
// Work counters belong to this Document, not to each provenance frame.
|
|
27
|
+
// The existing manual probe exposes totals; they never drive readiness.
|
|
28
|
+
const observationStats = { readCarrierScans: 0, anchorCollections: 0, codedTextScans: 0, readyPasses: 0, publications: 0 }
|
|
29
|
+
let readyFrame = null
|
|
30
|
+
let readyRetryTimer = null
|
|
31
|
+
let readyWork = null
|
|
32
|
+
let readyPassRunning = false
|
|
33
|
+
let documentActive = true
|
|
34
|
+
let cancelEpoch = 1
|
|
35
|
+
let iframeGeneration = 1
|
|
36
|
+
let currentRenderIdentity = { generation: 1, sourceOfTruth: 'iframe' }
|
|
37
|
+
let currentInteractionMode = 'edit'
|
|
38
|
+
let lastMomentaryNavigateKeyActive = null
|
|
39
|
+
let pendingNavigationEpoch = null
|
|
40
|
+
let lastPublisherRevision = 0
|
|
41
|
+
let activePublisherRoute = null
|
|
42
|
+
let nativeViewAnchor = null
|
|
43
|
+
let lastPublishedSnapshot = null
|
|
44
|
+
let lastRenderedContextKey = null
|
|
45
|
+
const publisherFramesById = new Map()
|
|
46
|
+
const readCarrierCache = new Map()
|
|
47
|
+
let readCarrierFrames = []
|
|
48
|
+
let primaryPageCandidates = []
|
|
49
|
+
let lastPrimaryPagesKey = null
|
|
50
|
+
let readCarrierSignature = ''
|
|
51
|
+
let readCarriersDirty = true
|
|
52
|
+
let publisherSnapshotDirty = false
|
|
53
|
+
const consumedPublisherElements = new WeakSet()
|
|
54
|
+
const pendingOccurrenceRemovals = new Map()
|
|
55
|
+
const PUBLISHER_CONSUMED_EVENT = 'storystick.preview.provenance-publisher-consumed'
|
|
56
|
+
const PUBLISHER_CONSUMED_REGISTRY = '__ssPreviewProvenanceConsumedPublishersV2'
|
|
57
|
+
const PAINT_ID_ATTRIBUTE = 'data-ss-preview-paint-id'
|
|
58
|
+
const PAINT_TARGET_REGISTRY = '__ssPreviewPaintTargetRegistryV1'
|
|
59
|
+
let nextPaintId = 1
|
|
60
|
+
const paintIdByElement = new WeakMap()
|
|
61
|
+
const paintTargetById = new Map()
|
|
62
|
+
let provenanceObserver = null
|
|
63
|
+
let viewportFrame = null
|
|
64
|
+
let hydrationSafeDomMaterializationEnabled = false
|
|
65
|
+
let hydrationSafeDomMaterializationScheduled = false
|
|
66
|
+
let hydrationTimer = null
|
|
67
|
+
let hydrationFrame = null
|
|
68
|
+
let hydrationLoadListener = null
|
|
69
|
+
const DOM_HYDRATION_GRACE_MS = 2000
|
|
70
|
+
const CODED_TEXT_START = encodeTagAscii('ss-p:')
|
|
71
|
+
const CODED_TEXT_SEPARATOR = encodeTagAscii('|')
|
|
72
|
+
const CODED_TEXT_END = encodeTagAscii('/ss-p')
|
|
73
|
+
const SYNTHETIC_OVERLAY_SELECTOR =
|
|
74
|
+
'[data-ss-preview-field-synthetic-overlay-root],[data-ss-preview-field-synthetic-overlay-box]'
|
|
75
|
+
const VISIBILITY_ATTRIBUTE_FILTER = [
|
|
76
|
+
'data-ss-p',
|
|
77
|
+
'data-ss-e',
|
|
78
|
+
'data-ss-provenance-frame',
|
|
79
|
+
'data-ss-provenance-publisher',
|
|
80
|
+
'data-ss-route-frame-hash',
|
|
81
|
+
'data-preview-message-session-id',
|
|
82
|
+
'hidden',
|
|
83
|
+
'style',
|
|
84
|
+
'class',
|
|
85
|
+
'aria-expanded',
|
|
86
|
+
'aria-hidden',
|
|
87
|
+
'data-state',
|
|
88
|
+
'open',
|
|
89
|
+
'popover',
|
|
90
|
+
'inert',
|
|
91
|
+
]
|
|
92
|
+
const PROVENANCE_OBSERVER_OPTIONS = {
|
|
93
|
+
childList: true,
|
|
94
|
+
subtree: true,
|
|
95
|
+
characterData: true,
|
|
96
|
+
attributes: true,
|
|
97
|
+
attributeFilter: VISIBILITY_ATTRIBUTE_FILTER,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function encodeTagAscii(value) {
|
|
101
|
+
let encoded = ''
|
|
102
|
+
for (const char of value) {
|
|
103
|
+
const code = char.codePointAt(0)
|
|
104
|
+
if (code === undefined || code < 0x20 || code > 0x7e) return ''
|
|
105
|
+
encoded += String.fromCodePoint(0xe0000 + code)
|
|
106
|
+
}
|
|
107
|
+
return encoded
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function decodeTagAscii(value) {
|
|
111
|
+
let decoded = ''
|
|
112
|
+
for (const char of value) {
|
|
113
|
+
const code = char.codePointAt(0)
|
|
114
|
+
if (code === undefined || code < 0xe0020 || code > 0xe007e) return ''
|
|
115
|
+
decoded += String.fromCodePoint(code - 0xe0000)
|
|
116
|
+
}
|
|
117
|
+
return decoded
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function decodeCodedText(value) {
|
|
121
|
+
const segments = []
|
|
122
|
+
let text = ''
|
|
123
|
+
let cursor = 0
|
|
124
|
+
while (cursor < value.length) {
|
|
125
|
+
const markerStart = value.indexOf(CODED_TEXT_START, cursor)
|
|
126
|
+
if (markerStart < 0) {
|
|
127
|
+
text += value.slice(cursor)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
text += value.slice(cursor, markerStart)
|
|
131
|
+
const encodedMarkerStart = markerStart + CODED_TEXT_START.length
|
|
132
|
+
const separator = value.indexOf(CODED_TEXT_SEPARATOR, encodedMarkerStart)
|
|
133
|
+
if (separator < 0) {
|
|
134
|
+
text += value.slice(markerStart)
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
const textStart = separator + CODED_TEXT_SEPARATOR.length
|
|
138
|
+
const textEnd = value.indexOf(CODED_TEXT_END, textStart)
|
|
139
|
+
if (textEnd < 0) {
|
|
140
|
+
text += value.slice(markerStart)
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
const carrierId = decodeTagAscii(value.slice(encodedMarkerStart, separator))
|
|
144
|
+
const markerId = normalizedCarrierMarkerId(carrierId)
|
|
145
|
+
const segmentText = value.slice(textStart, textEnd)
|
|
146
|
+
if (markerId.length > 0) {
|
|
147
|
+
segments.push({
|
|
148
|
+
markerId,
|
|
149
|
+
carrierId,
|
|
150
|
+
text: segmentText,
|
|
151
|
+
markerStart,
|
|
152
|
+
markerEnd: textEnd + CODED_TEXT_END.length,
|
|
153
|
+
textStart,
|
|
154
|
+
textEnd,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
text += segmentText
|
|
158
|
+
cursor = textEnd + CODED_TEXT_END.length
|
|
159
|
+
}
|
|
160
|
+
return { text, segments }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function studioTargetOrigin() {
|
|
164
|
+
const runtime = window.__storystick_preview_runtime__
|
|
165
|
+
const identity = runtime && runtime.state && runtime.state.identity
|
|
166
|
+
const parentOrigin = identity && identity.parentOrigin
|
|
167
|
+
if (typeof parentOrigin === 'string' && parentOrigin.length > 0) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = new URL(parentOrigin)
|
|
170
|
+
if (parsed.origin === parentOrigin) return parentOrigin
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
// The provenance script can run before the identity handshake, and older
|
|
174
|
+
// SDK runtimes may not expose identity on the shared cell. The initial
|
|
175
|
+
// message must remain discoverable; inbound validation still locks source,
|
|
176
|
+
// origin, session, schema, and monotonic identity before accepting an echo.
|
|
177
|
+
return '*'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isSyntheticOverlayNode(node) {
|
|
181
|
+
if (node instanceof Element) {
|
|
182
|
+
return node.closest(SYNTHETIC_OVERLAY_SELECTOR) !== null
|
|
183
|
+
}
|
|
184
|
+
const parent = node.parentNode
|
|
185
|
+
return parent instanceof Element &&
|
|
186
|
+
parent.closest(SYNTHETIC_OVERLAY_SELECTOR) !== null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isSyntheticOverlayOnlyMutation(record) {
|
|
190
|
+
if (isSyntheticOverlayNode(record.target)) return true
|
|
191
|
+
if (record.type !== 'childList') return false
|
|
192
|
+
const changedNodes = [...record.addedNodes, ...record.removedNodes]
|
|
193
|
+
return changedNodes.length > 0 &&
|
|
194
|
+
changedNodes.every((node) => isSyntheticOverlayNode(node))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeRenderIdentity(identity) {
|
|
198
|
+
const generation = Number.isFinite(identity && identity.generation) && identity.generation > 0
|
|
199
|
+
? identity.generation
|
|
200
|
+
: 1
|
|
201
|
+
const sourceOfTruth =
|
|
202
|
+
identity && (identity.sourceOfTruth === 'studio' || identity.sourceOfTruth === 'iframe' || identity.sourceOfTruth === 'system')
|
|
203
|
+
? identity.sourceOfTruth
|
|
204
|
+
: 'system'
|
|
205
|
+
return {
|
|
206
|
+
generation,
|
|
207
|
+
sourceOfTruth,
|
|
208
|
+
...(identity && typeof identity.commandId === 'string' && identity.commandId.length > 0
|
|
209
|
+
? { commandId: identity.commandId }
|
|
210
|
+
: {}),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseSidecar(text) {
|
|
215
|
+
if (typeof text !== 'string' || text.trim().length === 0) return emptySidecar()
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(text)
|
|
218
|
+
const dependencySet = normalizeDependencySet(parsed.dependencySet)
|
|
219
|
+
return {
|
|
220
|
+
type: 'storystick.preview.public-sidecar',
|
|
221
|
+
version: 1,
|
|
222
|
+
routeHash: typeof parsed.routeHash === 'string' ? parsed.routeHash : '',
|
|
223
|
+
...(normalizeVariantId(parsed.variantId) !== null
|
|
224
|
+
? { variantId: normalizeVariantId(parsed.variantId) }
|
|
225
|
+
: {}),
|
|
226
|
+
...(normalizePreviewScope(parsed.previewScope) !== null
|
|
227
|
+
? { previewScope: normalizePreviewScope(parsed.previewScope) }
|
|
228
|
+
: {}),
|
|
229
|
+
codeVersion: typeof parsed.codeVersion === 'string' ? parsed.codeVersion : '',
|
|
230
|
+
renderId: typeof parsed.renderId === 'string' ? parsed.renderId : '',
|
|
231
|
+
generation: typeof parsed.generation === 'number' ? parsed.generation : 0,
|
|
232
|
+
renderNonce: typeof parsed.renderNonce === 'string' ? parsed.renderNonce : '',
|
|
233
|
+
routeRenderNonce: typeof parsed.routeRenderNonce === 'string'
|
|
234
|
+
? parsed.routeRenderNonce
|
|
235
|
+
: '',
|
|
236
|
+
emissionRevision: typeof parsed.emissionRevision === 'number'
|
|
237
|
+
? parsed.emissionRevision
|
|
238
|
+
: 0,
|
|
239
|
+
...(dependencySet ? { dependencySet } : {}),
|
|
240
|
+
markers: Array.isArray(parsed.markers)
|
|
241
|
+
? parsed.markers.flatMap(normalizeMarker)
|
|
242
|
+
: [],
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
return emptySidecar()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function normalizeMarker(marker) {
|
|
250
|
+
if (typeof marker !== 'object' || marker === null) return []
|
|
251
|
+
if (typeof marker.markerId !== 'string' || marker.markerId.length === 0) return []
|
|
252
|
+
const editLocator = normalizeEditLocator(marker.editLocator)
|
|
253
|
+
return [{
|
|
254
|
+
markerId: marker.markerId,
|
|
255
|
+
...(editLocator ? { editLocator } : {}),
|
|
256
|
+
modality: typeof marker.modality === 'string' ? marker.modality : 'text',
|
|
257
|
+
anchorKind: typeof marker.anchorKind === 'string' ? marker.anchorKind : 'element',
|
|
258
|
+
anchorMultiplicity: marker.anchorMultiplicity === 'multi' ? 'multi' : undefined,
|
|
259
|
+
presence: marker.presence === 'optional' || marker.presence === 'expected'
|
|
260
|
+
? marker.presence
|
|
261
|
+
: undefined,
|
|
262
|
+
renderedTextFingerprint: typeof marker.renderedTextFingerprint === 'string'
|
|
263
|
+
? marker.renderedTextFingerprint
|
|
264
|
+
: undefined,
|
|
265
|
+
}]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizeEditLocator(value) {
|
|
269
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
|
270
|
+
if (value.version !== 1) return null
|
|
271
|
+
const sourceKinds = new Set(['stored', 'binding', 'model', 'computed', 'richtext', 'embed', 'emptyFallback', 'unknown'])
|
|
272
|
+
if (!sourceKinds.has(value.sourceKind)) return null
|
|
273
|
+
const bounded = (candidate, max, allowEmpty) => {
|
|
274
|
+
if (typeof candidate !== 'string' || candidate.length > max) return null
|
|
275
|
+
const normalized = candidate.trim()
|
|
276
|
+
return normalized.length > 0 || allowEmpty === true ? normalized : null
|
|
277
|
+
}
|
|
278
|
+
const sourceSetId = bounded(value.sourceSetId, 512, false)
|
|
279
|
+
const documentId = bounded(value.documentId, 256, false)
|
|
280
|
+
const canonicalPath = bounded(value.canonicalPath, 4096, false)
|
|
281
|
+
if (!sourceSetId || !documentId || !canonicalPath) return null
|
|
282
|
+
const pathAnchors = normalizeLocatorAnchors(value.pathAnchors)
|
|
283
|
+
if (pathAnchors === null) return null
|
|
284
|
+
const navigationAnchors = value.navigationAnchors === undefined
|
|
285
|
+
? undefined
|
|
286
|
+
: normalizeLocatorAnchors(value.navigationAnchors)
|
|
287
|
+
if (navigationAnchors === null) return null
|
|
288
|
+
const navigationPath = value.navigationPath === undefined
|
|
289
|
+
? undefined
|
|
290
|
+
: bounded(value.navigationPath, 4096, false)
|
|
291
|
+
if (value.navigationPath !== undefined && !navigationPath) return null
|
|
292
|
+
const variantId = normalizeVariantId(value.variantId)
|
|
293
|
+
if (value.variantId !== undefined && (typeof value.variantId !== 'string' || !variantId)) return null
|
|
294
|
+
const textNodeIndex = Number.isInteger(value.textNodeIndex) && value.textNodeIndex >= 0
|
|
295
|
+
? value.textNodeIndex
|
|
296
|
+
: undefined
|
|
297
|
+
if (value.textNodeIndex !== undefined && textNodeIndex === undefined) return null
|
|
298
|
+
const readScope = value.readScope === undefined
|
|
299
|
+
? undefined
|
|
300
|
+
: normalizePreviewScope(value.readScope)
|
|
301
|
+
if (value.readScope !== undefined && readScope === null) return null
|
|
302
|
+
return {
|
|
303
|
+
version: 1,
|
|
304
|
+
sourceSetId,
|
|
305
|
+
documentId,
|
|
306
|
+
...(variantId ? { variantId } : {}),
|
|
307
|
+
canonicalPath,
|
|
308
|
+
pathAnchors,
|
|
309
|
+
...(navigationPath !== undefined ? { navigationPath } : {}),
|
|
310
|
+
...(navigationAnchors !== undefined ? { navigationAnchors } : {}),
|
|
311
|
+
...(textNodeIndex !== undefined ? { textNodeIndex } : {}),
|
|
312
|
+
sourceKind: value.sourceKind,
|
|
313
|
+
...(readScope !== undefined ? { readScope } : {}),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeLocatorAnchors(value) {
|
|
318
|
+
if (!Array.isArray(value) || value.length > 64) return null
|
|
319
|
+
const anchors = []
|
|
320
|
+
for (const entry of value) {
|
|
321
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return null
|
|
322
|
+
if (typeof entry.path !== 'string' || entry.path.length > 4096) return null
|
|
323
|
+
if (typeof entry.key !== 'string' || entry.key.trim().length === 0 || entry.key.length > 512) return null
|
|
324
|
+
anchors.push({ path: entry.path.trim(), key: entry.key.trim() })
|
|
325
|
+
}
|
|
326
|
+
return anchors
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizePreviewScope(value) {
|
|
330
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
|
331
|
+
const boundedString = (candidate, max) =>
|
|
332
|
+
typeof candidate === 'string' && candidate.length > 0 && candidate.length <= max
|
|
333
|
+
const nullableString = (candidate, max) =>
|
|
334
|
+
candidate === null || boundedString(candidate, max)
|
|
335
|
+
const validStatus =
|
|
336
|
+
value.releaseScopeStatus === 'none' ||
|
|
337
|
+
value.releaseScopeStatus === 'active' ||
|
|
338
|
+
value.releaseScopeStatus === 'missing' ||
|
|
339
|
+
value.releaseScopeStatus === 'not-previewable' ||
|
|
340
|
+
value.releaseScopeStatus === 'unbound'
|
|
341
|
+
if (
|
|
342
|
+
!nullableString(value.requestedVariantId, 256) ||
|
|
343
|
+
!nullableString(value.requestedDocumentId, 256) ||
|
|
344
|
+
!(value.requestedLocale === undefined || nullableString(value.requestedLocale, 128)) ||
|
|
345
|
+
!(value.appliedLocale === undefined || nullableString(value.appliedLocale, 128)) ||
|
|
346
|
+
!(value.appliesToDocument === null || typeof value.appliesToDocument === 'boolean') ||
|
|
347
|
+
!nullableString(value.appliedVariantId, 256) ||
|
|
348
|
+
!nullableString(value.requestedReleaseId, 256) ||
|
|
349
|
+
!validStatus ||
|
|
350
|
+
!(value.releaseLifecycleStatus === undefined || boundedString(value.releaseLifecycleStatus, 64)) ||
|
|
351
|
+
!nullableString(value.appliedReleaseId, 256) ||
|
|
352
|
+
!(value.releaseMemberState === undefined || value.releaseMemberState === 'active' || value.releaseMemberState === 'archived' || value.releaseMemberState === 'missing' || value.releaseMemberState === 'locale-removed') ||
|
|
353
|
+
!(value.degradedReason === undefined || value.degradedReason === 'archived' || value.degradedReason === 'missing' || value.degradedReason === 'locale-removed')
|
|
354
|
+
) return null
|
|
355
|
+
return {
|
|
356
|
+
requestedVariantId: value.requestedVariantId,
|
|
357
|
+
requestedDocumentId: value.requestedDocumentId,
|
|
358
|
+
...(value.requestedLocale !== undefined
|
|
359
|
+
? { requestedLocale: value.requestedLocale }
|
|
360
|
+
: {}),
|
|
361
|
+
...(value.appliedLocale !== undefined
|
|
362
|
+
? { appliedLocale: value.appliedLocale }
|
|
363
|
+
: {}),
|
|
364
|
+
appliesToDocument: value.appliesToDocument,
|
|
365
|
+
appliedVariantId: value.appliedVariantId,
|
|
366
|
+
requestedReleaseId: value.requestedReleaseId,
|
|
367
|
+
releaseScopeStatus: value.releaseScopeStatus,
|
|
368
|
+
...(typeof value.releaseLifecycleStatus === 'string'
|
|
369
|
+
? { releaseLifecycleStatus: value.releaseLifecycleStatus }
|
|
370
|
+
: {}),
|
|
371
|
+
appliedReleaseId: value.appliedReleaseId,
|
|
372
|
+
...(typeof value.releaseMemberState === 'string'
|
|
373
|
+
? { releaseMemberState: value.releaseMemberState }
|
|
374
|
+
: {}),
|
|
375
|
+
...(typeof value.degradedReason === 'string'
|
|
376
|
+
? { degradedReason: value.degradedReason }
|
|
377
|
+
: {}),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeReadContext(value) {
|
|
382
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
383
|
+
Object.keys(value).sort().join(',') !== 'contextId,perspective,revision' ||
|
|
384
|
+
typeof value.contextId !== 'string' || !value.contextId || value.contextId.length > 512 ||
|
|
385
|
+
!Number.isSafeInteger(value.revision) || value.revision < 1) return null
|
|
386
|
+
const p = value.perspective
|
|
387
|
+
if (!p || typeof p !== 'object' || Array.isArray(p)) return null
|
|
388
|
+
const keys = Object.keys(p).sort().join(',')
|
|
389
|
+
if ((p.kind === 'living' || p.kind === 'published') && keys === 'kind') return value
|
|
390
|
+
const field = p.kind === 'variant' ? 'variantId' : p.kind === 'release' ? 'releaseId' : null
|
|
391
|
+
return field && keys === ['kind', field].sort().join(',') &&
|
|
392
|
+
typeof p[field] === 'string' && p[field].length > 0 && p[field].length <= 512 ? value : null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function authenticatedReadContext() {
|
|
396
|
+
const state = window.__storystick_preview_runtime__?.state
|
|
397
|
+
return state?.state === 'authenticated' && state?.session?.readContextStatus === 'active'
|
|
398
|
+
? normalizeReadContext(state.session.readContext) : null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function matchesCurrentReadContext(dependencies) {
|
|
402
|
+
if (!dependencies?.readContext) return true
|
|
403
|
+
const current = authenticatedReadContext()
|
|
404
|
+
return current !== null && current.contextId === dependencies.readContext.contextId &&
|
|
405
|
+
current.revision === dependencies.readContext.revision
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeDependencySet(value) {
|
|
409
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
|
410
|
+
if (
|
|
411
|
+
value.type !== 'storystick.preview.frame-dependency-set' ||
|
|
412
|
+
value.version !== 1 ||
|
|
413
|
+
(!(typeof value.sessionId === 'string' && value.sessionId.length > 0) && !normalizeReadContext(value.readContext)) ||
|
|
414
|
+
typeof value.routeHash !== 'string' || value.routeHash.length === 0 ||
|
|
415
|
+
typeof value.codeVersion !== 'string' || value.codeVersion.length === 0 ||
|
|
416
|
+
typeof value.frameId !== 'string' || value.frameId.length === 0 ||
|
|
417
|
+
typeof value.renderId !== 'string' || value.renderId.length === 0 ||
|
|
418
|
+
!Number.isFinite(value.generation) || value.generation <= 0 ||
|
|
419
|
+
!Array.isArray(value.dependencies) || value.dependencies.length > 256 ||
|
|
420
|
+
(value.observationId !== undefined && typeof value.observationId !== 'string') ||
|
|
421
|
+
(value.renderWatermark !== undefined && !Number.isFinite(value.renderWatermark))
|
|
422
|
+
) return null
|
|
423
|
+
if (JSON.stringify(value).length > 1_000_000) return null
|
|
424
|
+
const readContext = value.readContext === undefined ? undefined : normalizeReadContext(value.readContext)
|
|
425
|
+
if (readContext === null) return null
|
|
426
|
+
if (value.receiptIds !== undefined && (!Array.isArray(value.receiptIds) || value.receiptIds.length > 64 ||
|
|
427
|
+
!value.receiptIds.every((id) => typeof id === 'string' && /^pqr_[a-f0-9]{64}$/.test(id)))) return null
|
|
428
|
+
const dependencies = value.dependencies.flatMap(normalizeDependency)
|
|
429
|
+
if (dependencies.length !== value.dependencies.length) return null
|
|
430
|
+
const variantId = normalizeVariantId(value.variantId)
|
|
431
|
+
return {
|
|
432
|
+
type: 'storystick.preview.frame-dependency-set',
|
|
433
|
+
version: 1,
|
|
434
|
+
...(value.sessionId ? { sessionId: value.sessionId } : {}),
|
|
435
|
+
...(readContext ? { readContext } : {}),
|
|
436
|
+
...(value.receiptIds ? { receiptIds: value.receiptIds } : {}),
|
|
437
|
+
routeHash: value.routeHash,
|
|
438
|
+
codeVersion: value.codeVersion,
|
|
439
|
+
...(variantId ? { variantId } : {}),
|
|
440
|
+
frameId: value.frameId,
|
|
441
|
+
renderId: value.renderId,
|
|
442
|
+
generation: value.generation,
|
|
443
|
+
...(value.observationId !== undefined
|
|
444
|
+
? { observationId: value.observationId }
|
|
445
|
+
: {}),
|
|
446
|
+
...(value.renderWatermark !== undefined
|
|
447
|
+
? { renderWatermark: value.renderWatermark }
|
|
448
|
+
: {}),
|
|
449
|
+
dependencies,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizeDependency(value) {
|
|
454
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return []
|
|
455
|
+
const required = (candidate) =>
|
|
456
|
+
typeof candidate === 'string' && candidate.trim().length > 0
|
|
457
|
+
? candidate.trim()
|
|
458
|
+
: null
|
|
459
|
+
switch (value.kind) {
|
|
460
|
+
case 'document': {
|
|
461
|
+
const documentId = required(value.documentId)
|
|
462
|
+
if (!documentId) return []
|
|
463
|
+
return [{
|
|
464
|
+
kind: 'document',
|
|
465
|
+
documentId,
|
|
466
|
+
...(required(value.storyType) ? { storyType: required(value.storyType) } : {}),
|
|
467
|
+
...(required(value.reason) ? { reason: required(value.reason) } : {}),
|
|
468
|
+
}]
|
|
469
|
+
}
|
|
470
|
+
case 'asset': {
|
|
471
|
+
const assetId = required(value.assetId)
|
|
472
|
+
return assetId ? [{ kind: 'asset', assetId }] : []
|
|
473
|
+
}
|
|
474
|
+
case 'route-lookup': {
|
|
475
|
+
const pathname = required(value.pathname)
|
|
476
|
+
if (!pathname) return []
|
|
477
|
+
const storyTypes = Array.isArray(value.storyTypes)
|
|
478
|
+
? [...new Set(value.storyTypes.filter((entry) => required(entry)).map((entry) => required(entry)))]
|
|
479
|
+
: []
|
|
480
|
+
return [{ kind: 'route-lookup', pathname, ...(storyTypes.length > 0 ? { storyTypes } : {}) }]
|
|
481
|
+
}
|
|
482
|
+
case 'list': {
|
|
483
|
+
const storyType = required(value.storyType)
|
|
484
|
+
if (!storyType) return []
|
|
485
|
+
const orderBy = value.orderBy &&
|
|
486
|
+
required(value.orderBy.field) &&
|
|
487
|
+
(value.orderBy.direction === 'asc' || value.orderBy.direction === 'desc')
|
|
488
|
+
? { field: required(value.orderBy.field), direction: value.orderBy.direction }
|
|
489
|
+
: undefined
|
|
490
|
+
return [{
|
|
491
|
+
kind: 'list',
|
|
492
|
+
storyType,
|
|
493
|
+
...(value.where !== undefined ? { where: value.where } : {}),
|
|
494
|
+
...(orderBy ? { orderBy } : {}),
|
|
495
|
+
...(typeof value.limit === 'number' && Number.isFinite(value.limit)
|
|
496
|
+
? { limit: value.limit }
|
|
497
|
+
: {}),
|
|
498
|
+
}]
|
|
499
|
+
}
|
|
500
|
+
case 'get-by-slug':
|
|
501
|
+
case 'get-by-pathname': {
|
|
502
|
+
const storyType = required(value.storyType)
|
|
503
|
+
const field = value.kind === 'get-by-slug' ? 'slug' : 'pathname'
|
|
504
|
+
const target = required(value[field])
|
|
505
|
+
return storyType && target
|
|
506
|
+
? [{ kind: value.kind, storyType, [field]: target }]
|
|
507
|
+
: []
|
|
508
|
+
}
|
|
509
|
+
case 'batch-get': {
|
|
510
|
+
if (!Array.isArray(value.documentIds)) return []
|
|
511
|
+
const documentIds = [...new Set(value.documentIds
|
|
512
|
+
.filter((entry) => required(entry))
|
|
513
|
+
.map((entry) => required(entry)))]
|
|
514
|
+
return documentIds.length > 0 ? [{ kind: 'batch-get', documentIds }] : []
|
|
515
|
+
}
|
|
516
|
+
case 'template': {
|
|
517
|
+
const templateId = required(value.templateId)
|
|
518
|
+
return templateId ? [{ kind: 'template', templateId }] : []
|
|
519
|
+
}
|
|
520
|
+
case 'custom-read': {
|
|
521
|
+
const key = required(value.key)
|
|
522
|
+
return key ? [{ kind: 'custom-read', key }] : []
|
|
523
|
+
}
|
|
524
|
+
default:
|
|
525
|
+
return []
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function emptySidecar() {
|
|
530
|
+
return {
|
|
531
|
+
type: 'storystick.preview.public-sidecar',
|
|
532
|
+
version: 1,
|
|
533
|
+
routeHash: '',
|
|
534
|
+
codeVersion: '',
|
|
535
|
+
renderId: '',
|
|
536
|
+
generation: 0,
|
|
537
|
+
renderNonce: '',
|
|
538
|
+
routeRenderNonce: '',
|
|
539
|
+
emissionRevision: 0,
|
|
540
|
+
markers: [],
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function normalizeText(value) {
|
|
545
|
+
return value
|
|
546
|
+
.normalize('NFKD')
|
|
547
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
548
|
+
.replace(/[\u201c\u201d]/g, '"')
|
|
549
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
550
|
+
.replace(/\s+/g, ' ')
|
|
551
|
+
.trim()
|
|
552
|
+
.toLowerCase()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function fingerprintRenderedText(value) {
|
|
556
|
+
const normalized = normalizeText(value)
|
|
557
|
+
let hash = 0x811c9dc5
|
|
558
|
+
for (let index = 0; index < normalized.length; index++) {
|
|
559
|
+
hash ^= normalized.charCodeAt(index)
|
|
560
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
561
|
+
}
|
|
562
|
+
return 'txt:' + normalized.length + ':' + hash.toString(36)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function compareSnapshotOrder(left, right) {
|
|
566
|
+
if (left.generation !== right.generation) {
|
|
567
|
+
return left.generation < right.generation ? -1 : 1
|
|
568
|
+
}
|
|
569
|
+
if (left.emissionRevision === right.emissionRevision) return 0
|
|
570
|
+
return left.emissionRevision < right.emissionRevision ? -1 : 1
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function validSnapshot(snapshot) {
|
|
574
|
+
if (!snapshot || typeof snapshot !== 'object') return false
|
|
575
|
+
if (
|
|
576
|
+
snapshot.type !== 'storystick.preview.provenance-sidecar-snapshot' ||
|
|
577
|
+
snapshot.version !== 2 ||
|
|
578
|
+
typeof snapshot.sessionId !== 'string' || snapshot.sessionId.length === 0 ||
|
|
579
|
+
typeof snapshot.routeHash !== 'string' ||
|
|
580
|
+
typeof snapshot.codeVersion !== 'string' ||
|
|
581
|
+
typeof snapshot.renderId !== 'string' ||
|
|
582
|
+
typeof snapshot.renderNonce !== 'string' || snapshot.renderNonce.length === 0 ||
|
|
583
|
+
!Number.isFinite(snapshot.generation) || snapshot.generation <= 0 ||
|
|
584
|
+
!Number.isFinite(snapshot.emissionRevision) || snapshot.emissionRevision <= 0 ||
|
|
585
|
+
(snapshot.operation !== 'replace' && snapshot.operation !== 'tombstone') ||
|
|
586
|
+
snapshot.complete !== true ||
|
|
587
|
+
!Array.isArray(snapshot.expectedFrameIds) ||
|
|
588
|
+
!Array.isArray(snapshot.frames)
|
|
589
|
+
) return false
|
|
590
|
+
if (snapshot.operation === 'tombstone') {
|
|
591
|
+
return snapshot.expectedFrameIds.length === 0 && snapshot.frames.length === 0
|
|
592
|
+
}
|
|
593
|
+
const expected = new Set(snapshot.expectedFrameIds)
|
|
594
|
+
if (expected.size !== snapshot.expectedFrameIds.length || !expected.has('route')) return false
|
|
595
|
+
const received = new Set()
|
|
596
|
+
for (const frame of snapshot.frames) {
|
|
597
|
+
if (!frame || typeof frame !== 'object' || typeof frame.frameId !== 'string') return false
|
|
598
|
+
if (received.has(frame.frameId)) return false
|
|
599
|
+
const sidecar = frame.sidecar
|
|
600
|
+
if (
|
|
601
|
+
!sidecar || typeof sidecar !== 'object' ||
|
|
602
|
+
sidecar.type !== 'storystick.preview.public-sidecar' ||
|
|
603
|
+
sidecar.version !== 1 ||
|
|
604
|
+
sidecar.codeVersion !== snapshot.codeVersion ||
|
|
605
|
+
!Array.isArray(sidecar.markers) ||
|
|
606
|
+
typeof sidecar.renderNonce !== 'string' || sidecar.renderNonce.length === 0 ||
|
|
607
|
+
!Number.isFinite(sidecar.emissionRevision) || sidecar.emissionRevision <= 0 ||
|
|
608
|
+
(frame.frameId === 'route' && (
|
|
609
|
+
sidecar.routeHash !== snapshot.routeHash ||
|
|
610
|
+
sidecar.renderId !== snapshot.renderId ||
|
|
611
|
+
sidecar.renderNonce !== snapshot.renderNonce
|
|
612
|
+
))
|
|
613
|
+
) return false
|
|
614
|
+
received.add(frame.frameId)
|
|
615
|
+
}
|
|
616
|
+
return received.size === expected.size && [...expected].every((id) => received.has(id))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function installSidecarStore() {
|
|
620
|
+
const existing = window.__ssPreviewProvenanceSidecarStoreV2
|
|
621
|
+
if (existing && existing.version === 2) return existing
|
|
622
|
+
let active = null
|
|
623
|
+
let latestAccepted = null
|
|
624
|
+
const store = {
|
|
625
|
+
version: 2,
|
|
626
|
+
accept(snapshot) {
|
|
627
|
+
if (!validSnapshot(snapshot)) return false
|
|
628
|
+
if (latestAccepted !== null) {
|
|
629
|
+
const order = compareSnapshotOrder(snapshot, latestAccepted)
|
|
630
|
+
if (order < 0) return false
|
|
631
|
+
if (order === 0) {
|
|
632
|
+
if (snapshot.renderNonce !== latestAccepted.renderNonce) return false
|
|
633
|
+
if (snapshot.operation === 'tombstone') return true
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
latestAccepted = snapshot
|
|
637
|
+
active = snapshot.operation === 'tombstone' ? null : snapshot
|
|
638
|
+
return true
|
|
639
|
+
},
|
|
640
|
+
readSnapshot: () => active,
|
|
641
|
+
readFrames: () => active ? active.frames : [],
|
|
642
|
+
}
|
|
643
|
+
window.__ssPreviewProvenanceSidecarStoreV2 = store
|
|
644
|
+
return store
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const sidecarStore = installSidecarStore()
|
|
648
|
+
|
|
649
|
+
function readCarrier(value) {
|
|
650
|
+
if (typeof value !== 'string' || !value.startsWith('ssr1.')) return null
|
|
651
|
+
if (readCarrierCache.has(value)) return readCarrierCache.get(value)
|
|
652
|
+
const decoded = decodeReadCarrier(value)
|
|
653
|
+
const sidecar = decoded ? parseSidecar(JSON.stringify(decoded.sidecar)) : null
|
|
654
|
+
const frame = sidecar?.dependencySet && sidecar.markers.length === 1 &&
|
|
655
|
+
(!sidecar.dependencySet.readContext || sidecar.dependencySet.receiptIds?.length > 0)
|
|
656
|
+
? { frameId: decoded.frameId, sidecar, primaryPage: decoded.primaryPage } : null
|
|
657
|
+
if (readCarrierCache.size >= 2048) readCarrierCache.delete(readCarrierCache.keys().next().value)
|
|
658
|
+
readCarrierCache.set(value, frame)
|
|
659
|
+
return frame
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function normalizedCarrierMarkerId(value) {
|
|
663
|
+
return readCarrier(value)?.sidecar.markers[0].markerId || value
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Only explicitly encoded text/attributes participate. These frames describe
|
|
667
|
+
// what is in the DOM now, so removed branches need no React unmount publisher.
|
|
668
|
+
function collectReadCarrierFrames() {
|
|
669
|
+
observationStats.readCarrierScans += 1
|
|
670
|
+
const carriers = new Set()
|
|
671
|
+
const root = document.body || document.documentElement
|
|
672
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
673
|
+
let node = walker.nextNode()
|
|
674
|
+
while (node) {
|
|
675
|
+
if (node instanceof Text && node.data.includes(CODED_TEXT_START) &&
|
|
676
|
+
!node.parentElement?.closest('script,style,template,noscript,textarea,title,option')) {
|
|
677
|
+
// The closing delimiter can be in another text node (e.g. an <em>
|
|
678
|
+
// inside a heading). The complete prefix alone carries read identity.
|
|
679
|
+
let cursor = 0
|
|
680
|
+
while (cursor < node.data.length) {
|
|
681
|
+
const start = node.data.indexOf(CODED_TEXT_START, cursor)
|
|
682
|
+
if (start < 0) break
|
|
683
|
+
const separator = node.data.indexOf(CODED_TEXT_SEPARATOR, start + CODED_TEXT_START.length)
|
|
684
|
+
if (separator < 0) break
|
|
685
|
+
const id = decodeTagAscii(node.data.slice(start + CODED_TEXT_START.length, separator))
|
|
686
|
+
if (id.startsWith('ssr1.')) carriers.add(id)
|
|
687
|
+
cursor = separator + CODED_TEXT_SEPARATOR.length
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
node = walker.nextNode()
|
|
691
|
+
}
|
|
692
|
+
for (const element of document.querySelectorAll('[data-ss-e],[data-ss-p]')) {
|
|
693
|
+
for (const name of ['data-ss-e', 'data-ss-p']) {
|
|
694
|
+
const value = element.getAttribute(name)
|
|
695
|
+
if (value?.startsWith('ssr1.')) carriers.add(value)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const signature = [...carriers].sort().join('|')
|
|
699
|
+
if (signature === readCarrierSignature) return false
|
|
700
|
+
readCarrierSignature = signature
|
|
701
|
+
const frames = new Map()
|
|
702
|
+
primaryPageCandidates = []
|
|
703
|
+
for (const value of carriers) {
|
|
704
|
+
const frame = readCarrier(value)
|
|
705
|
+
if (!frame) continue
|
|
706
|
+
if (frame.primaryPage) primaryPageCandidates.push({ primaryPage: frame.primaryPage, dependencySet: frame.sidecar.dependencySet })
|
|
707
|
+
const existing = frames.get(frame.frameId)
|
|
708
|
+
const sidecar = !existing || frame.sidecar.emissionRevision > existing.sidecar.emissionRevision
|
|
709
|
+
? frame.sidecar : existing.sidecar
|
|
710
|
+
const markers = new Map((existing?.sidecar.markers || []).map(marker => [marker.markerId, marker]))
|
|
711
|
+
for (const marker of frame.sidecar.markers) markers.set(marker.markerId, marker)
|
|
712
|
+
frames.set(frame.frameId, { frameId: frame.frameId, sidecar: { ...sidecar, markers: [...markers.values()] } })
|
|
713
|
+
}
|
|
714
|
+
readCarrierFrames = [...frames.values()]
|
|
715
|
+
return true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function readFrames() {
|
|
719
|
+
const stored = sidecarStore.readFrames().filter((frame) => matchesCurrentReadContext(frame.sidecar.dependencySet))
|
|
720
|
+
if (stored.length > 0) return stored
|
|
721
|
+
const framesById = new Map()
|
|
722
|
+
for (const script of document.querySelectorAll('script[data-ss-provenance-frame]')) {
|
|
723
|
+
const frameId = script.getAttribute('data-ss-provenance-frame') || ''
|
|
724
|
+
if (frameId.length === 0) continue
|
|
725
|
+
framesById.set(frameId, {
|
|
726
|
+
frameId,
|
|
727
|
+
sidecar: parseSidecar(script.textContent),
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
return Array.from(framesById.values())
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function routeFrameHash() {
|
|
734
|
+
const storedRoute = sidecarStore.readFrames().find((frame) => frame.frameId === 'route')
|
|
735
|
+
if (storedRoute) return storedRoute.sidecar.routeHash
|
|
736
|
+
const route = document.querySelector('[data-ss-route-frame-hash]')
|
|
737
|
+
return route && route.getAttribute('data-ss-route-frame-hash') || ''
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function parsePublisher(element) {
|
|
741
|
+
const raw = element.content && typeof element.content.textContent === 'string'
|
|
742
|
+
? element.content.textContent
|
|
743
|
+
: element.textContent
|
|
744
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) return null
|
|
745
|
+
try {
|
|
746
|
+
const publisher = JSON.parse(raw)
|
|
747
|
+
if (
|
|
748
|
+
!publisher || typeof publisher !== 'object' ||
|
|
749
|
+
publisher.type !== 'storystick.preview.provenance-publisher' ||
|
|
750
|
+
publisher.version !== 2 ||
|
|
751
|
+
!publisher.frame || typeof publisher.frame !== 'object' ||
|
|
752
|
+
typeof publisher.frame.frameId !== 'string' || publisher.frame.frameId.length === 0
|
|
753
|
+
) return null
|
|
754
|
+
const sidecar = parseSidecar(JSON.stringify(publisher.frame.sidecar))
|
|
755
|
+
if (
|
|
756
|
+
sidecar.routeHash.length === 0 ||
|
|
757
|
+
sidecar.codeVersion.length === 0 ||
|
|
758
|
+
sidecar.renderId.length === 0 ||
|
|
759
|
+
sidecar.renderNonce.length === 0 ||
|
|
760
|
+
sidecar.generation <= 0 ||
|
|
761
|
+
sidecar.emissionRevision <= 0
|
|
762
|
+
) return null
|
|
763
|
+
const dependencySet = sidecar.dependencySet
|
|
764
|
+
const boundSession = sessionId()
|
|
765
|
+
if (dependencySet?.readContext) {
|
|
766
|
+
if (!authenticatedReadContext() || !boundSession ||
|
|
767
|
+
!dependencySet.receiptIds?.length) return null
|
|
768
|
+
sidecar.dependencySet = { ...dependencySet, sessionId: boundSession }
|
|
769
|
+
} else if (typeof publisher.sessionId !== 'string' || !publisher.sessionId) return null
|
|
770
|
+
return {
|
|
771
|
+
sessionId: dependencySet?.readContext ? boundSession : publisher.sessionId,
|
|
772
|
+
frame: { frameId: publisher.frame.frameId, sidecar },
|
|
773
|
+
}
|
|
774
|
+
} catch {
|
|
775
|
+
return null
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function publisherConsumptionKey(frame) {
|
|
780
|
+
return JSON.stringify([frame.frameId, frame.sidecar.renderNonce])
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function acknowledgePublisherConsumption(frame) {
|
|
784
|
+
const publisherKey = publisherConsumptionKey(frame)
|
|
785
|
+
const current = window[PUBLISHER_CONSUMED_REGISTRY]
|
|
786
|
+
const registry = current && typeof current === 'object' ? current : {}
|
|
787
|
+
registry[publisherKey] = true
|
|
788
|
+
const keys = Object.keys(registry)
|
|
789
|
+
for (let index = 0; index < keys.length - 128; index++) {
|
|
790
|
+
delete registry[keys[index]]
|
|
791
|
+
}
|
|
792
|
+
window[PUBLISHER_CONSUMED_REGISTRY] = registry
|
|
793
|
+
try {
|
|
794
|
+
window.dispatchEvent(new CustomEvent(PUBLISHER_CONSUMED_EVENT, {
|
|
795
|
+
detail: { publisherKey },
|
|
796
|
+
}))
|
|
797
|
+
} catch {
|
|
798
|
+
// The registry is the durable acknowledgement if CustomEvent is absent.
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function publisherFrameIsNewer(next, current) {
|
|
803
|
+
if (!current) return true
|
|
804
|
+
if (next.sidecar.generation !== current.sidecar.generation) {
|
|
805
|
+
return next.sidecar.generation > current.sidecar.generation
|
|
806
|
+
}
|
|
807
|
+
if (next.sidecar.emissionRevision !== current.sidecar.emissionRevision) {
|
|
808
|
+
return next.sidecar.emissionRevision > current.sidecar.emissionRevision
|
|
809
|
+
}
|
|
810
|
+
return false
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function publishRenderedContext(readContext) {
|
|
814
|
+
const detail = { sessionId: sessionId(), readContext }
|
|
815
|
+
const key = JSON.stringify(detail)
|
|
816
|
+
if (key === lastRenderedContextKey) return
|
|
817
|
+
lastRenderedContextKey = key
|
|
818
|
+
window.dispatchEvent(new CustomEvent('storystick.preview.rendered-context', { detail }))
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Read navigation metadata independently of overlay readiness/convergence.
|
|
822
|
+
function currentPrimaryPages() {
|
|
823
|
+
return authenticatedReadContext() ? primaryPageCandidates.filter(candidate =>
|
|
824
|
+
candidate.dependencySet?.readContext && matchesCurrentReadContext(candidate.dependencySet))
|
|
825
|
+
.map(candidate => candidate.primaryPage) : []
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function publishPrimaryPages() {
|
|
829
|
+
const context = authenticatedReadContext()
|
|
830
|
+
const pages = currentPrimaryPages()
|
|
831
|
+
const detail = { sessionId: sessionId(), readContext: context,
|
|
832
|
+
pathname: window.location.pathname, pages }
|
|
833
|
+
const key = JSON.stringify(detail)
|
|
834
|
+
if (key === lastPrimaryPagesKey) return
|
|
835
|
+
lastPrimaryPagesKey = key
|
|
836
|
+
window.__ssPreviewPrimaryPages = detail
|
|
837
|
+
window.dispatchEvent(new CustomEvent('storystick.preview.primary-pages', { detail }))
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Capsules are acknowledged synchronously so their React owner can retire
|
|
841
|
+
// them. A repeated reconciliation can reuse the last carrier observation
|
|
842
|
+
// until the document changes; it never caches geometry across passes.
|
|
843
|
+
function consumeProvenancePublishers() {
|
|
844
|
+
let changed = false
|
|
845
|
+
for (const element of document.querySelectorAll('template[data-ss-provenance-publisher]')) {
|
|
846
|
+
if (consumedPublisherElements.has(element)) continue
|
|
847
|
+
const publisher = parsePublisher(element)
|
|
848
|
+
// An unbound server capsule waits for authenticated browser identity.
|
|
849
|
+
if (publisher === null) continue
|
|
850
|
+
consumedPublisherElements.add(element)
|
|
851
|
+
if (element.getAttribute('data-ss-provenance-react-owned') !== 'true') {
|
|
852
|
+
element.remove()
|
|
853
|
+
}
|
|
854
|
+
if (publisher === null) continue
|
|
855
|
+
const frame = publisher.frame
|
|
856
|
+
acknowledgePublisherConsumption(frame)
|
|
857
|
+
if (
|
|
858
|
+
frame.frameId.startsWith('slice_') &&
|
|
859
|
+
(
|
|
860
|
+
activePublisherRoute === null ||
|
|
861
|
+
publisher.sessionId !== activePublisherRoute.sessionId ||
|
|
862
|
+
frame.sidecar.routeHash !== activePublisherRoute.routeHash ||
|
|
863
|
+
frame.sidecar.generation !== activePublisherRoute.generation ||
|
|
864
|
+
frame.sidecar.codeVersion !== activePublisherRoute.codeVersion ||
|
|
865
|
+
frame.sidecar.routeRenderNonce !== activePublisherRoute.renderNonce
|
|
866
|
+
)
|
|
867
|
+
) {
|
|
868
|
+
continue
|
|
869
|
+
}
|
|
870
|
+
const current = publisherFramesById.get(frame.frameId)
|
|
871
|
+
if (!publisherFrameIsNewer(frame, current)) continue
|
|
872
|
+
if (frame.frameId === 'route') {
|
|
873
|
+
const routeChanged = activePublisherRoute === null ||
|
|
874
|
+
activePublisherRoute.renderNonce !== frame.sidecar.renderNonce
|
|
875
|
+
if (routeChanged) {
|
|
876
|
+
const oldRouteHash = activePublisherRoute && activePublisherRoute.routeHash
|
|
877
|
+
if (oldRouteHash) {
|
|
878
|
+
for (const [frameId, registered] of publisherFramesById) {
|
|
879
|
+
if (registered.sidecar.routeHash === oldRouteHash) {
|
|
880
|
+
publisherFramesById.delete(frameId)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const activationGeneration = Math.max(
|
|
885
|
+
currentRenderIdentity.generation,
|
|
886
|
+
frame.sidecar.generation,
|
|
887
|
+
)
|
|
888
|
+
const activationRevision = Math.max(
|
|
889
|
+
lastPublisherRevision + 1,
|
|
890
|
+
frame.sidecar.emissionRevision,
|
|
891
|
+
)
|
|
892
|
+
sidecarStore.accept({
|
|
893
|
+
type: 'storystick.preview.provenance-sidecar-snapshot',
|
|
894
|
+
version: 2,
|
|
895
|
+
sessionId: publisher.sessionId,
|
|
896
|
+
routeHash: frame.sidecar.routeHash,
|
|
897
|
+
codeVersion: frame.sidecar.codeVersion,
|
|
898
|
+
variantId: normalizeVariantId(frame.sidecar.variantId),
|
|
899
|
+
renderId: frame.sidecar.renderId,
|
|
900
|
+
renderNonce: frame.sidecar.renderNonce,
|
|
901
|
+
generation: activationGeneration,
|
|
902
|
+
emissionRevision: activationRevision,
|
|
903
|
+
operation: 'tombstone',
|
|
904
|
+
complete: true,
|
|
905
|
+
expectedFrameIds: [],
|
|
906
|
+
frames: [],
|
|
907
|
+
})
|
|
908
|
+
lastPublisherRevision = activationRevision
|
|
909
|
+
activePublisherRoute = {
|
|
910
|
+
sessionId: publisher.sessionId,
|
|
911
|
+
routeHash: frame.sidecar.routeHash,
|
|
912
|
+
renderNonce: frame.sidecar.renderNonce,
|
|
913
|
+
generation: frame.sidecar.generation,
|
|
914
|
+
codeVersion: frame.sidecar.codeVersion,
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
publisherFramesById.set(frame.frameId, frame)
|
|
919
|
+
changed = true
|
|
920
|
+
}
|
|
921
|
+
if (changed) publisherSnapshotDirty = true
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function reconcileProvenancePublishers() {
|
|
925
|
+
// A context/probe event may arrive before MutationObserver delivery.
|
|
926
|
+
// Consume its pending facts before deciding whether the carrier sample is
|
|
927
|
+
// still current. The caller already owns scheduling the observation pass.
|
|
928
|
+
const pendingMutations = provenanceObserver ? provenanceObserver.takeRecords() : []
|
|
929
|
+
if (pendingMutations.some(invalidatesReadCarriers)) {
|
|
930
|
+
readCarriersDirty = true
|
|
931
|
+
}
|
|
932
|
+
if (readCarriersDirty) {
|
|
933
|
+
readCarriersDirty = false
|
|
934
|
+
if (collectReadCarrierFrames()) publisherSnapshotDirty = true
|
|
935
|
+
}
|
|
936
|
+
consumeProvenancePublishers()
|
|
937
|
+
// Navigation metadata remains independent of anchor/overlay readiness.
|
|
938
|
+
publishPrimaryPages()
|
|
939
|
+
if (!publisherSnapshotDirty) return
|
|
940
|
+
publisherSnapshotDirty = false
|
|
941
|
+
// A refresh may stream the page before its persistent layouts. Keep the
|
|
942
|
+
// whole view read-only until every mounted read has the accepted context;
|
|
943
|
+
// filtering old occurrences individually would make a mixed view editable.
|
|
944
|
+
const mounted = [...publisherFramesById.values(), ...readCarrierFrames]
|
|
945
|
+
const pendingReadContext = mounted.some((frame) =>
|
|
946
|
+
frame.sidecar.dependencySet?.readContext && !matchesCurrentReadContext(frame.sidecar.dependencySet))
|
|
947
|
+
const occurrences = (pendingReadContext ? [] : mounted)
|
|
948
|
+
.filter((frame) => matchesCurrentReadContext(frame.sidecar.dependencySet))
|
|
949
|
+
.map((frame) => frame.sidecar.dependencySet?.readContext ? {
|
|
950
|
+
...frame, sidecar: { ...frame.sidecar, dependencySet: {
|
|
951
|
+
...frame.sidecar.dependencySet, sessionId: sessionId(),
|
|
952
|
+
} },
|
|
953
|
+
} : frame)
|
|
954
|
+
let routeFrame = occurrences.find((frame) => frame.frameId === 'route')
|
|
955
|
+
if (routeFrame?.sidecar.dependencySet?.readContext && activePublisherRoute) {
|
|
956
|
+
activePublisherRoute = { ...activePublisherRoute, sessionId: sessionId() }
|
|
957
|
+
}
|
|
958
|
+
if (!routeFrame && occurrences.length > 0) {
|
|
959
|
+
// Native pages need no CMS document. This empty view anchor supplies
|
|
960
|
+
// navigation identity; read/edit authority stays with mounted occurrences.
|
|
961
|
+
const currentContext = authenticatedReadContext()
|
|
962
|
+
const candidates = occurrences.filter((frame) => frame.sidecar.dependencySet?.readContext
|
|
963
|
+
? matchesCurrentReadContext(frame.sidecar.dependencySet)
|
|
964
|
+
: frame.sidecar.dependencySet?.sessionId === sessionId())
|
|
965
|
+
const first = candidates[0]
|
|
966
|
+
if (first && currentContext && sessionId()) {
|
|
967
|
+
const key = JSON.stringify([window.location.pathname, window.location.search,
|
|
968
|
+
sessionId(), currentContext.contextId, currentContext.revision, first.sidecar.codeVersion])
|
|
969
|
+
if (nativeViewAnchor?.key !== key) {
|
|
970
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
|
971
|
+
const nonce = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
972
|
+
nativeViewAnchor = { key, nonce: 'view:' + nonce, revision: Date.now() * 1000 }
|
|
973
|
+
}
|
|
974
|
+
const routeHash = 'view:' + window.location.pathname
|
|
975
|
+
const renderId = nativeViewAnchor.nonce
|
|
976
|
+
const generation = Math.max(1, currentRenderIdentity.generation)
|
|
977
|
+
const receiptIds = [...new Set(candidates.flatMap((frame) => frame.sidecar.dependencySet.receiptIds || []))]
|
|
978
|
+
routeFrame = { frameId: 'route', sidecar: {
|
|
979
|
+
type: 'storystick.preview.public-sidecar', version: 1,
|
|
980
|
+
routeHash, codeVersion: first.sidecar.codeVersion, renderId,
|
|
981
|
+
renderNonce: nativeViewAnchor.nonce, generation, emissionRevision: nativeViewAnchor.revision,
|
|
982
|
+
...(currentContext.perspective.kind === 'variant' ? { variantId: currentContext.perspective.variantId } : {}),
|
|
983
|
+
...(first.sidecar.previewScope ? { previewScope: first.sidecar.previewScope } : {}),
|
|
984
|
+
markers: [],
|
|
985
|
+
dependencySet: { type: 'storystick.preview.frame-dependency-set', version: 1,
|
|
986
|
+
sessionId: sessionId(), readContext: currentContext, receiptIds,
|
|
987
|
+
routeHash, codeVersion: first.sidecar.codeVersion, frameId: 'route', renderId, generation,
|
|
988
|
+
...(candidates.every((frame) => Number.isFinite(frame.sidecar.dependencySet.renderWatermark))
|
|
989
|
+
? { renderWatermark: Math.min(...candidates.map((frame) => frame.sidecar.dependencySet.renderWatermark)) } : {}),
|
|
990
|
+
...(currentContext.perspective.kind === 'variant' ? { variantId: currentContext.perspective.variantId } : {}),
|
|
991
|
+
dependencies: [],
|
|
992
|
+
},
|
|
993
|
+
} }
|
|
994
|
+
activePublisherRoute = { sessionId: sessionId(), routeHash,
|
|
995
|
+
renderNonce: nativeViewAnchor.nonce, generation, codeVersion: first.sidecar.codeVersion }
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (!routeFrame || !activePublisherRoute || routeFrame.sidecar.renderNonce !== activePublisherRoute.renderNonce) {
|
|
999
|
+
publishRenderedContext(null)
|
|
1000
|
+
const previous = lastPublishedSnapshot ?? sidecarStore.readSnapshot()
|
|
1001
|
+
if (previous) {
|
|
1002
|
+
const tombstone = { ...previous, operation: 'tombstone', expectedFrameIds: [], frames: [],
|
|
1003
|
+
generation: Math.max(previous.generation, currentRenderIdentity.generation),
|
|
1004
|
+
emissionRevision: Math.max(lastPublisherRevision + 1, previous.emissionRevision + 1) }
|
|
1005
|
+
lastPublishedSnapshot = tombstone
|
|
1006
|
+
lastPublisherRevision = tombstone.emissionRevision
|
|
1007
|
+
sidecarStore.accept(tombstone)
|
|
1008
|
+
if (window.parent && window.parent !== window) window.parent.postMessage(tombstone, studioTargetOrigin())
|
|
1009
|
+
}
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
const frames = [routeFrame, ...occurrences.filter((frame) => frame.frameId !== 'route')]
|
|
1013
|
+
.filter((frame) =>
|
|
1014
|
+
frame.sidecar.codeVersion === routeFrame.sidecar.codeVersion &&
|
|
1015
|
+
(!frame.frameId.startsWith('slice_') ||
|
|
1016
|
+
(frame.sidecar.routeHash === routeFrame.sidecar.routeHash &&
|
|
1017
|
+
frame.sidecar.generation === routeFrame.sidecar.generation &&
|
|
1018
|
+
frame.sidecar.routeRenderNonce === routeFrame.sidecar.renderNonce))
|
|
1019
|
+
)
|
|
1020
|
+
.sort((left, right) => left.frameId.localeCompare(right.frameId))
|
|
1021
|
+
const emissionRevision = Math.max(
|
|
1022
|
+
lastPublisherRevision + 1,
|
|
1023
|
+
...frames.map((frame) => frame.sidecar.emissionRevision),
|
|
1024
|
+
)
|
|
1025
|
+
lastPublisherRevision = emissionRevision
|
|
1026
|
+
const snapshot = {
|
|
1027
|
+
type: 'storystick.preview.provenance-sidecar-snapshot',
|
|
1028
|
+
version: 2,
|
|
1029
|
+
sessionId: activePublisherRoute.sessionId,
|
|
1030
|
+
routeHash: routeFrame.sidecar.routeHash,
|
|
1031
|
+
codeVersion: routeFrame.sidecar.codeVersion,
|
|
1032
|
+
variantId: normalizeVariantId(routeFrame.sidecar.variantId),
|
|
1033
|
+
renderId: routeFrame.sidecar.renderId,
|
|
1034
|
+
renderNonce: routeFrame.sidecar.renderNonce,
|
|
1035
|
+
generation: Math.max(currentRenderIdentity.generation, routeFrame.sidecar.generation),
|
|
1036
|
+
emissionRevision,
|
|
1037
|
+
operation: 'replace',
|
|
1038
|
+
complete: true,
|
|
1039
|
+
expectedFrameIds: frames.map((frame) => frame.frameId),
|
|
1040
|
+
frames,
|
|
1041
|
+
}
|
|
1042
|
+
if (!validSnapshot(snapshot)) return
|
|
1043
|
+
lastPublishedSnapshot = snapshot
|
|
1044
|
+
publishRenderedContext(authenticatedReadContext())
|
|
1045
|
+
// In Studio, the parent validates and echoes this snapshot before the
|
|
1046
|
+
// reporter consumes it. A top-level preview has no parent relay, so the
|
|
1047
|
+
// same validated server-backed snapshot must enter the same local store
|
|
1048
|
+
// directly. The anchor-map builder then reads one authority in both hosts.
|
|
1049
|
+
if (!window.parent || window.parent === window) {
|
|
1050
|
+
sidecarStore.accept(snapshot)
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
window.parent.postMessage(snapshot, studioTargetOrigin())
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function sessionId() {
|
|
1057
|
+
const state = window.__storystick_preview_runtime__?.state
|
|
1058
|
+
if (state) {
|
|
1059
|
+
if (state.previewEnded) return ''
|
|
1060
|
+
return typeof state.identity?.sessionId === 'string' ? state.identity.sessionId : ''
|
|
1061
|
+
}
|
|
1062
|
+
return (document.body && document.body.getAttribute('data-preview-message-session-id')) ||
|
|
1063
|
+
document.documentElement.getAttribute('data-preview-message-session-id') ||
|
|
1064
|
+
(document.querySelector('[data-preview-message-session-id]') &&
|
|
1065
|
+
document.querySelector('[data-preview-message-session-id]').getAttribute('data-preview-message-session-id')) ||
|
|
1066
|
+
(activePublisherRoute && activePublisherRoute.sessionId) ||
|
|
1067
|
+
''
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function normalizeVariantId(value) {
|
|
1071
|
+
if (typeof value !== 'string') return null
|
|
1072
|
+
const trimmed = value.trim()
|
|
1073
|
+
return trimmed.length > 0 ? trimmed : null
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function previewScopeFromRuntimeStore() {
|
|
1077
|
+
const store = window.__storystick_preview_runtime__
|
|
1078
|
+
if (!store || typeof store !== 'object') return null
|
|
1079
|
+
const state = store.state
|
|
1080
|
+
if (!state || typeof state !== 'object') return null
|
|
1081
|
+
const variantId = normalizeVariantId(state.variantId)
|
|
1082
|
+
const releaseId = normalizeVariantId(state.releaseId)
|
|
1083
|
+
if (variantId && !releaseId) return { kind: 'variant', id: variantId }
|
|
1084
|
+
if (releaseId && !variantId) return { kind: 'release', id: releaseId }
|
|
1085
|
+
return null
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function activePreviewScope() {
|
|
1089
|
+
return previewScopeFromRuntimeStore()
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function activeVariantId() {
|
|
1093
|
+
const scope = activePreviewScope()
|
|
1094
|
+
return scope && scope.kind === 'variant' ? scope.id : null
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function isMomentaryNavigateKeyDown(event) {
|
|
1098
|
+
if (event.key !== 'Alt') return false
|
|
1099
|
+
if (!event.altKey) return false
|
|
1100
|
+
if (event.ctrlKey || event.metaKey) return false
|
|
1101
|
+
if (typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')) return false
|
|
1102
|
+
return true
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function emitMomentaryNavigateKey(active) {
|
|
1106
|
+
if (lastMomentaryNavigateKeyActive === active) return
|
|
1107
|
+
lastMomentaryNavigateKeyActive = active
|
|
1108
|
+
window.parent.postMessage({
|
|
1109
|
+
type: 'storystick.preview.momentary-navigate-key',
|
|
1110
|
+
key: 'Alt',
|
|
1111
|
+
active,
|
|
1112
|
+
}, studioTargetOrigin())
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function normalizeMomentaryNavigateLinkClick(event, target) {
|
|
1116
|
+
if (!shouldNormalizeMomentaryNavigateLinkClick(event, target)) return false
|
|
1117
|
+
event.preventDefault()
|
|
1118
|
+
event.stopImmediatePropagation()
|
|
1119
|
+
target.click()
|
|
1120
|
+
return true
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function shouldNormalizeMomentaryNavigateLinkClick(event, target) {
|
|
1124
|
+
if (event.defaultPrevented) return false
|
|
1125
|
+
if (event.button !== 0) return false
|
|
1126
|
+
if (!event.altKey) return false
|
|
1127
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey) return false
|
|
1128
|
+
if (target.hasAttribute('download')) return false
|
|
1129
|
+
try {
|
|
1130
|
+
return new URL(target.href, window.location.href).origin === window.location.origin
|
|
1131
|
+
} catch {
|
|
1132
|
+
return false
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function topLevelRouteHash(frames, domRouteHash) {
|
|
1137
|
+
return domRouteHash ||
|
|
1138
|
+
(frames.find((frame) => frame.frameId === 'route') || { sidecar: emptySidecar() }).sidecar.routeHash ||
|
|
1139
|
+
''
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function topLevelCodeVersion(frames) {
|
|
1143
|
+
const routeFrame = frames.find((frame) => frame.frameId === 'route')
|
|
1144
|
+
if (routeFrame && routeFrame.sidecar.codeVersion.length > 0) return routeFrame.sidecar.codeVersion
|
|
1145
|
+
const frameWithCodeVersion = frames.find((frame) => frame.sidecar.codeVersion.length > 0)
|
|
1146
|
+
return frameWithCodeVersion ? frameWithCodeVersion.sidecar.codeVersion : ''
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function collectAnchors(frames = readFrames()) {
|
|
1150
|
+
observationStats.anchorCollections += 1
|
|
1151
|
+
prunePaintTargets()
|
|
1152
|
+
if (hydrationSafeDomMaterializationEnabled) {
|
|
1153
|
+
materializeCodedTextAttributeCarriers(frames)
|
|
1154
|
+
}
|
|
1155
|
+
const codedTextAnchors = collectCodedTextAnchors(frames)
|
|
1156
|
+
const attributeElements = [...document.querySelectorAll('[data-ss-e],[data-ss-p]')]
|
|
1157
|
+
return {
|
|
1158
|
+
anchors: [...codedTextAnchors, ...collectAttributeAnchors(attributeElements)],
|
|
1159
|
+
markerCount: codedTextAnchors.length + attributeElements.length,
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function scheduleHydrationSafeDomMaterialization() {
|
|
1164
|
+
if (!documentActive || hydrationSafeDomMaterializationEnabled || hydrationSafeDomMaterializationScheduled) return
|
|
1165
|
+
hydrationSafeDomMaterializationScheduled = true
|
|
1166
|
+
const enableAfterHydrationTurn = () => {
|
|
1167
|
+
hydrationLoadListener = null
|
|
1168
|
+
hydrationTimer = setTimeout(() => {
|
|
1169
|
+
hydrationTimer = null
|
|
1170
|
+
if (!documentActive) return
|
|
1171
|
+
hydrationFrame = requestAnimationFrame(() => {
|
|
1172
|
+
hydrationFrame = null
|
|
1173
|
+
if (!documentActive) return
|
|
1174
|
+
hydrationFrame = requestAnimationFrame(() => {
|
|
1175
|
+
hydrationFrame = null
|
|
1176
|
+
if (!documentActive) return
|
|
1177
|
+
hydrationTimer = setTimeout(() => {
|
|
1178
|
+
hydrationTimer = null
|
|
1179
|
+
if (!documentActive) return
|
|
1180
|
+
hydrationSafeDomMaterializationEnabled = true
|
|
1181
|
+
scheduleReady('hydration-safe-dom:' + window.location.pathname)
|
|
1182
|
+
}, 0)
|
|
1183
|
+
})
|
|
1184
|
+
})
|
|
1185
|
+
}, domHydrationGraceMs())
|
|
1186
|
+
}
|
|
1187
|
+
if (document.readyState !== 'complete') {
|
|
1188
|
+
hydrationLoadListener = enableAfterHydrationTurn
|
|
1189
|
+
window.addEventListener('load', enableAfterHydrationTurn, { once: true })
|
|
1190
|
+
} else {
|
|
1191
|
+
enableAfterHydrationTurn()
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function domHydrationGraceMs() {
|
|
1196
|
+
const value = window.__ssPreviewProvenanceHydrationGraceMs
|
|
1197
|
+
return Number.isFinite(value) && value >= 0
|
|
1198
|
+
? value
|
|
1199
|
+
: DOM_HYDRATION_GRACE_MS
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// DOM-native provenance materialization: coded text is the identity carrier,
|
|
1203
|
+
// but overlays paint only real [data-ss-p] elements. Promote only when a
|
|
1204
|
+
// single sidecar-backed element marker owns the element's text surface.
|
|
1205
|
+
function materializeCodedTextAttributeCarriers(frames) {
|
|
1206
|
+
const root = document.body || document.documentElement
|
|
1207
|
+
if (!root) return false
|
|
1208
|
+
const eligibleMarkerIds = elementMarkerIdsBySidecar(frames)
|
|
1209
|
+
if (eligibleMarkerIds.size === 0) return false
|
|
1210
|
+
let materialized = false
|
|
1211
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
1212
|
+
let node = walker.nextNode()
|
|
1213
|
+
while (node) {
|
|
1214
|
+
if (node instanceof Text && node.data.includes(CODED_TEXT_START)) {
|
|
1215
|
+
materialized =
|
|
1216
|
+
materializeCodedTextNodeAttributeCarrier(node, eligibleMarkerIds) ||
|
|
1217
|
+
materialized
|
|
1218
|
+
}
|
|
1219
|
+
node = walker.nextNode()
|
|
1220
|
+
}
|
|
1221
|
+
return materialized
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function elementMarkerIdsBySidecar(frames) {
|
|
1225
|
+
const markerIds = new Set()
|
|
1226
|
+
for (const frame of frames) {
|
|
1227
|
+
for (const marker of frame.sidecar.markers) {
|
|
1228
|
+
if (marker.anchorKind === 'element') markerIds.add(marker.markerId)
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return markerIds
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function materializeCodedTextNodeAttributeCarrier(node, eligibleMarkerIds) {
|
|
1235
|
+
let element = node.parentElement
|
|
1236
|
+
while (element) {
|
|
1237
|
+
if (element.closest('script,style,template,noscript,textarea,title,option')) return false
|
|
1238
|
+
if (element === document.body || element === document.documentElement) return false
|
|
1239
|
+
const decoded = decodeCodedText(element.textContent || '')
|
|
1240
|
+
const markerId = singleEligibleDecodedMarkerId(decoded, eligibleMarkerIds)
|
|
1241
|
+
if (markerId !== null) {
|
|
1242
|
+
return writeCodedTextAttributeCarrier(element, markerId)
|
|
1243
|
+
}
|
|
1244
|
+
if (decoded.segments.length > 0) return false
|
|
1245
|
+
element = element.parentElement
|
|
1246
|
+
}
|
|
1247
|
+
return false
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function singleEligibleDecodedMarkerId(decoded, eligibleMarkerIds) {
|
|
1251
|
+
if (decoded.segments.length !== 1) return null
|
|
1252
|
+
const markerId = decoded.segments[0] && decoded.segments[0].markerId || ''
|
|
1253
|
+
return eligibleMarkerIds.has(markerId) ? markerId : null
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function writeCodedTextAttributeCarrier(element, markerId) {
|
|
1257
|
+
// Autonomous values use the memory paint registry. React owns this ordinary
|
|
1258
|
+
// element; leaving synthetic attrs behind would outlive the coded text.
|
|
1259
|
+
if (decodeCodedText(element.textContent || '').segments.some(segment => segment.carrierId.startsWith('ssr1.'))) return false
|
|
1260
|
+
if (element.hasAttribute('data-ss-e') || element.querySelector('[data-ss-e]') !== null) return false
|
|
1261
|
+
const existing = normalizedCarrierMarkerId(element.getAttribute('data-ss-p'))
|
|
1262
|
+
if (existing === markerId) return false
|
|
1263
|
+
if (element.querySelector('[data-ss-p]') !== null) return false
|
|
1264
|
+
if (existing !== null && !isStaleCodedTextAttributeCarrier(element, markerId)) return false
|
|
1265
|
+
element.setAttribute('data-ss-p', markerId)
|
|
1266
|
+
return true
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function isStaleCodedTextAttributeCarrier(element, markerId) {
|
|
1270
|
+
const decoded = decodeCodedText(element.textContent || '')
|
|
1271
|
+
return decoded.segments.length === 1 &&
|
|
1272
|
+
((decoded.segments[0] && decoded.segments[0].markerId) || '') === markerId
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function collectAttributeAnchors(elements) {
|
|
1276
|
+
return elements.flatMap((element) => {
|
|
1277
|
+
const rect = element.getBoundingClientRect()
|
|
1278
|
+
const elementMarkerId = element.getAttribute('data-ss-e')
|
|
1279
|
+
const markerId = normalizedCarrierMarkerId(elementMarkerId !== null
|
|
1280
|
+
? elementMarkerId
|
|
1281
|
+
: element.getAttribute('data-ss-p') || '')
|
|
1282
|
+
if (elementMarkerId === null && isSupersededCodedTextAttributeCarrier(element, markerId)) {
|
|
1283
|
+
return []
|
|
1284
|
+
}
|
|
1285
|
+
return [{
|
|
1286
|
+
markerId,
|
|
1287
|
+
paintId: ensurePaintId(element, markerId),
|
|
1288
|
+
tagName: element.tagName,
|
|
1289
|
+
text: elementMarkerId !== null
|
|
1290
|
+
? ''
|
|
1291
|
+
: decodeCodedText(element.textContent || '').text,
|
|
1292
|
+
connected: element.isConnected,
|
|
1293
|
+
carrier: 'attribute',
|
|
1294
|
+
documentRect: {
|
|
1295
|
+
x: rect.left + window.scrollX,
|
|
1296
|
+
y: rect.top + window.scrollY,
|
|
1297
|
+
width: rect.width,
|
|
1298
|
+
height: rect.height,
|
|
1299
|
+
},
|
|
1300
|
+
}]
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function isSupersededCodedTextAttributeCarrier(element, markerId) {
|
|
1305
|
+
const decoded = decodeCodedText(element.textContent || '')
|
|
1306
|
+
if (decoded.segments.length !== 1) return false
|
|
1307
|
+
const decodedMarkerId = (decoded.segments[0] && decoded.segments[0].markerId) || ''
|
|
1308
|
+
return decodedMarkerId.length > 0 && decodedMarkerId !== markerId
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function canStampPaintId(element) {
|
|
1312
|
+
return hydrationSafeDomMaterializationEnabled &&
|
|
1313
|
+
(element.hasAttribute('data-ss-e') || element.hasAttribute('data-ss-p'))
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function ensurePaintId(element, markerId) {
|
|
1317
|
+
if (typeof markerId !== 'string' || markerId.length === 0) return undefined
|
|
1318
|
+
const knownPaintId = paintIdByElement.get(element)
|
|
1319
|
+
if (typeof knownPaintId === 'string') {
|
|
1320
|
+
registerPaintTarget(knownPaintId, markerId, element)
|
|
1321
|
+
if (
|
|
1322
|
+
canStampPaintId(element) &&
|
|
1323
|
+
element.getAttribute(PAINT_ID_ATTRIBUTE) !== knownPaintId
|
|
1324
|
+
) {
|
|
1325
|
+
element.setAttribute(PAINT_ID_ATTRIBUTE, knownPaintId)
|
|
1326
|
+
}
|
|
1327
|
+
return knownPaintId
|
|
1328
|
+
}
|
|
1329
|
+
const paintId = 'paint:' + nextPaintId
|
|
1330
|
+
nextPaintId += 1
|
|
1331
|
+
paintIdByElement.set(element, paintId)
|
|
1332
|
+
registerPaintTarget(paintId, markerId, element)
|
|
1333
|
+
if (canStampPaintId(element)) {
|
|
1334
|
+
element.setAttribute(PAINT_ID_ATTRIBUTE, paintId)
|
|
1335
|
+
}
|
|
1336
|
+
return paintId
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function registerPaintTarget(paintId, markerId, element) {
|
|
1340
|
+
paintTargetById.set(paintId, {
|
|
1341
|
+
markerId,
|
|
1342
|
+
elementRef: typeof WeakRef === 'function' ? new WeakRef(element) : element,
|
|
1343
|
+
})
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function prunePaintTargets() {
|
|
1347
|
+
for (const [paintId, entry] of paintTargetById) {
|
|
1348
|
+
const element = entry.elementRef && typeof entry.elementRef.deref === 'function'
|
|
1349
|
+
? entry.elementRef.deref()
|
|
1350
|
+
: entry.elementRef
|
|
1351
|
+
if (!(element instanceof Element) || !element.isConnected) {
|
|
1352
|
+
paintTargetById.delete(paintId)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function resolvePaintTarget(paintId) {
|
|
1358
|
+
const entry = paintTargetById.get(paintId)
|
|
1359
|
+
if (!entry) return null
|
|
1360
|
+
const element = entry.elementRef && typeof entry.elementRef.deref === 'function'
|
|
1361
|
+
? entry.elementRef.deref()
|
|
1362
|
+
: entry.elementRef
|
|
1363
|
+
if (!(element instanceof Element) || !element.isConnected) {
|
|
1364
|
+
paintTargetById.delete(paintId)
|
|
1365
|
+
return null
|
|
1366
|
+
}
|
|
1367
|
+
return { element, markerId: entry.markerId }
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function teardownPaintIdentityStamps() {
|
|
1371
|
+
documentActive = false
|
|
1372
|
+
cancelEpoch += 1
|
|
1373
|
+
pendingNavigationEpoch = null
|
|
1374
|
+
cancelReadyObservation()
|
|
1375
|
+
if (viewportFrame !== null) cancelAnimationFrame(viewportFrame)
|
|
1376
|
+
viewportFrame = null
|
|
1377
|
+
if (hydrationFrame !== null) cancelAnimationFrame(hydrationFrame)
|
|
1378
|
+
hydrationFrame = null
|
|
1379
|
+
if (hydrationTimer !== null) clearTimeout(hydrationTimer)
|
|
1380
|
+
hydrationTimer = null
|
|
1381
|
+
if (hydrationLoadListener !== null) window.removeEventListener('load', hydrationLoadListener)
|
|
1382
|
+
hydrationLoadListener = null
|
|
1383
|
+
hydrationSafeDomMaterializationScheduled = false
|
|
1384
|
+
if (provenanceObserver) provenanceObserver.disconnect()
|
|
1385
|
+
for (const element of document.querySelectorAll('[' + PAINT_ID_ATTRIBUTE + ']')) {
|
|
1386
|
+
element.removeAttribute(PAINT_ID_ATTRIBUTE)
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function restorePaintIdentityStampsFromBackForwardCache(event) {
|
|
1391
|
+
if (!event.persisted || !provenanceObserver) return
|
|
1392
|
+
documentActive = true
|
|
1393
|
+
readCarriersDirty = true
|
|
1394
|
+
publisherSnapshotDirty = true
|
|
1395
|
+
provenanceObserver.observe(document.documentElement, PROVENANCE_OBSERVER_OPTIONS)
|
|
1396
|
+
scheduleHydrationSafeDomMaterialization()
|
|
1397
|
+
scheduleReady('pageshow:' + window.location.pathname)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function collectCodedTextAnchors(frames = readFrames()) {
|
|
1401
|
+
observationStats.codedTextScans += 1
|
|
1402
|
+
const root = document.body || document.documentElement
|
|
1403
|
+
if (!root) return []
|
|
1404
|
+
const anchors = []
|
|
1405
|
+
const seenElements = new WeakMap()
|
|
1406
|
+
const eligibleMarkerIds = elementMarkerIdsBySidecar(frames)
|
|
1407
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
1408
|
+
let node = walker.nextNode()
|
|
1409
|
+
while (node) {
|
|
1410
|
+
if (node instanceof Text && node.data.includes(CODED_TEXT_START)) {
|
|
1411
|
+
const directAnchors = anchorsForCodedTextNode(node, eligibleMarkerIds)
|
|
1412
|
+
if (directAnchors.length > 0) {
|
|
1413
|
+
for (const anchor of directAnchors) {
|
|
1414
|
+
const seen = seenElements.get(anchor.element) || new Set()
|
|
1415
|
+
if (seen.has(anchor.markerId)) continue
|
|
1416
|
+
seen.add(anchor.markerId)
|
|
1417
|
+
seenElements.set(anchor.element, seen)
|
|
1418
|
+
const { element, ...publicAnchor } = anchor
|
|
1419
|
+
anchors.push(publicAnchor)
|
|
1420
|
+
}
|
|
1421
|
+
} else {
|
|
1422
|
+
for (const anchor of anchorsForSpanningCodedTextNode(node, eligibleMarkerIds)) {
|
|
1423
|
+
const seen = seenElements.get(anchor.element) || new Set()
|
|
1424
|
+
if (seen.has(anchor.markerId)) continue
|
|
1425
|
+
seen.add(anchor.markerId)
|
|
1426
|
+
seenElements.set(anchor.element, seen)
|
|
1427
|
+
const { element, ...publicAnchor } = anchor
|
|
1428
|
+
anchors.push(publicAnchor)
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
node = walker.nextNode()
|
|
1433
|
+
}
|
|
1434
|
+
return anchors
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function currentDocumentViewport() {
|
|
1438
|
+
return {
|
|
1439
|
+
x: window.scrollX,
|
|
1440
|
+
y: window.scrollY,
|
|
1441
|
+
width: window.innerWidth,
|
|
1442
|
+
height: window.innerHeight,
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function anchorsForCodedTextNode(node, eligibleMarkerIds) {
|
|
1447
|
+
const parent = node.parentElement
|
|
1448
|
+
if (!parent || parent.closest('script,style,template,noscript,textarea,title,option')) return []
|
|
1449
|
+
const decoded = decodeCodedText(node.data)
|
|
1450
|
+
const paintId = codedTextPaintId(parent, decoded, eligibleMarkerIds)
|
|
1451
|
+
const anchors = decoded.segments.map((segment) => {
|
|
1452
|
+
// A paintId already identifies this exact, unambiguous element. Publish
|
|
1453
|
+
// the same box the painter measures; invisible metadata can distort
|
|
1454
|
+
// browser Range geometry even when the element renders correctly.
|
|
1455
|
+
const rect = paintId
|
|
1456
|
+
? parent.getBoundingClientRect()
|
|
1457
|
+
: rectForTextRange(node, segment.textStart, segment.textEnd) || parent.getBoundingClientRect()
|
|
1458
|
+
return {
|
|
1459
|
+
markerId: segment.markerId,
|
|
1460
|
+
element: parent,
|
|
1461
|
+
...(decoded.segments.length === 1 && paintId ? { paintId } : {}),
|
|
1462
|
+
tagName: parent.tagName,
|
|
1463
|
+
text: segment.text,
|
|
1464
|
+
connected: node.isConnected && parent.isConnected,
|
|
1465
|
+
carrier: 'coded-text',
|
|
1466
|
+
documentRect: {
|
|
1467
|
+
x: rect.left + window.scrollX,
|
|
1468
|
+
y: rect.top + window.scrollY,
|
|
1469
|
+
width: rect.width,
|
|
1470
|
+
height: rect.height,
|
|
1471
|
+
},
|
|
1472
|
+
}
|
|
1473
|
+
})
|
|
1474
|
+
return anchors.filter((anchor) =>
|
|
1475
|
+
!hasMatchingAttributeCarrier(parent, anchor.markerId)
|
|
1476
|
+
)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function anchorsForSpanningCodedTextNode(node, eligibleMarkerIds) {
|
|
1480
|
+
let element = node.parentElement
|
|
1481
|
+
while (element) {
|
|
1482
|
+
if (element.closest('script,style,template,noscript,textarea,title,option')) return []
|
|
1483
|
+
const decoded = decodeCodedText(element.textContent || '')
|
|
1484
|
+
if (decoded.segments.length > 0) {
|
|
1485
|
+
const segments = decoded.segments.filter((segment) =>
|
|
1486
|
+
!hasMatchingAttributeCarrier(element, segment.markerId)
|
|
1487
|
+
)
|
|
1488
|
+
if (segments.length === 0) return []
|
|
1489
|
+
const rect = element.getBoundingClientRect()
|
|
1490
|
+
const paintId = codedTextPaintId(element, decoded, eligibleMarkerIds)
|
|
1491
|
+
return segments.map((segment) => ({
|
|
1492
|
+
markerId: segment.markerId,
|
|
1493
|
+
element,
|
|
1494
|
+
...(segments.length === 1 && paintId ? { paintId } : {}),
|
|
1495
|
+
tagName: element.tagName,
|
|
1496
|
+
text: segment.text,
|
|
1497
|
+
connected: node.isConnected && element.isConnected,
|
|
1498
|
+
carrier: 'coded-text',
|
|
1499
|
+
documentRect: {
|
|
1500
|
+
x: rect.left + window.scrollX,
|
|
1501
|
+
y: rect.top + window.scrollY,
|
|
1502
|
+
width: rect.width,
|
|
1503
|
+
height: rect.height,
|
|
1504
|
+
},
|
|
1505
|
+
}))
|
|
1506
|
+
}
|
|
1507
|
+
element = element.parentElement
|
|
1508
|
+
}
|
|
1509
|
+
return []
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function hasMatchingAttributeCarrier(element, markerId) {
|
|
1513
|
+
return normalizedCarrierMarkerId(element.getAttribute('data-ss-p')) === markerId
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function codedTextPaintId(element, decoded, eligibleMarkerIds) {
|
|
1517
|
+
if (decoded.segments.length !== 1) return undefined
|
|
1518
|
+
const markerId = (decoded.segments[0] && decoded.segments[0].markerId) || ''
|
|
1519
|
+
if (!eligibleMarkerIds.has(markerId)) return undefined
|
|
1520
|
+
// A text node is not an element boundary. Inline rich text can put
|
|
1521
|
+
// independently owned nodes on both sides of a link in the same <p>.
|
|
1522
|
+
// Painting that parent for both would reuse its paint ID, overwrite the
|
|
1523
|
+
// registry owner and prevent the entire overlay map from being confirmed.
|
|
1524
|
+
// Keep their text provenance, but only promote an unambiguous element.
|
|
1525
|
+
if (decodeCodedText(element.textContent || '').segments.some((segment) =>
|
|
1526
|
+
segment.markerId !== markerId
|
|
1527
|
+
)) return undefined
|
|
1528
|
+
if (element.hasAttribute('data-ss-e') || element.querySelector('[data-ss-e]') !== null) {
|
|
1529
|
+
return undefined
|
|
1530
|
+
}
|
|
1531
|
+
if (element.querySelector('[data-ss-p]') !== null) return undefined
|
|
1532
|
+
const existing = normalizedCarrierMarkerId(element.getAttribute('data-ss-p'))
|
|
1533
|
+
if (
|
|
1534
|
+
existing !== null &&
|
|
1535
|
+
existing !== markerId &&
|
|
1536
|
+
!isStaleCodedTextAttributeCarrier(element, markerId)
|
|
1537
|
+
) {
|
|
1538
|
+
return undefined
|
|
1539
|
+
}
|
|
1540
|
+
return ensurePaintId(element, markerId)
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function rectForTextRange(node, start, end) {
|
|
1544
|
+
if (start >= end || typeof document.createRange !== 'function') return null
|
|
1545
|
+
const range = document.createRange()
|
|
1546
|
+
try {
|
|
1547
|
+
range.setStart(node, start)
|
|
1548
|
+
range.setEnd(node, end)
|
|
1549
|
+
return typeof range.getBoundingClientRect === 'function'
|
|
1550
|
+
? range.getBoundingClientRect()
|
|
1551
|
+
: null
|
|
1552
|
+
} catch {
|
|
1553
|
+
return null
|
|
1554
|
+
} finally {
|
|
1555
|
+
range.detach()
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function buildMap(pathname, domRouteHash, frame, anchors, invalidMarkerIds, duplicateSidecarMarkerIds, phase, pending, lifecycle) {
|
|
1560
|
+
const sidecarMarkers = new Map(frame.sidecar.markers.map((marker) => [marker.markerId, marker]))
|
|
1561
|
+
const sidecarMarkerIds = new Set(sidecarMarkers.keys())
|
|
1562
|
+
const rawAnchors = anchors.filter((anchor) => anchor.markerId.length > 0)
|
|
1563
|
+
const counts = new Map()
|
|
1564
|
+
for (const anchor of rawAnchors) counts.set(anchor.markerId, (counts.get(anchor.markerId) || 0) + 1)
|
|
1565
|
+
|
|
1566
|
+
const observedMarkerIds = new Set(rawAnchors.map((anchor) => anchor.markerId))
|
|
1567
|
+
const localInvalidMarkerIds = rawAnchors
|
|
1568
|
+
.filter((anchor) => !sidecarMarkerIds.has(anchor.markerId))
|
|
1569
|
+
.map((anchor) => anchor.markerId)
|
|
1570
|
+
const unpaintedMarkerIds = [...sidecarMarkerIds].filter((markerId) =>
|
|
1571
|
+
!observedMarkerIds.has(markerId)
|
|
1572
|
+
)
|
|
1573
|
+
const missingMarkerIds = [...sidecarMarkerIds].filter((markerId) =>
|
|
1574
|
+
!observedMarkerIds.has(markerId) && isRequiredMarker(markerId, sidecarMarkers)
|
|
1575
|
+
)
|
|
1576
|
+
const ambiguousMarkerIds = [...counts.entries()]
|
|
1577
|
+
.filter(([markerId, count]) => count > 1 && !allowsMultipleAnchors(markerId, sidecarMarkers))
|
|
1578
|
+
.map(([markerId]) => markerId)
|
|
1579
|
+
const contentMismatchMarkerIds = rawAnchors
|
|
1580
|
+
.filter((anchor) => {
|
|
1581
|
+
const marker = sidecarMarkers.get(anchor.markerId)
|
|
1582
|
+
const renderedTextFingerprint = fingerprintRenderedText(anchor.text)
|
|
1583
|
+
return marker &&
|
|
1584
|
+
marker.renderedTextFingerprint !== undefined &&
|
|
1585
|
+
marker.renderedTextFingerprint !== renderedTextFingerprint
|
|
1586
|
+
})
|
|
1587
|
+
.map((anchor) => anchor.markerId)
|
|
1588
|
+
const contentMismatchMarkerIdSet = new Set(contentMismatchMarkerIds)
|
|
1589
|
+
const fatalContentMismatchMarkerIds = [...contentMismatchMarkerIdSet]
|
|
1590
|
+
.filter((markerId) => isRequiredMarker(markerId, sidecarMarkers))
|
|
1591
|
+
const mergedInvalidMarkerIds = [...new Set([...localInvalidMarkerIds, ...invalidMarkerIds])]
|
|
1592
|
+
const routeReady = frame.frameId !== 'route' || domRouteHash === frame.sidecar.routeHash
|
|
1593
|
+
const sidecarVariantId = normalizeVariantId(frame.sidecar.variantId)
|
|
1594
|
+
|
|
1595
|
+
const resolvedAnchors = rawAnchors.filter((anchor) =>
|
|
1596
|
+
anchor.connected &&
|
|
1597
|
+
sidecarMarkerIds.has(anchor.markerId) &&
|
|
1598
|
+
((counts.get(anchor.markerId) || 0) === 1 || allowsMultipleAnchors(anchor.markerId, sidecarMarkers)) &&
|
|
1599
|
+
!contentMismatchMarkerIdSet.has(anchor.markerId)
|
|
1600
|
+
)
|
|
1601
|
+
const codedTextMarkerIds = new Set(
|
|
1602
|
+
resolvedAnchors
|
|
1603
|
+
.filter((anchor) => anchor.carrier === 'coded-text')
|
|
1604
|
+
.map((anchor) => anchor.markerId)
|
|
1605
|
+
)
|
|
1606
|
+
const domMarkerIds = new Set(
|
|
1607
|
+
resolvedAnchors
|
|
1608
|
+
.filter((anchor) => typeof anchor.paintId === 'string' && anchor.paintId.length > 0)
|
|
1609
|
+
.map((anchor) => anchor.markerId)
|
|
1610
|
+
)
|
|
1611
|
+
const dependencySet = frame.sidecar.dependencySet
|
|
1612
|
+
const dependencySetMatches = !dependencySet || (
|
|
1613
|
+
matchesCurrentReadContext(dependencySet) &&
|
|
1614
|
+
dependencySet.sessionId === lifecycle.sessionId &&
|
|
1615
|
+
dependencySet.routeHash === frame.sidecar.routeHash &&
|
|
1616
|
+
dependencySet.codeVersion === frame.sidecar.codeVersion &&
|
|
1617
|
+
dependencySet.frameId === frame.frameId &&
|
|
1618
|
+
dependencySet.renderId === frame.sidecar.renderId &&
|
|
1619
|
+
dependencySet.generation === frame.sidecar.generation &&
|
|
1620
|
+
normalizeVariantId(dependencySet.variantId) === sidecarVariantId
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
return {
|
|
1624
|
+
sessionId: lifecycle.sessionId,
|
|
1625
|
+
generation: lifecycle.generation,
|
|
1626
|
+
sourceOfTruth: lifecycle.sourceOfTruth,
|
|
1627
|
+
...(lifecycle.commandId ? { commandId: lifecycle.commandId } : {}),
|
|
1628
|
+
pathname,
|
|
1629
|
+
routeHash: frame.sidecar.routeHash,
|
|
1630
|
+
variantId: sidecarVariantId,
|
|
1631
|
+
...(frame.sidecar.previewScope ? { previewScope: frame.sidecar.previewScope } : {}),
|
|
1632
|
+
codeVersion: frame.sidecar.codeVersion,
|
|
1633
|
+
renderId: frame.sidecar.renderId,
|
|
1634
|
+
...(frame.sidecar.renderNonce ? { renderNonce: frame.sidecar.renderNonce } : {}),
|
|
1635
|
+
...(frame.sidecar.emissionRevision > 0
|
|
1636
|
+
? { emissionRevision: frame.sidecar.emissionRevision }
|
|
1637
|
+
: {}),
|
|
1638
|
+
...(dependencySet && dependencySetMatches ? { dependencySet } : {}),
|
|
1639
|
+
frameId: frame.frameId,
|
|
1640
|
+
phase,
|
|
1641
|
+
pending,
|
|
1642
|
+
anchors: resolvedAnchors,
|
|
1643
|
+
editLocators: frame.sidecar.markers.flatMap((marker) =>
|
|
1644
|
+
marker.editLocator
|
|
1645
|
+
? [{
|
|
1646
|
+
markerId: marker.markerId,
|
|
1647
|
+
locator: marker.editLocator,
|
|
1648
|
+
modality: marker.modality,
|
|
1649
|
+
...(marker.presence ? { presence: marker.presence } : {}),
|
|
1650
|
+
}]
|
|
1651
|
+
: []
|
|
1652
|
+
),
|
|
1653
|
+
sidecarMarkerIds: [...sidecarMarkerIds],
|
|
1654
|
+
invalidMarkerIds: mergedInvalidMarkerIds,
|
|
1655
|
+
unpaintedMarkerIds,
|
|
1656
|
+
missingMarkerIds,
|
|
1657
|
+
ambiguousMarkerIds,
|
|
1658
|
+
contentMismatchMarkerIds: [...contentMismatchMarkerIdSet],
|
|
1659
|
+
fatalContentMismatchMarkerIds,
|
|
1660
|
+
paintDiagnostics: {
|
|
1661
|
+
codedTextOnlyNotPaintable: [...sidecarMarkerIds].filter((markerId) =>
|
|
1662
|
+
codedTextMarkerIds.has(markerId) && !domMarkerIds.has(markerId)
|
|
1663
|
+
),
|
|
1664
|
+
domTargetPainted: [...sidecarMarkerIds].filter((markerId) =>
|
|
1665
|
+
domMarkerIds.has(markerId)
|
|
1666
|
+
),
|
|
1667
|
+
},
|
|
1668
|
+
valid:
|
|
1669
|
+
!pending &&
|
|
1670
|
+
lifecycle.sessionId.length > 0 &&
|
|
1671
|
+
Number.isFinite(lifecycle.generation) &&
|
|
1672
|
+
lifecycle.generation > 0 &&
|
|
1673
|
+
lifecycle.codeVersion.length > 0 &&
|
|
1674
|
+
frame.sidecar.codeVersion === lifecycle.codeVersion &&
|
|
1675
|
+
(frame.frameId !== 'route' || sidecarVariantId === lifecycle.variantId) &&
|
|
1676
|
+
routeReady &&
|
|
1677
|
+
frame.sidecar.routeHash.length > 0 &&
|
|
1678
|
+
frame.sidecar.codeVersion.length > 0 &&
|
|
1679
|
+
frame.sidecar.renderId.length > 0 &&
|
|
1680
|
+
dependencySetMatches &&
|
|
1681
|
+
mergedInvalidMarkerIds.length === 0 &&
|
|
1682
|
+
missingMarkerIds.length === 0 &&
|
|
1683
|
+
fatalContentMismatchMarkerIds.length === 0 &&
|
|
1684
|
+
duplicateSidecarMarkerIds.length === 0,
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function collectObservation() {
|
|
1689
|
+
reconcileProvenancePublishers()
|
|
1690
|
+
const frames = readFrames()
|
|
1691
|
+
return {
|
|
1692
|
+
frames,
|
|
1693
|
+
...collectAnchors(frames),
|
|
1694
|
+
pathname: window.location.pathname,
|
|
1695
|
+
domRouteHash: routeFrameHash(),
|
|
1696
|
+
viewport: currentDocumentViewport(),
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function buildMultiFrameMaps(phase, pending, identity, observation) {
|
|
1701
|
+
const { frames, anchors, pathname, domRouteHash, viewport } = observation
|
|
1702
|
+
const lifecycle = {
|
|
1703
|
+
sessionId: sessionId(),
|
|
1704
|
+
generation: identity.generation,
|
|
1705
|
+
sourceOfTruth: identity.sourceOfTruth,
|
|
1706
|
+
commandId: identity.commandId,
|
|
1707
|
+
routeHash: topLevelRouteHash(frames, domRouteHash),
|
|
1708
|
+
variantId: activeVariantId(),
|
|
1709
|
+
codeVersion: topLevelCodeVersion(frames),
|
|
1710
|
+
}
|
|
1711
|
+
const ownerByMarkerId = new Map()
|
|
1712
|
+
const duplicateSidecarMarkerIds = new Set()
|
|
1713
|
+
for (const frame of frames) {
|
|
1714
|
+
for (const marker of frame.sidecar.markers) {
|
|
1715
|
+
const existing = ownerByMarkerId.get(marker.markerId)
|
|
1716
|
+
if (existing !== undefined && existing !== frame.frameId) duplicateSidecarMarkerIds.add(marker.markerId)
|
|
1717
|
+
ownerByMarkerId.set(marker.markerId, frame.frameId)
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const invalidMarkerIds = [...new Set(
|
|
1722
|
+
anchors
|
|
1723
|
+
.filter((anchor) => anchor.markerId.length > 0 && !ownerByMarkerId.has(anchor.markerId))
|
|
1724
|
+
.map((anchor) => anchor.markerId)
|
|
1725
|
+
)]
|
|
1726
|
+
const invalidMarkerIdSet = new Set(invalidMarkerIds)
|
|
1727
|
+
const anchorsByFrame = new Map(frames.map((frame) => [frame.frameId, []]))
|
|
1728
|
+
for (const anchor of anchors) {
|
|
1729
|
+
if (invalidMarkerIdSet.has(anchor.markerId)) {
|
|
1730
|
+
// Unknown anchors must still invalidate every frame. Preserve their
|
|
1731
|
+
// original relative order in diagnostics without rescanning all anchors.
|
|
1732
|
+
for (const frameAnchors of anchorsByFrame.values()) frameAnchors.push(anchor)
|
|
1733
|
+
} else {
|
|
1734
|
+
anchorsByFrame.get(ownerByMarkerId.get(anchor.markerId))?.push(anchor)
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const duplicateIds = [...duplicateSidecarMarkerIds]
|
|
1738
|
+
const maps = frames.map((frame) => buildMap(
|
|
1739
|
+
pathname,
|
|
1740
|
+
domRouteHash,
|
|
1741
|
+
frame,
|
|
1742
|
+
anchorsByFrame.get(frame.frameId),
|
|
1743
|
+
invalidMarkerIds,
|
|
1744
|
+
duplicateIds,
|
|
1745
|
+
phase,
|
|
1746
|
+
pending === true,
|
|
1747
|
+
lifecycle,
|
|
1748
|
+
))
|
|
1749
|
+
const routeMap = maps.find((map) => map.frameId === 'route')
|
|
1750
|
+
const lifecycleValid =
|
|
1751
|
+
pending !== true &&
|
|
1752
|
+
lifecycle.sessionId.length > 0 &&
|
|
1753
|
+
Number.isFinite(lifecycle.generation) &&
|
|
1754
|
+
lifecycle.generation > 0 &&
|
|
1755
|
+
lifecycle.routeHash.length > 0 &&
|
|
1756
|
+
lifecycle.codeVersion.length > 0 &&
|
|
1757
|
+
routeMap !== undefined &&
|
|
1758
|
+
routeMap.routeHash === lifecycle.routeHash &&
|
|
1759
|
+
routeMap.variantId === lifecycle.variantId &&
|
|
1760
|
+
maps.every((map) => map.codeVersion === lifecycle.codeVersion)
|
|
1761
|
+
|
|
1762
|
+
return {
|
|
1763
|
+
type: 'storystick.preview.multi-frame-anchor-maps',
|
|
1764
|
+
version: 1,
|
|
1765
|
+
sessionId: lifecycle.sessionId,
|
|
1766
|
+
generation: lifecycle.generation,
|
|
1767
|
+
sourceOfTruth: lifecycle.sourceOfTruth,
|
|
1768
|
+
...(lifecycle.commandId ? { commandId: lifecycle.commandId } : {}),
|
|
1769
|
+
phase,
|
|
1770
|
+
pending: pending === true,
|
|
1771
|
+
pathname,
|
|
1772
|
+
routeHash: lifecycle.routeHash,
|
|
1773
|
+
variantId: lifecycle.variantId,
|
|
1774
|
+
...(routeMap && routeMap.previewScope ? { previewScope: routeMap.previewScope } : {}),
|
|
1775
|
+
primaryPage: selectPrimaryPage(currentPrimaryPages(), pathname, runtimeBasePath),
|
|
1776
|
+
codeVersion: lifecycle.codeVersion,
|
|
1777
|
+
viewport,
|
|
1778
|
+
maps,
|
|
1779
|
+
invalidMarkerIds,
|
|
1780
|
+
duplicateSidecarMarkerIds: duplicateIds,
|
|
1781
|
+
valid: lifecycleValid && duplicateIds.length === 0 && invalidMarkerIds.length === 0 && maps.length > 0 && maps.every((map) => map.valid),
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function logEvent(name) {
|
|
1786
|
+
window.__ssPreviewProvenanceEventLog = [...(window.__ssPreviewProvenanceEventLog || []), name].slice(-40)
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function logLifecycle(result, observation) {
|
|
1790
|
+
window.__ssPreviewProvenanceLifecycleLog = [...(window.__ssPreviewProvenanceLifecycleLog || []), {
|
|
1791
|
+
phase: result.phase,
|
|
1792
|
+
pending: result.pending,
|
|
1793
|
+
valid: result.valid,
|
|
1794
|
+
sessionId: result.sessionId,
|
|
1795
|
+
generation: result.generation,
|
|
1796
|
+
sourceOfTruth: result.sourceOfTruth,
|
|
1797
|
+
commandId: result.commandId || null,
|
|
1798
|
+
pathname: result.pathname,
|
|
1799
|
+
routeHash: result.routeHash,
|
|
1800
|
+
variantId: result.variantId,
|
|
1801
|
+
previewScope: result.previewScope || null,
|
|
1802
|
+
codeVersion: result.codeVersion,
|
|
1803
|
+
domRouteHash: observation.domRouteHash,
|
|
1804
|
+
routeMapRouteHash: (result.maps.find((map) => map.frameId === 'route') || {}).routeHash || '',
|
|
1805
|
+
routeMapValid: (result.maps.find((map) => map.frameId === 'route') || {}).valid === true,
|
|
1806
|
+
anyFrameValid: result.maps.some((map) => map.valid === true),
|
|
1807
|
+
markerCount: observation.markerCount,
|
|
1808
|
+
sidecarMarkerCount: totalSidecarMarkers(observation.frames),
|
|
1809
|
+
}].slice(-80)
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function publish(phase, pending, identity = currentRenderIdentity, observation = null, epoch = cancelEpoch) {
|
|
1813
|
+
if (!documentActive || epoch !== cancelEpoch) return
|
|
1814
|
+
observation = observation || collectObservation()
|
|
1815
|
+
if (!documentActive || epoch !== cancelEpoch) return
|
|
1816
|
+
observationStats.publications += 1
|
|
1817
|
+
logEvent(phase)
|
|
1818
|
+
const result = buildMultiFrameMaps(phase, pending, identity, observation)
|
|
1819
|
+
logLifecycle(result, observation)
|
|
1820
|
+
window.__ssMultiFrameAnchorMaps = result
|
|
1821
|
+
window.__ssAnchorMap = result.maps.find((map) => map.frameId === 'route') || result.maps[0]
|
|
1822
|
+
window.dispatchEvent(new CustomEvent(LOCAL_ANCHOR_MAP_EVENT, { detail: result }))
|
|
1823
|
+
if (documentActive && epoch === cancelEpoch) publishToStudio(result)
|
|
1824
|
+
return result
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function publishToStudio(anchorMap) {
|
|
1828
|
+
if (!window.parent || window.parent === window) return
|
|
1829
|
+
if (typeof window.parent.postMessage !== 'function') return
|
|
1830
|
+
window.parent.postMessage({
|
|
1831
|
+
type: 'storystick.preview.provenance-anchor-map',
|
|
1832
|
+
timestamp: Date.now(),
|
|
1833
|
+
anchorMap,
|
|
1834
|
+
}, studioTargetOrigin())
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function publishViewportAfterScroll() {
|
|
1838
|
+
if (!documentActive || document.visibilityState === 'hidden' || viewportFrame !== null) return
|
|
1839
|
+
viewportFrame = requestAnimationFrame(() => {
|
|
1840
|
+
viewportFrame = null
|
|
1841
|
+
if (!documentActive || document.visibilityState === 'hidden') return
|
|
1842
|
+
const current = window.__ssMultiFrameAnchorMaps
|
|
1843
|
+
if (!current || typeof current !== 'object') return
|
|
1844
|
+
const next = {
|
|
1845
|
+
...current,
|
|
1846
|
+
viewport: currentDocumentViewport(),
|
|
1847
|
+
}
|
|
1848
|
+
window.__ssMultiFrameAnchorMaps = next
|
|
1849
|
+
const routeFrame = current.maps.find((map) => map.frameId === 'route')
|
|
1850
|
+
if (!routeFrame) return
|
|
1851
|
+
const message = {
|
|
1852
|
+
type: 'storystick.preview.provenance-viewport',
|
|
1853
|
+
timestamp: Date.now(),
|
|
1854
|
+
sessionId: current.sessionId,
|
|
1855
|
+
generation: current.generation,
|
|
1856
|
+
sourceOfTruth: current.sourceOfTruth,
|
|
1857
|
+
...(current.commandId ? { commandId: current.commandId } : {}),
|
|
1858
|
+
pathname: current.pathname,
|
|
1859
|
+
routeHash: current.routeHash,
|
|
1860
|
+
variantId: current.variantId || null,
|
|
1861
|
+
codeVersion: current.codeVersion,
|
|
1862
|
+
renderId: routeFrame.renderId,
|
|
1863
|
+
viewport: next.viewport,
|
|
1864
|
+
}
|
|
1865
|
+
const previousStats = window.__ssPreviewProvenanceViewportStats || {
|
|
1866
|
+
publications: 0,
|
|
1867
|
+
maxPayloadBytes: 0,
|
|
1868
|
+
}
|
|
1869
|
+
const payloadBytes = JSON.stringify(message).length
|
|
1870
|
+
window.__ssPreviewProvenanceViewportStats = {
|
|
1871
|
+
publications: previousStats.publications + 1,
|
|
1872
|
+
lastPayloadBytes: payloadBytes,
|
|
1873
|
+
maxPayloadBytes: Math.max(previousStats.maxPayloadBytes, payloadBytes),
|
|
1874
|
+
}
|
|
1875
|
+
if (!window.parent || window.parent === window) return
|
|
1876
|
+
if (typeof window.parent.postMessage !== 'function') return
|
|
1877
|
+
window.parent.postMessage(message, studioTargetOrigin())
|
|
1878
|
+
})
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function totalSidecarMarkers(frames) {
|
|
1882
|
+
return frames.reduce((sum, frame) => sum + frame.sidecar.markers.length, 0)
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function allSidecarMarkersRendered(frames, anchors) {
|
|
1886
|
+
const renderedMarkerIds = new Set(anchors.map((anchor) => anchor.markerId))
|
|
1887
|
+
return frames.every((frame) =>
|
|
1888
|
+
frame.sidecar.markers.every((marker) =>
|
|
1889
|
+
marker.presence === 'optional' ||
|
|
1890
|
+
marker.presence === 'expected' ||
|
|
1891
|
+
renderedMarkerIds.has(marker.markerId)
|
|
1892
|
+
)
|
|
1893
|
+
)
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function allowsMultipleAnchors(markerId, sidecarMarkers) {
|
|
1897
|
+
const marker = sidecarMarkers.get(markerId)
|
|
1898
|
+
return marker && marker.anchorMultiplicity === 'multi'
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function isRequiredMarker(markerId, sidecarMarkers) {
|
|
1902
|
+
const marker = sidecarMarkers.get(markerId)
|
|
1903
|
+
return marker && marker.presence !== 'optional' && marker.presence !== 'expected'
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function cancelReadyObservation() {
|
|
1907
|
+
if (readyFrame !== null) cancelAnimationFrame(readyFrame)
|
|
1908
|
+
if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
|
|
1909
|
+
readyFrame = null
|
|
1910
|
+
readyRetryTimer = null
|
|
1911
|
+
readyWork = null
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function queueReadyFrame() {
|
|
1915
|
+
if (!documentActive || document.visibilityState === 'hidden' || readyFrame !== null || readyRetryTimer !== null || readyPassRunning || readyWork === null) return
|
|
1916
|
+
readyFrame = requestAnimationFrame(runReadyPass)
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function runReadyPass() {
|
|
1920
|
+
readyFrame = null
|
|
1921
|
+
const work = readyWork
|
|
1922
|
+
if (!documentActive || document.visibilityState === 'hidden' || work === null || work.epoch !== cancelEpoch) return
|
|
1923
|
+
// Preserve the initial two-paint boundary; retries remain in this chain.
|
|
1924
|
+
if (!work.afterPaint) {
|
|
1925
|
+
work.afterPaint = true
|
|
1926
|
+
queueReadyFrame()
|
|
1927
|
+
return
|
|
1928
|
+
}
|
|
1929
|
+
readyPassRunning = true
|
|
1930
|
+
const revision = work.revision
|
|
1931
|
+
const phase = work.phase
|
|
1932
|
+
const identity = work.identity
|
|
1933
|
+
try {
|
|
1934
|
+
observationStats.readyPasses += 1
|
|
1935
|
+
const observation = collectObservation()
|
|
1936
|
+
if (!documentActive || work !== readyWork || work.epoch !== cancelEpoch || revision !== work.revision) return
|
|
1937
|
+
const routeFrame = observation.frames.find((frame) => frame.frameId === 'route')
|
|
1938
|
+
const routeReady = !routeFrame || observation.domRouteHash === routeFrame.sidecar.routeHash
|
|
1939
|
+
const anchorsReady = allSidecarMarkersRendered(observation.frames, observation.anchors)
|
|
1940
|
+
const remainingMs = work.deadline - performance.now()
|
|
1941
|
+
if ((!routeReady || !anchorsReady) && remainingMs > 0) {
|
|
1942
|
+
readyRetryTimer = setTimeout(() => {
|
|
1943
|
+
readyRetryTimer = null
|
|
1944
|
+
queueReadyFrame()
|
|
1945
|
+
}, Math.min(READY_RETRY_MS, remainingMs))
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
publish(phase, false, identity, observation, work.epoch)
|
|
1949
|
+
if (!documentActive || work !== readyWork || work.epoch !== cancelEpoch) return
|
|
1950
|
+
if (work.epoch === pendingNavigationEpoch) pendingNavigationEpoch = null
|
|
1951
|
+
if (revision === work.revision) readyWork = null
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
// An unexpected collector failure must not become a perpetual rAF loop.
|
|
1954
|
+
if (readyWork === work) readyWork = null
|
|
1955
|
+
throw error
|
|
1956
|
+
} finally {
|
|
1957
|
+
readyPassRunning = false
|
|
1958
|
+
queueReadyFrame()
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function scheduleReady(phase, identity = currentRenderIdentity, epoch = cancelEpoch) {
|
|
1963
|
+
if (!documentActive || epoch !== cancelEpoch) return
|
|
1964
|
+
if (readyWork === null || readyWork.epoch !== epoch) {
|
|
1965
|
+
readyWork = { phase, identity, epoch, deadline: performance.now() + READY_DEADLINE_MS, afterPaint: false, revision: 0 }
|
|
1966
|
+
} else {
|
|
1967
|
+
readyWork.phase = phase
|
|
1968
|
+
readyWork.identity = identity
|
|
1969
|
+
readyWork.revision += 1
|
|
1970
|
+
}
|
|
1971
|
+
// Actual invalidations can run promptly but never restart the deadline.
|
|
1972
|
+
if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
|
|
1973
|
+
readyRetryTimer = null
|
|
1974
|
+
queueReadyFrame()
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function beginNavigation(pathname) {
|
|
1978
|
+
if (!documentActive) return
|
|
1979
|
+
cancelReadyObservation()
|
|
1980
|
+
cancelEpoch += 1
|
|
1981
|
+
iframeGeneration = Math.max(iframeGeneration + 1, currentRenderIdentity.generation + 1)
|
|
1982
|
+
const iframeIdentity = { generation: iframeGeneration, sourceOfTruth: 'iframe' }
|
|
1983
|
+
currentRenderIdentity = iframeIdentity
|
|
1984
|
+
pendingNavigationEpoch = cancelEpoch
|
|
1985
|
+
publish('pending/navigation-start', true, iframeIdentity)
|
|
1986
|
+
scheduleReady('navigation:' + pathname, iframeIdentity, cancelEpoch)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function ensureNavigationGeneration(pathname) {
|
|
1990
|
+
if (pendingNavigationEpoch !== null) {
|
|
1991
|
+
scheduleReady('navigation:' + pathname, currentRenderIdentity, pendingNavigationEpoch)
|
|
1992
|
+
return
|
|
1993
|
+
}
|
|
1994
|
+
beginNavigation(pathname)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function applyRenderIdentity(identity) {
|
|
1998
|
+
const nextIdentity = identity === null || identity === undefined
|
|
1999
|
+
? { generation: currentRenderIdentity.generation, sourceOfTruth: 'system' }
|
|
2000
|
+
: normalizeRenderIdentity(identity)
|
|
2001
|
+
if (identity !== null && identity !== undefined) {
|
|
2002
|
+
if (nextIdentity.generation < currentRenderIdentity.generation) return
|
|
2003
|
+
if (
|
|
2004
|
+
pendingNavigationEpoch !== null &&
|
|
2005
|
+
currentRenderIdentity.sourceOfTruth === 'iframe' &&
|
|
2006
|
+
nextIdentity.sourceOfTruth !== 'iframe' &&
|
|
2007
|
+
nextIdentity.generation <= currentRenderIdentity.generation
|
|
2008
|
+
) {
|
|
2009
|
+
return
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (
|
|
2013
|
+
nextIdentity.generation === currentRenderIdentity.generation &&
|
|
2014
|
+
nextIdentity.sourceOfTruth === currentRenderIdentity.sourceOfTruth &&
|
|
2015
|
+
nextIdentity.commandId === currentRenderIdentity.commandId
|
|
2016
|
+
) return
|
|
2017
|
+
cancelReadyObservation()
|
|
2018
|
+
cancelEpoch += 1
|
|
2019
|
+
pendingNavigationEpoch = null
|
|
2020
|
+
currentRenderIdentity = nextIdentity
|
|
2021
|
+
iframeGeneration = Math.max(iframeGeneration, currentRenderIdentity.generation)
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function isPreviewLifecycleSourceOfTruth(value) {
|
|
2025
|
+
return value === 'studio' || value === 'iframe' || value === 'system'
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function renderIdentityFromMessage(message) {
|
|
2029
|
+
if (!message || typeof message !== 'object') return null
|
|
2030
|
+
if (message.type !== 'render-mode') return null
|
|
2031
|
+
const lifecycle = message.previewLifecycle
|
|
2032
|
+
if (
|
|
2033
|
+
lifecycle &&
|
|
2034
|
+
typeof lifecycle === 'object' &&
|
|
2035
|
+
Number.isFinite(lifecycle.generation) &&
|
|
2036
|
+
isPreviewLifecycleSourceOfTruth(lifecycle.sourceOfTruth)
|
|
2037
|
+
) {
|
|
2038
|
+
return normalizeRenderIdentity({
|
|
2039
|
+
generation: lifecycle.generation,
|
|
2040
|
+
sourceOfTruth: lifecycle.sourceOfTruth,
|
|
2041
|
+
...(typeof lifecycle.commandId === 'string' && lifecycle.commandId.length > 0
|
|
2042
|
+
? { commandId: lifecycle.commandId }
|
|
2043
|
+
: {}),
|
|
2044
|
+
})
|
|
2045
|
+
}
|
|
2046
|
+
if (Number.isFinite(message.epoch)) {
|
|
2047
|
+
return normalizeRenderIdentity({
|
|
2048
|
+
generation: message.epoch,
|
|
2049
|
+
sourceOfTruth: 'studio',
|
|
2050
|
+
})
|
|
2051
|
+
}
|
|
2052
|
+
return null
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function interactionModeFromMessage(message) {
|
|
2056
|
+
if (!message || typeof message !== 'object') return null
|
|
2057
|
+
if (message.type !== 'storystick.preview.interaction-mode') return null
|
|
2058
|
+
const payload = message.payload
|
|
2059
|
+
if (!payload || typeof payload !== 'object') return null
|
|
2060
|
+
return payload.mode === 'edit' || payload.mode === 'browse'
|
|
2061
|
+
? payload.mode
|
|
2062
|
+
: null
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
window.__ssPreviewProvenanceSetRenderIdentity = function setRenderIdentity(identity) {
|
|
2066
|
+
if (!documentActive) return
|
|
2067
|
+
applyRenderIdentity(identity)
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
window.addEventListener('message', (event) => {
|
|
2071
|
+
if (!documentActive) return
|
|
2072
|
+
if (event.source !== window.parent) return
|
|
2073
|
+
if (validSnapshot(event.data)) {
|
|
2074
|
+
if (event.data.sessionId !== sessionId()) return
|
|
2075
|
+
if (sidecarStore.accept(event.data)) {
|
|
2076
|
+
scheduleReady('provenance-sidecar:' + window.location.pathname)
|
|
2077
|
+
}
|
|
2078
|
+
return
|
|
2079
|
+
}
|
|
2080
|
+
const interactionMode = interactionModeFromMessage(event.data)
|
|
2081
|
+
if (interactionMode !== null) {
|
|
2082
|
+
currentInteractionMode = interactionMode
|
|
2083
|
+
return
|
|
2084
|
+
}
|
|
2085
|
+
const identity = renderIdentityFromMessage(event.data)
|
|
2086
|
+
if (identity === null) return
|
|
2087
|
+
applyRenderIdentity(identity)
|
|
2088
|
+
scheduleReady('render-mode:' + window.location.pathname)
|
|
2089
|
+
})
|
|
2090
|
+
|
|
2091
|
+
document.addEventListener('click', (event) => {
|
|
2092
|
+
const target = event.target instanceof Element ? event.target.closest('a[href][data-ss-e], a[href][data-ss-p], [data-ss-e] a[href], [data-ss-p] a[href]') : null
|
|
2093
|
+
if (!(target instanceof HTMLAnchorElement)) return
|
|
2094
|
+
if (target.origin !== window.location.origin) return
|
|
2095
|
+
if (currentInteractionMode === 'edit') return
|
|
2096
|
+
if (normalizeMomentaryNavigateLinkClick(event, target)) return
|
|
2097
|
+
beginNavigation(new URL(target.href).pathname)
|
|
2098
|
+
}, { capture: true })
|
|
2099
|
+
|
|
2100
|
+
document.addEventListener('keydown', (event) => {
|
|
2101
|
+
if (!isMomentaryNavigateKeyDown(event)) return
|
|
2102
|
+
emitMomentaryNavigateKey(true)
|
|
2103
|
+
}, { capture: true })
|
|
2104
|
+
|
|
2105
|
+
document.addEventListener('keyup', (event) => {
|
|
2106
|
+
if (event.key === 'Alt' || !event.altKey) emitMomentaryNavigateKey(false)
|
|
2107
|
+
}, { capture: true })
|
|
2108
|
+
|
|
2109
|
+
window.addEventListener('pagehide', () => emitMomentaryNavigateKey(false))
|
|
2110
|
+
window.addEventListener('pagehide', teardownPaintIdentityStamps)
|
|
2111
|
+
window.addEventListener('pageshow', restorePaintIdentityStampsFromBackForwardCache)
|
|
2112
|
+
document.addEventListener('visibilitychange', () => {
|
|
2113
|
+
emitMomentaryNavigateKey(false)
|
|
2114
|
+
if (!documentActive) return
|
|
2115
|
+
if (document.visibilityState === 'hidden') {
|
|
2116
|
+
if (readyFrame !== null) cancelAnimationFrame(readyFrame)
|
|
2117
|
+
if (readyRetryTimer !== null) clearTimeout(readyRetryTimer)
|
|
2118
|
+
if (viewportFrame !== null) cancelAnimationFrame(viewportFrame)
|
|
2119
|
+
readyFrame = null
|
|
2120
|
+
readyRetryTimer = null
|
|
2121
|
+
viewportFrame = null
|
|
2122
|
+
} else {
|
|
2123
|
+
scheduleReady('visible:' + window.location.pathname)
|
|
2124
|
+
}
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2127
|
+
// Framework adapters report committed locations through the existing router API.
|
|
2128
|
+
// Never patch customer history methods or rewrite authored hrefs.
|
|
2129
|
+
window.addEventListener('storystick.preview.location-committed', () => {
|
|
2130
|
+
if (!documentActive || !sessionId()) return
|
|
2131
|
+
readCarriersDirty = true
|
|
2132
|
+
publisherSnapshotDirty = true
|
|
2133
|
+
ensureNavigationGeneration(window.location.pathname)
|
|
2134
|
+
reconcileProvenancePublishers()
|
|
2135
|
+
})
|
|
2136
|
+
|
|
2137
|
+
window.addEventListener('popstate', () => beginNavigation(window.location.pathname))
|
|
2138
|
+
window.addEventListener('scroll', publishViewportAfterScroll, { passive: true })
|
|
2139
|
+
|
|
2140
|
+
window.addEventListener('storystick.preview.read-context-changed', () => {
|
|
2141
|
+
if (!documentActive) return
|
|
2142
|
+
publisherSnapshotDirty = true
|
|
2143
|
+
reconcileProvenancePublishers()
|
|
2144
|
+
scheduleReady('read-context:' + window.location.pathname)
|
|
2145
|
+
})
|
|
2146
|
+
window.addEventListener('storystick.preview.provenance-occurrence', (event) => {
|
|
2147
|
+
if (!documentActive) return
|
|
2148
|
+
const occurrence = event.detail
|
|
2149
|
+
if (!occurrence || typeof occurrence.frameId !== 'string' || typeof occurrence.renderNonce !== 'string') return
|
|
2150
|
+
const key = JSON.stringify([occurrence.frameId, occurrence.renderNonce])
|
|
2151
|
+
if (occurrence.operation === 'mount') {
|
|
2152
|
+
pendingOccurrenceRemovals.delete(key)
|
|
2153
|
+
readCarriersDirty = true
|
|
2154
|
+
reconcileProvenancePublishers()
|
|
2155
|
+
scheduleReady('mount:' + window.location.pathname)
|
|
2156
|
+
} else if (occurrence.operation === 'unmount') {
|
|
2157
|
+
pendingOccurrenceRemovals.set(key, occurrence)
|
|
2158
|
+
queueMicrotask(() => {
|
|
2159
|
+
if (!pendingOccurrenceRemovals.delete(key)) return
|
|
2160
|
+
const frame = publisherFramesById.get(occurrence.frameId)
|
|
2161
|
+
if (frame?.sidecar.renderNonce !== occurrence.renderNonce) return
|
|
2162
|
+
publisherFramesById.delete(occurrence.frameId)
|
|
2163
|
+
publisherSnapshotDirty = true
|
|
2164
|
+
readCarriersDirty = true
|
|
2165
|
+
if (!documentActive) return
|
|
2166
|
+
reconcileProvenancePublishers()
|
|
2167
|
+
scheduleReady('unmount:' + window.location.pathname)
|
|
2168
|
+
})
|
|
2169
|
+
}
|
|
2170
|
+
})
|
|
2171
|
+
|
|
2172
|
+
function invalidatesReadCarriers(record) {
|
|
2173
|
+
if (isSyntheticOverlayOnlyMutation(record)) return false
|
|
2174
|
+
return record.type !== 'attributes' || (
|
|
2175
|
+
record.attributeName !== 'style' &&
|
|
2176
|
+
record.attributeName !== 'class' &&
|
|
2177
|
+
record.attributeName !== 'hidden' &&
|
|
2178
|
+
record.attributeName !== 'aria-expanded' &&
|
|
2179
|
+
record.attributeName !== 'aria-hidden' &&
|
|
2180
|
+
record.attributeName !== 'data-state' &&
|
|
2181
|
+
record.attributeName !== 'popover' &&
|
|
2182
|
+
record.attributeName !== 'inert' &&
|
|
2183
|
+
record.attributeName !== 'open'
|
|
2184
|
+
)
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
provenanceObserver = new MutationObserver((records) => {
|
|
2188
|
+
if (!documentActive || records.every(isSyntheticOverlayOnlyMutation)) return
|
|
2189
|
+
if (records.some(invalidatesReadCarriers)) readCarriersDirty = true
|
|
2190
|
+
reconcileProvenancePublishers()
|
|
2191
|
+
scheduleReady('mutation:' + window.location.pathname)
|
|
2192
|
+
})
|
|
2193
|
+
provenanceObserver.observe(document.documentElement, PROVENANCE_OBSERVER_OPTIONS)
|
|
2194
|
+
|
|
2195
|
+
window.__ssPreviewProvenanceProbe = {
|
|
2196
|
+
rebuild: (phase = 'probe') => publish(phase, false, currentRenderIdentity),
|
|
2197
|
+
observationStats: () => ({ ...observationStats }),
|
|
2198
|
+
eventLog: () => [...(window.__ssPreviewProvenanceEventLog || [])],
|
|
2199
|
+
lifecycleLog: () => [...(window.__ssPreviewProvenanceLifecycleLog || [])],
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
window[PAINT_TARGET_REGISTRY] = {
|
|
2203
|
+
resolve: resolvePaintTarget,
|
|
2204
|
+
isDomMutationSafe: () => hydrationSafeDomMaterializationEnabled,
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
scheduleHydrationSafeDomMaterialization()
|
|
2208
|
+
// Preserve synchronous authority publication, including initial publisher
|
|
2209
|
+
// ACKs. The scheduled pass reuses its carrier sample if the DOM is unchanged.
|
|
2210
|
+
reconcileProvenancePublishers()
|
|
2211
|
+
scheduleReady('initial:' + window.location.pathname)
|
|
2212
|
+
// This script can load after the snapshot reporter's handshake. Ask Studio
|
|
2213
|
+
// to replay its current contract only after our message listener is installed;
|
|
2214
|
+
// a previously posted render-mode cannot establish this reporter's identity.
|
|
2215
|
+
if (window.parent !== window) window.parent.postMessage({ type: 'ready' }, studioTargetOrigin())
|
|
2216
|
+
})()
|
|
2217
|
+
`)();
|