@dfosco/storyboard-react 4.0.0-beta.21 → 4.0.0-beta.22
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/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
7
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
6
|
+
"@dfosco/storyboard-core": "4.0.0-beta.22",
|
|
7
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.22",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1",
|
|
@@ -344,6 +344,7 @@ export default function CanvasPage({ name, siblingPages = [], canvasMeta = null
|
|
|
344
344
|
const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
|
|
345
345
|
const [snapEnabled, setSnapEnabled] = useState(canvas?.snapToGrid ?? false)
|
|
346
346
|
const [snapGridSize, setSnapGridSize] = useState(canvas?.gridSize || 40)
|
|
347
|
+
|
|
347
348
|
// Refs for snap settings (used by drop handler inside effect closure)
|
|
348
349
|
const snapEnabledRef = useRef(snapEnabled)
|
|
349
350
|
const snapGridSizeRef = useRef(snapGridSize)
|
|
@@ -5,6 +5,7 @@ import WidgetWrapper from './WidgetWrapper.jsx'
|
|
|
5
5
|
import { readProp, prototypeEmbedSchema } from './widgetProps.js'
|
|
6
6
|
import { getEmbedChromeVars } from './embedTheme.js'
|
|
7
7
|
import { uploadImage } from '../canvasApi.js'
|
|
8
|
+
import { useIframeQueue } from './useViewportEntry.js'
|
|
8
9
|
import styles from './PrototypeEmbed.module.css'
|
|
9
10
|
import overlayStyles from './embedOverlay.module.css'
|
|
10
11
|
|
|
@@ -68,12 +69,29 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
68
69
|
const validSnapshotDark = snapshotMatchesWidget(snapshotDark) ? snapshotDark : null
|
|
69
70
|
const currentSnapshot = canvasTheme?.startsWith('dark') ? validSnapshotDark : validSnapshotLight
|
|
70
71
|
const hasSnapshot = !!currentSnapshot
|
|
71
|
-
|
|
72
|
+
|
|
73
|
+
// Sequential iframe queue — prevents stampede when many embeds lack snapshots.
|
|
74
|
+
// Widgets with snapshots skip the queue entirely; others load one at a time.
|
|
75
|
+
const { ready: queueReady, releaseSlot } = useIframeQueue(hasSnapshot || isExternal)
|
|
76
|
+
const [preloadIframe, setPreloadIframe] = useState(hasSnapshot || isExternal)
|
|
72
77
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
|
73
|
-
const [showIframe, setShowIframe] = useState(
|
|
78
|
+
const [showIframe, setShowIframe] = useState(hasSnapshot || isExternal)
|
|
74
79
|
const [showSpinner, setShowSpinner] = useState(false)
|
|
75
80
|
const capturingRef = useRef(false)
|
|
76
81
|
|
|
82
|
+
// Start loading when the queue grants this widget a slot
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (queueReady && !preloadIframe) {
|
|
85
|
+
setPreloadIframe(true)
|
|
86
|
+
setShowIframe(true)
|
|
87
|
+
}
|
|
88
|
+
}, [queueReady, preloadIframe])
|
|
89
|
+
|
|
90
|
+
// Release the queue slot once the iframe has loaded
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (iframeLoaded) releaseSlot()
|
|
93
|
+
}, [iframeLoaded, releaseSlot])
|
|
94
|
+
|
|
77
95
|
// Show spinner only after 500ms of loading
|
|
78
96
|
useEffect(() => {
|
|
79
97
|
if (showIframe && !iframeLoaded && hasSnapshot) {
|
|
@@ -17,6 +17,7 @@ import { createInspectorHighlighter } from '@dfosco/storyboard-core/inspector/hi
|
|
|
17
17
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
18
18
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
19
19
|
import { uploadImage } from '../canvasApi.js'
|
|
20
|
+
import { useIframeQueue } from './useViewportEntry.js'
|
|
20
21
|
import styles from './StoryWidget.module.css'
|
|
21
22
|
import overlayStyles from './embedOverlay.module.css'
|
|
22
23
|
|
|
@@ -111,12 +112,28 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
111
112
|
const validSnapshotDark = snapshotMatchesWidget(snapshotDark) ? snapshotDark : null
|
|
112
113
|
const currentSnapshot = isDark ? validSnapshotDark : validSnapshotLight
|
|
113
114
|
const hasSnapshot = !!currentSnapshot
|
|
114
|
-
|
|
115
|
+
|
|
116
|
+
// Sequential iframe queue — prevents stampede when many embeds lack snapshots.
|
|
117
|
+
const { ready: queueReady, releaseSlot } = useIframeQueue(hasSnapshot)
|
|
118
|
+
const [preloadIframe, setPreloadIframe] = useState(hasSnapshot)
|
|
115
119
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
|
116
|
-
const [showIframe, setShowIframe] = useState(
|
|
120
|
+
const [showIframe, setShowIframe] = useState(hasSnapshot)
|
|
117
121
|
const [showSpinner, setShowSpinner] = useState(false)
|
|
118
122
|
const capturingRef = useRef(false)
|
|
119
123
|
|
|
124
|
+
// Start loading when the queue grants this widget a slot
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (queueReady && !preloadIframe) {
|
|
127
|
+
setPreloadIframe(true)
|
|
128
|
+
setShowIframe(true)
|
|
129
|
+
}
|
|
130
|
+
}, [queueReady, preloadIframe])
|
|
131
|
+
|
|
132
|
+
// Release the queue slot once the iframe has loaded
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (iframeLoaded) releaseSlot()
|
|
135
|
+
}, [iframeLoaded, releaseSlot])
|
|
136
|
+
|
|
120
137
|
// Show spinner only after 500ms of loading
|
|
121
138
|
useEffect(() => {
|
|
122
139
|
if (showIframe && !iframeLoaded && hasSnapshot) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sequential iframe loading queue.
|
|
5
|
+
*
|
|
6
|
+
* Embed widgets (prototype, story) that lack snapshots call `requestSlot()`
|
|
7
|
+
* which returns a promise that resolves when it's their turn to load.
|
|
8
|
+
* Only one iframe loads at a time — the next starts when the previous
|
|
9
|
+
* calls the returned `release()` function (or on timeout).
|
|
10
|
+
*
|
|
11
|
+
* This prevents the "iframe stampede" when many embeds lack snapshots
|
|
12
|
+
* and would otherwise all try to load simultaneously.
|
|
13
|
+
*/
|
|
14
|
+
const SLOT_TIMEOUT = 15_000
|
|
15
|
+
|
|
16
|
+
let _queue = []
|
|
17
|
+
let _active = false
|
|
18
|
+
|
|
19
|
+
function processQueue() {
|
|
20
|
+
if (_active || _queue.length === 0) return
|
|
21
|
+
_active = true
|
|
22
|
+
const { resolve } = _queue.shift()
|
|
23
|
+
|
|
24
|
+
let released = false
|
|
25
|
+
const release = () => {
|
|
26
|
+
if (released) return
|
|
27
|
+
released = true
|
|
28
|
+
_active = false
|
|
29
|
+
processQueue()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Safety timeout — if the iframe never signals load, release after 15s
|
|
33
|
+
setTimeout(release, SLOT_TIMEOUT)
|
|
34
|
+
resolve(release)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requestSlot() {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
_queue.push({ resolve })
|
|
40
|
+
processQueue()
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook for embed widgets that need sequential iframe loading.
|
|
46
|
+
*
|
|
47
|
+
* When the widget has a usable snapshot, this is a no-op (returns ready immediately).
|
|
48
|
+
* When no snapshot exists, it queues the widget and returns `ready: true` when
|
|
49
|
+
* it's this widget's turn.
|
|
50
|
+
*
|
|
51
|
+
* @param {boolean} hasUsableSnapshot - whether the widget has a working snapshot
|
|
52
|
+
* @returns {{ ready: boolean }}
|
|
53
|
+
*/
|
|
54
|
+
export function useIframeQueue(hasUsableSnapshot) {
|
|
55
|
+
const [ready, setReady] = useState(hasUsableSnapshot)
|
|
56
|
+
const releaseRef = useRef(null)
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (hasUsableSnapshot || ready) return
|
|
60
|
+
|
|
61
|
+
let cancelled = false
|
|
62
|
+
requestSlot().then((release) => {
|
|
63
|
+
if (cancelled) {
|
|
64
|
+
release()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
releaseRef.current = release
|
|
68
|
+
setReady(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true
|
|
73
|
+
releaseRef.current?.()
|
|
74
|
+
}
|
|
75
|
+
}, [hasUsableSnapshot]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
76
|
+
|
|
77
|
+
// Release the slot when the component unmounts
|
|
78
|
+
const releaseSlot = useCallback(() => {
|
|
79
|
+
releaseRef.current?.()
|
|
80
|
+
releaseRef.current = null
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
return { ready, releaseSlot }
|
|
84
|
+
}
|