@storystick/next 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 +31 -0
- package/README.md +522 -2
- package/dist/.source-hash +1 -0
- package/dist/cache-tags.d.ts +2 -0
- package/dist/cache-tags.js +12 -0
- package/dist/clean.d.ts +1 -0
- package/dist/clean.js +1 -0
- package/dist/code-version.d.ts +29 -0
- package/dist/code-version.js +48 -0
- package/dist/file.d.ts +1 -0
- package/dist/file.js +1 -0
- package/dist/i18n-seo.d.ts +1 -0
- package/dist/i18n-seo.js +1 -0
- package/dist/image.d.ts +11 -0
- package/dist/image.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.react-server.d.ts +3 -0
- package/dist/index.react-server.js +12 -0
- package/dist/link.d.ts +10 -0
- package/dist/link.js +8 -0
- package/dist/preview/index.d.ts +29 -0
- package/dist/preview/index.js +24 -0
- package/dist/preview/internal.d.ts +61 -0
- package/dist/preview/internal.js +97 -0
- package/dist/preview/preview-request-identity.d.ts +4 -0
- package/dist/preview/preview-request-identity.js +63 -0
- package/dist/preview/provenance-config.d.ts +42 -0
- package/dist/preview/provenance-config.js +132 -0
- package/dist/preview/provenance-frame-builder.d.ts +6 -0
- package/dist/preview/provenance-frame-builder.js +10 -0
- package/dist/preview/provenance-response.d.ts +5 -0
- package/dist/preview/provenance-response.js +9 -0
- package/dist/preview/provenance-runtime-adapter.d.ts +9 -0
- package/dist/preview/provenance-runtime-adapter.js +42 -0
- package/dist/preview/render-frame-provenance.d.ts +23 -0
- package/dist/preview/render-frame-provenance.js +47 -0
- package/dist/preview/route-authoring.d.ts +3 -0
- package/dist/preview/route-authoring.js +80 -0
- package/dist/preview/route-context.d.ts +3 -0
- package/dist/preview/route-context.js +38 -0
- package/dist/preview/route-disable.d.ts +7 -0
- package/dist/preview/route-disable.js +63 -0
- package/dist/preview/route-document-provenance.d.ts +23 -0
- package/dist/preview/route-document-provenance.js +46 -0
- package/dist/preview/route-errors.d.ts +1 -0
- package/dist/preview/route-errors.js +1 -0
- package/dist/preview/route-handler.d.ts +36 -0
- package/dist/preview/route-handler.js +139 -0
- package/dist/preview/route-initial.d.ts +9 -0
- package/dist/preview/route-initial.js +141 -0
- package/dist/preview/route-provenance-runtime.d.ts +34 -0
- package/dist/preview/route-provenance-runtime.js +90 -0
- package/dist/preview/route-token.d.ts +6 -0
- package/dist/preview/route-token.js +46 -0
- package/dist/preview/server-preview-internals.d.ts +3 -0
- package/dist/preview/server-preview-internals.js +3 -0
- package/dist/preview/slice-locale.d.ts +5 -0
- package/dist/preview/slice-locale.js +15 -0
- package/dist/preview-error-surface.d.ts +7 -0
- package/dist/preview-error-surface.js +24 -0
- package/dist/preview-exit.d.ts +7 -0
- package/dist/preview-exit.js +71 -0
- package/dist/preview-metadata.d.ts +15 -0
- package/dist/preview-metadata.js +13 -0
- package/dist/preview-mode-latch.d.ts +133 -0
- package/dist/preview-mode-latch.js +231 -0
- package/dist/preview-nav-location.d.ts +12 -0
- package/dist/preview-nav-location.js +149 -0
- package/dist/preview-nav.d.ts +94 -0
- package/dist/preview-nav.js +523 -0
- package/dist/preview-refresh-coordinator.d.ts +26 -0
- package/dist/preview-refresh-coordinator.js +93 -0
- package/dist/preview-render-error.d.ts +11 -0
- package/dist/preview-render-error.js +36 -0
- package/dist/preview-render-recovery.d.ts +9 -0
- package/dist/preview-render-recovery.js +42 -0
- package/dist/preview-session-recovery.d.ts +1 -0
- package/dist/preview-session-recovery.js +1 -0
- package/dist/preview-standalone-host.d.ts +3 -0
- package/dist/preview-standalone-host.js +55 -0
- package/dist/preview-text.d.ts +2 -0
- package/dist/preview-text.js +1 -0
- package/dist/preview-view-bootstrap.d.ts +5 -0
- package/dist/preview-view-bootstrap.js +13 -0
- package/dist/public-delivery.d.ts +6 -0
- package/dist/public-delivery.js +7 -0
- package/dist/query-fetch-options.d.ts +7 -0
- package/dist/query-fetch-options.js +193 -0
- package/dist/query.d.ts +2 -0
- package/dist/query.js +1 -0
- package/dist/rendering.d.ts +33 -0
- package/dist/rendering.js +42 -0
- package/dist/revalidate.d.ts +12 -0
- package/dist/revalidate.js +33 -0
- package/dist/rich-text.d.ts +2 -0
- package/dist/rich-text.js +1 -0
- package/dist/server-runtime.d.ts +208 -0
- package/dist/server-runtime.js +1584 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +6 -0
- package/dist/slice-zone.d.ts +2 -0
- package/dist/slice-zone.js +1 -0
- package/dist/storystick-component-types.d.ts +26 -0
- package/dist/storystick-component-types.js +1 -0
- package/dist/storystick-component.d.ts +10 -0
- package/dist/storystick-component.js +45 -0
- package/dist/storystick-error-recovery.d.ts +12 -0
- package/dist/storystick-error-recovery.js +11 -0
- package/dist/storystick-preview-runtime.d.ts +30 -0
- package/dist/storystick-preview-runtime.js +561 -0
- package/dist/storystick.d.ts +24 -0
- package/dist/storystick.js +23 -0
- package/dist/testing.d.ts +9 -0
- package/dist/testing.js +40 -0
- package/dist/video.d.ts +1 -0
- package/dist/video.js +1 -0
- package/package.json +128 -3
- package/recipes/README.md +91 -0
- package/recipes/resource-mirror.ts +49 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createPreviewRequestId } from '@storystick/preview/runtime/request-id';
|
|
3
|
+
import { previewRuntime } from '@storystick/preview/runtime/store';
|
|
4
|
+
import { previewPerfNow, recordPreviewPerfMeasure, recordPreviewPerfMetric } from '@storystick/preview/runtime/perf-metrics';
|
|
5
|
+
import { confirmPreviewRender, resetPreviewRenderFailure } from './preview-render-recovery.js';
|
|
6
|
+
import { readPreviewRenderReceipt } from '@storystick/preview/framework/render-receipt';
|
|
7
|
+
/**
|
|
8
|
+
* Next.js preview navigation adapter (ADR-087 Lane D').
|
|
9
|
+
*
|
|
10
|
+
* Wires the framework-agnostic `setNavigateHandler` exported by
|
|
11
|
+
* `@storystick/preview` (Lane D contract) to Next's router so a
|
|
12
|
+
* Studio-issued `storystick.preview.navigate` postMessage drives a
|
|
13
|
+
* client-side `router.replace(pathname)` inside the preview iframe.
|
|
14
|
+
*
|
|
15
|
+
* **Honest claim per ADR §3.2 / §7** : this is **best-effort CSR
|
|
16
|
+
* navigation**, NOT *"framework-agnostic navigation"*.
|
|
17
|
+
*
|
|
18
|
+
* - **App Router (RSC)** : `router.replace(pathname)` triggers a CSR
|
|
19
|
+
* transition. The preview selector is preserved and the destination
|
|
20
|
+
* Server Components resolve their short-lived access token from the
|
|
21
|
+
* durable HttpOnly PreviewGrant.
|
|
22
|
+
* - **Pages Router** : `router.replace(pathname)` performs the usual
|
|
23
|
+
* client-side transition. `getServerSideProps` re-runs and
|
|
24
|
+
* preserves the Storystick preview selector in the destination URL.
|
|
25
|
+
*
|
|
26
|
+
* **Why no fully-automatic router auto-detect** : the brief's first
|
|
27
|
+
* draft tried calling `useRouter` from BOTH `next/navigation` and
|
|
28
|
+
* `next/router` unconditionally and picking the first non-null
|
|
29
|
+
* result, but Next's hooks throw outside their respective contexts
|
|
30
|
+
* (codex round 1 P0) — `next/navigation::useRouter` throws in Pages
|
|
31
|
+
* Router (Next 15 invariant E238), and `next/router::useRouter`
|
|
32
|
+
* throws in App Router. Detecting "which router context am I in?"
|
|
33
|
+
* without internal Next APIs is impossible inside a single hook.
|
|
34
|
+
*
|
|
35
|
+
* The robust fix : we use **`next/compat/router::useRouter`**, the
|
|
36
|
+
* Next-officially-supported universal hook that returns the Pages
|
|
37
|
+
* `NextRouter` instance when mounted inside Pages Router, returns
|
|
38
|
+
* `null` otherwise (App Router or no-router env), and **never
|
|
39
|
+
* throws**. App Router consumers pass their `useRouter()` instance
|
|
40
|
+
* (from `next/navigation`) explicitly as the optional `appRouter`
|
|
41
|
+
* argument. The hook prefers the explicit App Router argument when
|
|
42
|
+
* provided, otherwise falls back to the compat-detected Pages
|
|
43
|
+
* router.
|
|
44
|
+
*
|
|
45
|
+
* Usage — App Router :
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* 'use client'
|
|
49
|
+
* import { useRouter } from 'next/navigation'
|
|
50
|
+
* import { StorystickQueryProvider } from '@storystick/next'
|
|
51
|
+
* import { useStorystickPreviewNavigation } from '@storystick/next/preview-nav'
|
|
52
|
+
*
|
|
53
|
+
* export function Providers({ children }: { children: React.ReactNode }) {
|
|
54
|
+
* const router = useRouter()
|
|
55
|
+
* useStorystickPreviewNavigation(router)
|
|
56
|
+
* return (
|
|
57
|
+
* <StorystickQueryProvider projectId="..." publishableKey="...">
|
|
58
|
+
* {children}
|
|
59
|
+
* </StorystickQueryProvider>
|
|
60
|
+
* )
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Usage — Pages Router (no argument needed) :
|
|
65
|
+
*
|
|
66
|
+
* ```tsx
|
|
67
|
+
* import { StorystickQueryProvider } from '@storystick/next'
|
|
68
|
+
* import { useStorystickPreviewNavigation } from '@storystick/next/preview-nav'
|
|
69
|
+
*
|
|
70
|
+
* export function Providers({ children }: { children: React.ReactNode }) {
|
|
71
|
+
* useStorystickPreviewNavigation()
|
|
72
|
+
* return (
|
|
73
|
+
* <StorystickQueryProvider projectId="..." publishableKey="...">
|
|
74
|
+
* {children}
|
|
75
|
+
* </StorystickQueryProvider>
|
|
76
|
+
* )
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* **Why a separate subpath** : `@storystick/next` (root) is imported
|
|
81
|
+
* by Server Components for `fetchDocument` / `fetchQuery`. The
|
|
82
|
+
* preview hook drags `next/compat/router` and React client-only
|
|
83
|
+
* primitives, which would pollute the server module graph (codex
|
|
84
|
+
* Lane D' round 2 P1 fix). Keeping the hook at
|
|
85
|
+
* `@storystick/next/preview-nav` keeps the server entry pure.
|
|
86
|
+
*
|
|
87
|
+
* **Sentinel guard** : the hook is a no-op when not in preview mode.
|
|
88
|
+
* Preview mode = embedded iframe (`window.parent !== window`) AND a
|
|
89
|
+
* `_ss_session` URL parameter — same heuristic as `detectPreviewMode`
|
|
90
|
+
* in `@storystick/react`'s provider. Outside preview mode the hook
|
|
91
|
+
* never registers a handler and `setNavigateHandler` keeps its
|
|
92
|
+
* default no-op.
|
|
93
|
+
*
|
|
94
|
+
* **Cleanup** : on unmount the hook restores a surviving layout adapter,
|
|
95
|
+
* or the no-op default when none remains. External overrides stay intact.
|
|
96
|
+
*
|
|
97
|
+
* **Hook vs imperative** : `useRouter` from Next can ONLY be called
|
|
98
|
+
* inside React components, which forces the public surface to be a
|
|
99
|
+
* hook. Consumers place this hook in their root client component
|
|
100
|
+
* (typically the same component that renders `<StorystickQueryProvider>`).
|
|
101
|
+
*/
|
|
102
|
+
import { useEffect, useLayoutEffect, useRef, useState, useTransition } from 'react';
|
|
103
|
+
import { useRouter as useCompatRouter } from 'next/compat/router';
|
|
104
|
+
// ADR-116 FU2 — synchronous post-render commit avoids an HMR re-mount
|
|
105
|
+
// race where a residual `activate()` listener delivers a navigate
|
|
106
|
+
// envelope between the new mount's render and a passive `useEffect`
|
|
107
|
+
// commit, dispatching to the no-op handler.
|
|
108
|
+
const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
109
|
+
import { setNavigateHandler, getNavigateHandler, setReloadHandler, getReloadHandler, } from '@storystick/preview/nav-handler';
|
|
110
|
+
import { notifyRouterTransition } from '@storystick/preview/router-subscriber';
|
|
111
|
+
import { isPreviewModeLatched, readPreviewSessionIdForNavigation, readPreviewVariantIdForNavigation, readPreviewReleaseIdForNavigation, setLatchedPreviewScope, } from './preview-mode-latch.js';
|
|
112
|
+
import { beginPreviewNavigation, cancelPreviewRefreshCoordination, observePreviewNavigationCommit, } from './preview-refresh-coordinator.js';
|
|
113
|
+
import { PREVIEW_SESSION_QUERY_PARAM, writePreviewScopeToSearchParams, } from '@storystick/preview/identity';
|
|
114
|
+
// A nested error fallback may coexist with the layout adapter. When it leaves,
|
|
115
|
+
// return control to the latest still-mounted adapter, never to a stale closure.
|
|
116
|
+
const navigationAdapters = new Set();
|
|
117
|
+
/**
|
|
118
|
+
* ADR-108 D6.bis — strip a configured basePath from a browser-form
|
|
119
|
+
* pathname. Pure function exported for tests.
|
|
120
|
+
*
|
|
121
|
+
* stripBasePath('/admin', '/admin/page') → '/page'
|
|
122
|
+
* stripBasePath('/admin/', '/admin/page') → '/page' (trailing slash normalised)
|
|
123
|
+
* stripBasePath('/admin', '/admin') → '/' (exact match → root)
|
|
124
|
+
* stripBasePath('/admin', '/administrator') → '/administrator' (segment-boundary)
|
|
125
|
+
* stripBasePath('', '/page') → '/page' (no-op)
|
|
126
|
+
*/
|
|
127
|
+
export function stripBasePath(basePath, pathname) {
|
|
128
|
+
if (basePath === '' || basePath === '/')
|
|
129
|
+
return pathname;
|
|
130
|
+
// Normalise: ensure leading slash, drop trailing slash.
|
|
131
|
+
let bp = basePath;
|
|
132
|
+
if (!bp.startsWith('/'))
|
|
133
|
+
bp = '/' + bp;
|
|
134
|
+
if (bp.endsWith('/'))
|
|
135
|
+
bp = bp.replace(/\/+$/, '');
|
|
136
|
+
if (bp === '')
|
|
137
|
+
return pathname;
|
|
138
|
+
if (!pathname.startsWith(bp))
|
|
139
|
+
return pathname;
|
|
140
|
+
const rest = pathname.slice(bp.length);
|
|
141
|
+
// Path-segment boundary: rest must be empty (exact match) OR start
|
|
142
|
+
// with `/` (next segment). `/administrator` does NOT match
|
|
143
|
+
// basePath `/admin` because rest would be `istrator` (no leading
|
|
144
|
+
// slash) — bail out.
|
|
145
|
+
if (rest === '')
|
|
146
|
+
return '/';
|
|
147
|
+
if (!rest.startsWith('/'))
|
|
148
|
+
return pathname;
|
|
149
|
+
return rest;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Preserve the `_ss_session` URL parameter when navigating inside
|
|
153
|
+
* the preview iframe. Studio's `storystick.preview.navigate`
|
|
154
|
+
* envelope only carries the destination `pathname` ; if we push it
|
|
155
|
+
* raw, the current `?_ss_session=...` marker is dropped and
|
|
156
|
+
* `<StorystickQueryProvider>`'s `detectPreviewMode()` re-evaluates to
|
|
157
|
+
* false on the next render, tearing down the PreviewGrant-backed
|
|
158
|
+
* Convex preview subscription mid-session (codex Lane D' round 4
|
|
159
|
+
* P1).
|
|
160
|
+
*
|
|
161
|
+
* Source precedence (ADR-113 follow-up — codex R4 finding #2):
|
|
162
|
+
* 1. **Destination's own `_ss_session`**: if Studio explicitly put
|
|
163
|
+
* a session id in the destination path, respect it and do not
|
|
164
|
+
* overwrite.
|
|
165
|
+
* 2. **Latched preview session id**: the boot-time value captured
|
|
166
|
+
* by `isPreviewModeLatched()`. Studio-authoritative, stable
|
|
167
|
+
* across customer intra-iframe nav that strips the URL param.
|
|
168
|
+
* 3. **Live `window.location.search`**: fallback only. Useful when
|
|
169
|
+
* the latch is empty (e.g. an old SDK build that didn't track
|
|
170
|
+
* sessionId AND the URL never lost it) — the opportunistic
|
|
171
|
+
* backfill in `isPreviewModeLatched()` normally upgrades the
|
|
172
|
+
* latch before we get here.
|
|
173
|
+
*
|
|
174
|
+
* The exact variant/release selector is carried with the same destination so
|
|
175
|
+
* the request-owned read scope survives customer navigation. A trusted Studio
|
|
176
|
+
* command replaces the latched scope atomically; living clears both selectors.
|
|
177
|
+
*
|
|
178
|
+
* The pathname is already validated upstream as a same-origin
|
|
179
|
+
* absolute path (starts with `/`, not `//`) by
|
|
180
|
+
* `validatePreviewEnvelope`.
|
|
181
|
+
*/
|
|
182
|
+
function preservePreviewSession(pathname, explicitScope) {
|
|
183
|
+
if (typeof window === 'undefined')
|
|
184
|
+
return pathname;
|
|
185
|
+
// ADR-116 PR5 — preview identity facade owns the precedence chain:
|
|
186
|
+
// identity layer first, latch fallback second, live URL bootstrap
|
|
187
|
+
// last. Keep adapter code away from direct URL/storage/latch reads.
|
|
188
|
+
const currentSession = readPreviewSessionIdForNavigation();
|
|
189
|
+
if (currentSession === null)
|
|
190
|
+
return pathname;
|
|
191
|
+
const currentVariant = readPreviewVariantIdForNavigation();
|
|
192
|
+
const currentRelease = readPreviewReleaseIdForNavigation();
|
|
193
|
+
// Split the destination on `?` to merge query strings cleanly.
|
|
194
|
+
// The pathname may already include a query (e.g. `/about?foo=bar`)
|
|
195
|
+
// and/or a hash (e.g. `/about#section`). We preserve those.
|
|
196
|
+
const hashIndex = pathname.indexOf('#');
|
|
197
|
+
const hash = hashIndex >= 0 ? pathname.slice(hashIndex) : '';
|
|
198
|
+
const pathWithQuery = hashIndex >= 0 ? pathname.slice(0, hashIndex) : pathname;
|
|
199
|
+
const queryIndex = pathWithQuery.indexOf('?');
|
|
200
|
+
const path = queryIndex >= 0 ? pathWithQuery.slice(0, queryIndex) : pathWithQuery;
|
|
201
|
+
const existingQuery = queryIndex >= 0 ? pathWithQuery.slice(queryIndex + 1) : '';
|
|
202
|
+
const params = new URLSearchParams(existingQuery);
|
|
203
|
+
// If the destination already specifies `_ss_session`, respect it.
|
|
204
|
+
if (!params.has(PREVIEW_SESSION_QUERY_PARAM)) {
|
|
205
|
+
params.set(PREVIEW_SESSION_QUERY_PARAM, currentSession);
|
|
206
|
+
}
|
|
207
|
+
if (explicitScope !== undefined) {
|
|
208
|
+
// Studio navigation is authoritative. In particular, living scope must
|
|
209
|
+
// remove a variant/release left in the current tab URL instead of
|
|
210
|
+
// inheriting it accidentally.
|
|
211
|
+
writePreviewScopeToSearchParams(params, explicitScope);
|
|
212
|
+
}
|
|
213
|
+
else if (currentRelease !== null) {
|
|
214
|
+
writePreviewScopeToSearchParams(params, {
|
|
215
|
+
kind: 'release',
|
|
216
|
+
releaseId: currentRelease,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (currentVariant !== null) {
|
|
220
|
+
writePreviewScopeToSearchParams(params, {
|
|
221
|
+
kind: 'variant',
|
|
222
|
+
variantId: currentVariant,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const merged = params.toString();
|
|
226
|
+
return merged.length > 0 ? `${path}?${merged}${hash}` : `${path}${hash}`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Wire `@storystick/preview`'s navigate handler to a Next router.
|
|
230
|
+
*
|
|
231
|
+
* @param appRouter — App Router instance from `next/navigation`'s
|
|
232
|
+
* `useRouter()`. App Router consumers pass it explicitly (see
|
|
233
|
+
* module JSDoc). Optional — when omitted, the hook falls back to
|
|
234
|
+
* the Pages Router instance auto-detected via `next/compat/router`.
|
|
235
|
+
*
|
|
236
|
+
* No-op outside preview mode. On unmount, restores a surviving adapter or
|
|
237
|
+
* the default no-op handler, leaving no stale registration behind.
|
|
238
|
+
*/
|
|
239
|
+
export function useStorystickPreviewNavigation(appRouter, options) {
|
|
240
|
+
const [refreshPending, startRefresh] = useTransition();
|
|
241
|
+
const [refreshCommand, setRefreshCommand] = useState(null);
|
|
242
|
+
const navigationRevisionRef = useRef(0);
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (refreshPending || refreshCommand === null || typeof window === 'undefined')
|
|
245
|
+
return;
|
|
246
|
+
const target = new URL(refreshCommand.destination, window.location.href);
|
|
247
|
+
const basePath = options?.basePath ?? '';
|
|
248
|
+
const receipt = readPreviewRenderReceipt();
|
|
249
|
+
recordPreviewPerfMeasure(window, 'next.navigate.refreshSettled', refreshCommand.startedAt, {
|
|
250
|
+
freshReceipt: receipt !== null && receipt.renderId !== refreshCommand.previousRenderId,
|
|
251
|
+
});
|
|
252
|
+
if (target.pathname === stripBasePath(basePath, window.location.pathname) && target.search === window.location.search && target.hash === window.location.hash && confirmPreviewRender()) {
|
|
253
|
+
// App Router location hooks do not fire when only the draft identity
|
|
254
|
+
// changes. React's transition settlement supplies that render boundary.
|
|
255
|
+
notifyRouterTransition({
|
|
256
|
+
pathname: target.pathname,
|
|
257
|
+
search: target.search.replace(/^\?/, ''), hash: target.hash,
|
|
258
|
+
commandId: refreshCommand.commandId,
|
|
259
|
+
...(receipt && receipt.renderId !== refreshCommand.previousRenderId
|
|
260
|
+
? { presentation: { ...receipt, pathname: target.pathname } } : {}),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
setRefreshCommand(null);
|
|
264
|
+
}, [refreshCommand, refreshPending, options?.basePath]);
|
|
265
|
+
// `next/compat/router::useRouter` is the only Next router hook
|
|
266
|
+
// that is safe to call unconditionally in BOTH App Router and
|
|
267
|
+
// Pages Router contexts (it returns `NextRouter | null` and never
|
|
268
|
+
// throws). The compat hook reads from Pages Router's
|
|
269
|
+
// `RouterContext`, which is null in App Router → returns null →
|
|
270
|
+
// we fall through to the explicit `appRouter` argument.
|
|
271
|
+
const pagesRouter = useCompatRouter();
|
|
272
|
+
// App Router argument wins when provided AND has a usable
|
|
273
|
+
// `replace` ; otherwise fall back to the auto-detected Pages
|
|
274
|
+
// router. If neither is available we leave `setNavigateHandler`
|
|
275
|
+
// untouched (default no-op).
|
|
276
|
+
const router = appRouter !== undefined &&
|
|
277
|
+
appRouter !== null &&
|
|
278
|
+
typeof appRouter.replace === 'function'
|
|
279
|
+
? appRouter
|
|
280
|
+
: pagesRouter !== null && typeof pagesRouter.replace === 'function'
|
|
281
|
+
? pagesRouter
|
|
282
|
+
: null;
|
|
283
|
+
useIsoLayoutEffect(() => {
|
|
284
|
+
// ADR-116 PR8 — register the handler UNCONDITIONALLY (gated only
|
|
285
|
+
// on `router !== null`). The preview-mode / identity gate moves
|
|
286
|
+
// INSIDE the handler body so dispatches fire correctly even after
|
|
287
|
+
// a hard navigation inside the iframe.
|
|
288
|
+
//
|
|
289
|
+
// Pre-PR8 the effect bailed at mount when `isPreviewModeLatched()`
|
|
290
|
+
// returned false. After a hard-nav the new realm boots with the
|
|
291
|
+
// URL stripped of `_ss_session` AND `globalThis` reset, so the
|
|
292
|
+
// synchronous latch read returned false. Identity arrived ~50-300ms
|
|
293
|
+
// later via the resolver's storage carrier / Studio session-broadcast
|
|
294
|
+
// (ADR-116 D3) but the `useEffect` deps `[router]` did NOT change,
|
|
295
|
+
// so the effect never re-ran → handler never registered → Studio's
|
|
296
|
+
// navigate envelopes silently dropped by the default no-op
|
|
297
|
+
// `getNavigateHandler()`. Forward channel dead post-hard-nav.
|
|
298
|
+
//
|
|
299
|
+
// The fix : register unconditionally, gate at dispatch time. The
|
|
300
|
+
// observable behaviour for non-preview mode is unchanged (handler
|
|
301
|
+
// bails immediately) but the post-recovery dispatch flows because
|
|
302
|
+
// the handler is already wired by the time identity arrives.
|
|
303
|
+
//
|
|
304
|
+
// ADR-113 latch seed (preserved side effect) — call
|
|
305
|
+
// `isPreviewModeLatched()` once at mount, ignoring the return
|
|
306
|
+
// value. The function's first invocation populates
|
|
307
|
+
// `globalThis.__storystick_preview_mode_latched__` from the boot
|
|
308
|
+
// URL state. Without this eager seed, the cache stays undefined
|
|
309
|
+
// until a customer intra-iframe nav (which can strip `_ss_session`
|
|
310
|
+
// from the URL) AND the first dispatch — which would then capture
|
|
311
|
+
// the post-strip URL state and lose the original sessionId. The
|
|
312
|
+
// dispatch-time `isPreviewModeLatched()` inside the handler reads
|
|
313
|
+
// the now-cached value, not the live URL.
|
|
314
|
+
isPreviewModeLatched();
|
|
315
|
+
if (router === null)
|
|
316
|
+
return;
|
|
317
|
+
// Hold the handler we register so the cleanup can verify that the
|
|
318
|
+
// currently-installed handler is still ours before clearing it.
|
|
319
|
+
// Without this guard, two adapters mounted concurrently would
|
|
320
|
+
// race : the second `setNavigateHandler` wins, but unmounting the
|
|
321
|
+
// FIRST adapter would then clear the second one's handler (codex
|
|
322
|
+
// Lane D' round 3 P2 fix).
|
|
323
|
+
const ourHandler = async (pathname, navigateOptions) => {
|
|
324
|
+
// ADR-116 PR8 dispatch-time gate. The handler is registered
|
|
325
|
+
// unconditionally at mount ; this check ensures we never call
|
|
326
|
+
// `router.replace()` outside preview mode. After hard-nav
|
|
327
|
+
// recovery, `isPreviewModeLatched()` reads through to
|
|
328
|
+
// `previewRuntime.getIdentity()` (PR5 read-through) and returns
|
|
329
|
+
// true once identity is restored.
|
|
330
|
+
if (!isPreviewModeLatched())
|
|
331
|
+
return;
|
|
332
|
+
// Pathname is already validated as a same-origin absolute
|
|
333
|
+
// path (starts with `/`, not `//`) by
|
|
334
|
+
// `validatePreviewEnvelope` in the foundation Lane C — see
|
|
335
|
+
// `nav-handler.ts` JSDoc. Trust the value here.
|
|
336
|
+
//
|
|
337
|
+
// We MUST preserve `_ss_session` across the replace,
|
|
338
|
+
// otherwise `<StorystickQueryProvider>`'s `detectPreviewMode()`
|
|
339
|
+
// re-evaluates to false on the next render and tears
|
|
340
|
+
// down the PreviewGrant-backed Convex preview subscription
|
|
341
|
+
// (codex Lane D' round 4 P1 fix).
|
|
342
|
+
//
|
|
343
|
+
// Studio retains its own history (replace). Explicit standalone selector
|
|
344
|
+
// gestures request push; both transports use this same framework adapter.
|
|
345
|
+
const revision = ++navigationRevisionRef.current;
|
|
346
|
+
let scope = previewRuntime.getState().session?.readContext?.perspective;
|
|
347
|
+
if (navigateOptions?.contextCommand && navigateOptions.commandId) {
|
|
348
|
+
const startedAt = previewPerfNow(window);
|
|
349
|
+
const context = await previewRuntime.changePerspective({
|
|
350
|
+
...navigateOptions.contextCommand, commandId: navigateOptions.commandId,
|
|
351
|
+
perspective: navigateOptions.scope,
|
|
352
|
+
}, preservePreviewSession(`${options?.basePath ?? ''}/api/storystick/preview/context`));
|
|
353
|
+
recordPreviewPerfMeasure(window, 'next.navigate.context', startedAt, { accepted: context !== null });
|
|
354
|
+
if (!context || navigationRevisionRef.current !== revision)
|
|
355
|
+
return;
|
|
356
|
+
scope = context.perspective;
|
|
357
|
+
}
|
|
358
|
+
const destination = preservePreviewSession(pathname, scope);
|
|
359
|
+
if (scope !== undefined) {
|
|
360
|
+
setLatchedPreviewScope(scope);
|
|
361
|
+
options?.onScopeChange?.(scope);
|
|
362
|
+
}
|
|
363
|
+
// ADR-111 still requires a root-layout refresh when Studio changed the
|
|
364
|
+
// preview target. It must not run concurrently with `replace()`: both
|
|
365
|
+
// start Flight transitions, and overlapping responses can corrupt the
|
|
366
|
+
// Next development client. The location adapter settles this intent
|
|
367
|
+
// after it observes the committed destination. Exact content refreshes
|
|
368
|
+
// arriving in the meantime are folded into that single post-commit call.
|
|
369
|
+
const shouldReplace = beginPreviewNavigation({
|
|
370
|
+
destination,
|
|
371
|
+
currentLocation: typeof window === 'undefined' ? destination :
|
|
372
|
+
`${stripBasePath(options?.basePath ?? '', window.location.pathname)}${window.location.search}${window.location.hash}`,
|
|
373
|
+
...(navigateOptions?.refresh === true &&
|
|
374
|
+
typeof router.refresh === 'function'
|
|
375
|
+
? {
|
|
376
|
+
refresh: () => { router.refresh?.(); },
|
|
377
|
+
runRefresh: (refresh) => {
|
|
378
|
+
if (navigationRevisionRef.current !== revision)
|
|
379
|
+
return;
|
|
380
|
+
recordPreviewPerfMetric(window, { name: 'next.navigate.refresh' });
|
|
381
|
+
if (navigateOptions.commandId)
|
|
382
|
+
setRefreshCommand({
|
|
383
|
+
destination, commandId: navigateOptions.commandId,
|
|
384
|
+
previousRenderId: readPreviewRenderReceipt()?.renderId,
|
|
385
|
+
startedAt: previewPerfNow(window),
|
|
386
|
+
});
|
|
387
|
+
// Reset and data refresh belong to the same transition.
|
|
388
|
+
// Next otherwise keeps same-path errors latched (draft → home).
|
|
389
|
+
startRefresh(() => { resetPreviewRenderFailure(); refresh(); });
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
: {}),
|
|
393
|
+
});
|
|
394
|
+
if (shouldReplace || (navigateOptions?.refresh === true && typeof router.refresh !== 'function')) {
|
|
395
|
+
recordPreviewPerfMetric(window, { name: 'next.navigate.replace' });
|
|
396
|
+
// Pages Router has no refresh(). A private query revision forces its
|
|
397
|
+
// data navigation even for the exact same URL and fragment (which
|
|
398
|
+
// Next otherwise treats as a hash-only transition without new data).
|
|
399
|
+
if (navigateOptions?.refresh === true && typeof router.refresh !== 'function' && router === pagesRouter) {
|
|
400
|
+
const refreshed = new URL(destination, 'https://preview.invalid');
|
|
401
|
+
refreshed.searchParams.set('_ss_refresh', navigateOptions.commandId ?? createPreviewRequestId());
|
|
402
|
+
startRefresh(() => {
|
|
403
|
+
resetPreviewRenderFailure();
|
|
404
|
+
router[navigateOptions?.history === 'push' ? 'push' : 'replace'](`${refreshed.pathname}${refreshed.search}${refreshed.hash}`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
startRefresh(() => {
|
|
409
|
+
resetPreviewRenderFailure();
|
|
410
|
+
if (navigateOptions?.history === 'push')
|
|
411
|
+
router.push(destination);
|
|
412
|
+
else
|
|
413
|
+
router.replace(destination);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
setNavigateHandler(ourHandler);
|
|
418
|
+
const ourReloadHandler = (mode) => {
|
|
419
|
+
if (!isPreviewModeLatched())
|
|
420
|
+
return;
|
|
421
|
+
if (mode === 'soft') {
|
|
422
|
+
if (typeof router.refresh === 'function') {
|
|
423
|
+
startRefresh(() => { resetPreviewRenderFailure(); router.refresh?.(); });
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (typeof window !== 'undefined') {
|
|
428
|
+
window.location.reload();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
setReloadHandler(ourReloadHandler);
|
|
432
|
+
const registration = { navigate: ourHandler, reload: ourReloadHandler };
|
|
433
|
+
navigationAdapters.add(registration);
|
|
434
|
+
return () => {
|
|
435
|
+
navigationRevisionRef.current += 1;
|
|
436
|
+
navigationAdapters.delete(registration);
|
|
437
|
+
const survivingAdapter = [...navigationAdapters].at(-1);
|
|
438
|
+
// Only restore a survivor/default when WE'RE still the registered
|
|
439
|
+
// handler. If another adapter (or override) installed
|
|
440
|
+
// its own handler after us, leave it alone — clearing
|
|
441
|
+
// would silently break that adapter.
|
|
442
|
+
if (getNavigateHandler() === ourHandler) {
|
|
443
|
+
setNavigateHandler(survivingAdapter?.navigate ?? null);
|
|
444
|
+
cancelPreviewRefreshCoordination();
|
|
445
|
+
}
|
|
446
|
+
if (getReloadHandler() === ourReloadHandler) {
|
|
447
|
+
setReloadHandler(survivingAdapter?.reload ?? null);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
// The router reference from Next's hooks is stable across
|
|
451
|
+
// renders (Next memoises both instances internally), so
|
|
452
|
+
// depending on it here is safe.
|
|
453
|
+
}, [router, pagesRouter, options?.onScopeChange, options?.basePath]);
|
|
454
|
+
// ADR-108 D6 — Pages Router location-subscriber wiring. App Router
|
|
455
|
+
// consumers must additionally call `useStorystickPreviewLocation` —
|
|
456
|
+
// it lives in a separate module to avoid pulling `next/navigation`
|
|
457
|
+
// hooks into this entry's module graph.
|
|
458
|
+
//
|
|
459
|
+
// For Pages Router we attach `router.events.on('routeChangeComplete',
|
|
460
|
+
// cb)` and emit a transition tuple to the SDK's
|
|
461
|
+
// `notifyRouterTransition`. basePath: auto-default from
|
|
462
|
+
// `pagesRouter.basePath` (NextRouter public API). Hash is read from
|
|
463
|
+
// `window.location.hash` (events.on doesn't surface it).
|
|
464
|
+
//
|
|
465
|
+
// App Router consumers fall through this effect (the explicit
|
|
466
|
+
// `appRouter` argument wins above, leaving `pagesRouter` null when
|
|
467
|
+
// App context is in play), so no subscriber registration races
|
|
468
|
+
// with the App-side hook.
|
|
469
|
+
const explicitBasePath = options?.basePath;
|
|
470
|
+
useIsoLayoutEffect(() => {
|
|
471
|
+
// ADR-116 PR8 — same pattern as the navigate handler above : wire
|
|
472
|
+
// `events.on('routeChangeComplete', ...)` unconditionally and gate
|
|
473
|
+
// INSIDE the callback. Pre-PR8 the synchronous latch check at
|
|
474
|
+
// effect-start bailed on a fresh post-hard-nav realm, and the
|
|
475
|
+
// deps `[appRouter, pagesRouter, explicitBasePath]` did not
|
|
476
|
+
// include identity — so the subscriber was never wired and
|
|
477
|
+
// reverse-channel emissions from Pages Router customers died
|
|
478
|
+
// post-hard-nav.
|
|
479
|
+
isPreviewModeLatched(); // seed the globalThis latch eagerly
|
|
480
|
+
// Only wire the Pages Router events path when the resolved
|
|
481
|
+
// router IS the pagesRouter (i.e. no explicit appRouter was
|
|
482
|
+
// provided, OR appRouter was provided but unusable and we fell
|
|
483
|
+
// back to pages). Detecting via `pagesRouter !== null AND
|
|
484
|
+
// !appRouter` is the simplest invariant.
|
|
485
|
+
if (appRouter)
|
|
486
|
+
return;
|
|
487
|
+
if (pagesRouter === null)
|
|
488
|
+
return;
|
|
489
|
+
// Type-narrow the compat router instance to the Pages NextRouter
|
|
490
|
+
// shape we need: events.on/.off + a readable basePath. The
|
|
491
|
+
// compat router exposes both by spec, but the RouterReplaceable
|
|
492
|
+
// narrowed type doesn't include them — re-narrow locally.
|
|
493
|
+
const pr = pagesRouter;
|
|
494
|
+
if (!pr.events || typeof pr.events.on !== 'function')
|
|
495
|
+
return;
|
|
496
|
+
const basePath = explicitBasePath ?? pr.basePath ?? '';
|
|
497
|
+
const onRouteChange = (url) => {
|
|
498
|
+
// ADR-116 PR8 dispatch-time gate. Skip the emission when not in
|
|
499
|
+
// preview mode (top-level page, identity not yet restored, etc.).
|
|
500
|
+
if (!isPreviewModeLatched())
|
|
501
|
+
return;
|
|
502
|
+
// `url` is the Pages Router's "as" path (e.g.
|
|
503
|
+
// `/admin/page?x=1`); not the rewritten internal route.
|
|
504
|
+
// Split into pathname / search; hash is read separately.
|
|
505
|
+
const hashIdx = url.indexOf('#');
|
|
506
|
+
const beforeHash = hashIdx >= 0 ? url.slice(0, hashIdx) : url;
|
|
507
|
+
// events.on doesn't surface the hash (the Pages router
|
|
508
|
+
// doesn't fire on pure hash changes), but hash present on
|
|
509
|
+
// a route change is read from window.location.
|
|
510
|
+
const hash = typeof window !== 'undefined' ? window.location.hash : '';
|
|
511
|
+
const queryIdx = beforeHash.indexOf('?');
|
|
512
|
+
const rawPath = queryIdx >= 0 ? beforeHash.slice(0, queryIdx) : beforeHash;
|
|
513
|
+
const search = queryIdx >= 0 ? beforeHash.slice(queryIdx + 1) : '';
|
|
514
|
+
const pathname = stripBasePath(basePath, rawPath);
|
|
515
|
+
observePreviewNavigationCommit(`${pathname}${search ? `?${search}` : ''}${hash}`);
|
|
516
|
+
notifyRouterTransition({ pathname, search, hash, browserPathname: rawPath });
|
|
517
|
+
};
|
|
518
|
+
pr.events.on('routeChangeComplete', onRouteChange);
|
|
519
|
+
return () => {
|
|
520
|
+
pr.events?.off('routeChangeComplete', onRouteChange);
|
|
521
|
+
};
|
|
522
|
+
}, [appRouter, pagesRouter, explicitBasePath]);
|
|
523
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes framework refreshes around App Router navigation commits.
|
|
3
|
+
*
|
|
4
|
+
* `router.replace()` and `router.refresh()` both start React Flight
|
|
5
|
+
* transitions. Issuing them back-to-back can make two responses race inside
|
|
6
|
+
* Next's Flight client. We therefore wait for the location hook to observe the
|
|
7
|
+
* committed destination, then issue at most one refresh. A projection-ready
|
|
8
|
+
* content refresh arriving meanwhile supersedes the navigation-only refresh.
|
|
9
|
+
*/
|
|
10
|
+
export declare const NAVIGATION_COMMIT_FALLBACK_MS = 8000;
|
|
11
|
+
export declare function beginPreviewNavigation(input: {
|
|
12
|
+
readonly destination: string;
|
|
13
|
+
readonly currentLocation: string;
|
|
14
|
+
readonly refresh?: () => void;
|
|
15
|
+
/** Preserve command settlement when a content refresh replaces the physical refresh. */
|
|
16
|
+
readonly runRefresh?: (refresh: () => void) => void;
|
|
17
|
+
}): boolean;
|
|
18
|
+
/** Called by the framework location adapter after a destination commit. */
|
|
19
|
+
export declare function observePreviewNavigationCommit(committedLocation: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Run an exact content refresh now, or fold it into the navigation refresh
|
|
22
|
+
* that will run immediately after the destination commits.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requestPreviewContentRefresh(refresh: () => void): void;
|
|
25
|
+
export declare function cancelPreviewRefreshCoordination(): void;
|
|
26
|
+
export declare function _resetPreviewRefreshCoordinatorForTests(): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes framework refreshes around App Router navigation commits.
|
|
3
|
+
*
|
|
4
|
+
* `router.replace()` and `router.refresh()` both start React Flight
|
|
5
|
+
* transitions. Issuing them back-to-back can make two responses race inside
|
|
6
|
+
* Next's Flight client. We therefore wait for the location hook to observe the
|
|
7
|
+
* committed destination, then issue at most one refresh. A projection-ready
|
|
8
|
+
* content refresh arriving meanwhile supersedes the navigation-only refresh.
|
|
9
|
+
*/
|
|
10
|
+
export const NAVIGATION_COMMIT_FALLBACK_MS = 8_000;
|
|
11
|
+
let pendingNavigation = null;
|
|
12
|
+
let deferredContentRefresh = null;
|
|
13
|
+
export function beginPreviewNavigation(input) {
|
|
14
|
+
clearPendingNavigation();
|
|
15
|
+
if (sameBrowserLocation(input.destination, input.currentLocation)) {
|
|
16
|
+
const refresh = deferredContentRefresh ?? input.refresh ?? null;
|
|
17
|
+
deferredContentRefresh = null;
|
|
18
|
+
if (refresh)
|
|
19
|
+
(input.runRefresh ?? ((run) => run()))(refresh);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const pending = {
|
|
23
|
+
destination: input.destination,
|
|
24
|
+
refresh: input.refresh ?? null,
|
|
25
|
+
runRefresh: input.runRefresh,
|
|
26
|
+
timer: setTimeout(() => {
|
|
27
|
+
if (pendingNavigation !== pending)
|
|
28
|
+
return;
|
|
29
|
+
settlePendingNavigation();
|
|
30
|
+
}, NAVIGATION_COMMIT_FALLBACK_MS),
|
|
31
|
+
};
|
|
32
|
+
pendingNavigation = pending;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** Called by the framework location adapter after a destination commit. */
|
|
36
|
+
export function observePreviewNavigationCommit(committedLocation) {
|
|
37
|
+
if (pendingNavigation === null ||
|
|
38
|
+
!sameBrowserLocation(pendingNavigation.destination, committedLocation)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
settlePendingNavigation();
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run an exact content refresh now, or fold it into the navigation refresh
|
|
46
|
+
* that will run immediately after the destination commits.
|
|
47
|
+
*/
|
|
48
|
+
export function requestPreviewContentRefresh(refresh) {
|
|
49
|
+
if (pendingNavigation !== null) {
|
|
50
|
+
deferredContentRefresh = refresh;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
refresh();
|
|
54
|
+
}
|
|
55
|
+
export function cancelPreviewRefreshCoordination() {
|
|
56
|
+
clearPendingNavigation();
|
|
57
|
+
deferredContentRefresh = null;
|
|
58
|
+
}
|
|
59
|
+
function settlePendingNavigation() {
|
|
60
|
+
const pending = pendingNavigation;
|
|
61
|
+
if (pending === null)
|
|
62
|
+
return;
|
|
63
|
+
clearTimeout(pending.timer);
|
|
64
|
+
pendingNavigation = null;
|
|
65
|
+
const refresh = deferredContentRefresh ?? pending.refresh;
|
|
66
|
+
deferredContentRefresh = null;
|
|
67
|
+
if (refresh)
|
|
68
|
+
(pending.runRefresh ?? ((run) => run()))(refresh);
|
|
69
|
+
}
|
|
70
|
+
function clearPendingNavigation() {
|
|
71
|
+
if (pendingNavigation === null)
|
|
72
|
+
return;
|
|
73
|
+
clearTimeout(pendingNavigation.timer);
|
|
74
|
+
pendingNavigation = null;
|
|
75
|
+
}
|
|
76
|
+
function sameBrowserLocation(destination, currentLocation) {
|
|
77
|
+
try {
|
|
78
|
+
const current = new URL(currentLocation, 'https://preview.invalid');
|
|
79
|
+
const target = new URL(destination, current);
|
|
80
|
+
// Pages Router uses this private revision only to force a data load.
|
|
81
|
+
current.searchParams.delete('_ss_refresh');
|
|
82
|
+
target.searchParams.delete('_ss_refresh');
|
|
83
|
+
return (target.pathname === current.pathname &&
|
|
84
|
+
target.search === current.search &&
|
|
85
|
+
target.hash === current.hash);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function _resetPreviewRefreshCoordinatorForTests() {
|
|
92
|
+
cancelPreviewRefreshCoordination();
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PREVIEW_READ_UNAVAILABLE_DIGEST = "storystick.preview.read-unavailable.v1";
|
|
2
|
+
export declare const PREVIEW_CONTEXT_SUPERSEDED_DIGEST = "storystick.preview.context-superseded.v1";
|
|
3
|
+
export declare const CUSTOMER_RENDER_ERROR = "runtime.customer_render_failed";
|
|
4
|
+
export declare const PREVIEW_READ_UNAVAILABLE = "runtime.preview_read_unavailable";
|
|
5
|
+
export declare function previewReadUnavailableDigest(retryAfterMs: number | undefined, now: number): string;
|
|
6
|
+
export declare function previewRenderError(digest: string | undefined): {
|
|
7
|
+
retryNotBefore?: number | undefined;
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
retryable: boolean;
|
|
11
|
+
};
|