@doenet/doenetml-iframe 0.7.20-dev.326 → 0.7.20-dev.327
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/README.md +16 -8
- package/index.d.ts +30 -11
- package/index.js +130 -13
- package/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,13 +102,15 @@ inner viewer.
|
|
|
102
102
|
|
|
103
103
|
Pages that embed many viewers (assignment pages, textbook chapters) pay
|
|
104
104
|
memory for every mounted iframe, whether or not the student can see it. The
|
|
105
|
-
opt-in `mountPolicy` prop bounds this:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
opt-in `mountPolicy` prop bounds this: windowed viewers **start as
|
|
106
|
+
placeholders and only create their iframe when they come near the viewport**
|
|
107
|
+
(an off-screen viewer never boots at all), simultaneous boots are capped
|
|
108
|
+
page-wide, and at most `maxLiveViewers` viewers stay live. Off-screen
|
|
109
|
+
viewers beyond the budget are **parked** — their state is flushed (via the
|
|
110
|
+
`SPLICE.flushState` machinery) and their iframe is replaced by a placeholder
|
|
111
|
+
of the same height, so the page layout doesn't shift. Scrolling a parked
|
|
112
|
+
viewer back near the viewport restores it, seeded with the flushed state:
|
|
113
|
+
typed work survives the round trip with no user interaction.
|
|
112
114
|
|
|
113
115
|
```tsx
|
|
114
116
|
<DoenetViewer
|
|
@@ -127,12 +129,18 @@ user interaction.
|
|
|
127
129
|
before it may be parked (debounce against scroll flicker).
|
|
128
130
|
- `flushTimeoutMs` (default 5000) — how long to wait for the pre-park state
|
|
129
131
|
flush to be acknowledged before parking anyway.
|
|
132
|
+
- `maxConcurrentBoots` (default 2) — page-wide cap on how many windowed
|
|
133
|
+
viewers may be booting their iframe realm at once (each boot parses the
|
|
134
|
+
multi-MB standalone bundle and starts a core worker). Additional viewers
|
|
135
|
+
wait for a slot, visible-first — this removes the initialization stampede
|
|
136
|
+
when a page with many activities loads.
|
|
130
137
|
|
|
131
138
|
**Parking requires a persistence path** so no student work can be lost:
|
|
132
139
|
either `flags.allowSaveState` (the wrapper snapshots the flushed
|
|
133
140
|
`reportScoreAndState` and seeds `initialState` on restore) or
|
|
134
141
|
`flags.allowLocalState` (IndexedDB restores on reboot). Windowed viewers
|
|
135
|
-
with neither flag
|
|
142
|
+
with neither flag still mount lazily but, once booted, always stay live
|
|
143
|
+
(a console warning points this out).
|
|
136
144
|
|
|
137
145
|
Notes:
|
|
138
146
|
|
package/index.d.ts
CHANGED
|
@@ -124,16 +124,20 @@ export declare type DoenetViewerIframeProps = DoenetViewerProps & {
|
|
|
124
124
|
useSharedCoreWorker?: boolean;
|
|
125
125
|
/**
|
|
126
126
|
* Opt-in windowed mounting (#1441, stream B): keep at most
|
|
127
|
-
* `maxLiveViewers` viewers live on the page.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* `
|
|
133
|
-
*
|
|
127
|
+
* `maxLiveViewers` viewers live on the page. Windowed viewers start as
|
|
128
|
+
* fixed-height placeholders and only create their iframe when they come
|
|
129
|
+
* near the viewport AND a boot slot is free (`maxConcurrentBoots` caps
|
|
130
|
+
* simultaneous realm boots page-wide — an off-screen viewer never boots
|
|
131
|
+
* at all). Off-screen viewers beyond the budget are *parked* — their
|
|
132
|
+
* state is flushed (`SPLICE.flushState`) and their iframe is replaced
|
|
133
|
+
* by the placeholder again — and restored when scrolled back near the
|
|
134
|
+
* viewport. Parking requires a persistence path: it only activates for
|
|
135
|
+
* viewers with `flags.allowSaveState` (the wrapper snapshots the
|
|
136
|
+
* flushed `reportScoreAndState` and seeds `initialState` on restore) or
|
|
134
137
|
* `flags.allowLocalState` (IndexedDB restores on reboot); otherwise the
|
|
135
|
-
* viewer always stays live. The policy is
|
|
136
|
-
* afterwards is not supported. Default off
|
|
138
|
+
* viewer boots on visibility but then always stays live. The policy is
|
|
139
|
+
* read at mount; changing it afterwards is not supported. Default off
|
|
140
|
+
* (no prop = today's behavior).
|
|
137
141
|
*/
|
|
138
142
|
mountPolicy?: MountPolicy;
|
|
139
143
|
};
|
|
@@ -152,6 +156,8 @@ export declare function getViewerLifecycleStats(): {
|
|
|
152
156
|
live: number;
|
|
153
157
|
parking: number;
|
|
154
158
|
parked: number;
|
|
159
|
+
booting: number;
|
|
160
|
+
bootQueue: number;
|
|
155
161
|
};
|
|
156
162
|
|
|
157
163
|
export { mathjaxConfig }
|
|
@@ -175,8 +181,13 @@ export { mediaLicenses }
|
|
|
175
181
|
* a placeholder), so idle memory tracks what the user can see rather than
|
|
176
182
|
* how many documents the page embeds.
|
|
177
183
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
184
|
+
* It also gates *when* a lazily-mounted viewer may boot its iframe realm: a
|
|
185
|
+
* page-wide boot-slot semaphore (`maxConcurrentBoots`) caps how many viewers
|
|
186
|
+
* evaluate the multi-MB standalone bundle at once, serving visible viewers
|
|
187
|
+
* first (see `requestBootSlot`).
|
|
188
|
+
*
|
|
189
|
+
* This module is pure policy: *when* to park and *when* to boot. The
|
|
190
|
+
* mechanics (flushing state, creating/swapping the iframe, restoring) live in
|
|
180
191
|
* the `DoenetViewer` wrapper, which registers callbacks here. Module-level
|
|
181
192
|
* state is intentional — the budget is shared by every windowed viewer on
|
|
182
193
|
* the page (same pattern as `shared-core-pool.ts`).
|
|
@@ -214,6 +225,14 @@ export declare type MountPolicy = {
|
|
|
214
225
|
* parked. Default 2000.
|
|
215
226
|
*/
|
|
216
227
|
parkDelayMs?: number;
|
|
228
|
+
/**
|
|
229
|
+
* Page-wide cap on how many windowed viewers may be *booting* their
|
|
230
|
+
* iframe realm at once (each boot parses the multi-MB standalone bundle
|
|
231
|
+
* and starts a core worker — the initialization stampede #1439
|
|
232
|
+
* targets). Additional viewers wait for a slot, visible-first. As with
|
|
233
|
+
* `maxLiveViewers`, the smallest value across viewers wins. Default 2.
|
|
234
|
+
*/
|
|
235
|
+
maxConcurrentBoots?: number;
|
|
217
236
|
};
|
|
218
237
|
|
|
219
238
|
export declare const version: string;
|
package/index.js
CHANGED
|
@@ -25858,6 +25858,9 @@ function createIframeBodyOpenTag(darkMode) {
|
|
|
25858
25858
|
}
|
|
25859
25859
|
function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneUrl, cssUrl, useSharedCoreWorker = false) {
|
|
25860
25860
|
const doenetViewerPropsSpecified = Object.keys(doenetViewerProps);
|
|
25861
|
+
if (!doenetViewerPropsSpecified.includes("initializedCallback")) {
|
|
25862
|
+
doenetViewerPropsSpecified.push("initializedCallback");
|
|
25863
|
+
}
|
|
25861
25864
|
return `
|
|
25862
25865
|
<html style="overflow:hidden">
|
|
25863
25866
|
${createIframeHead(standaloneUrl, cssUrl)}
|
|
@@ -26030,19 +26033,27 @@ const DEFAULT_MAX_LIVE_VIEWERS = 3;
|
|
|
26030
26033
|
const DEFAULT_VISIBLE_MARGIN = "1000px";
|
|
26031
26034
|
const DEFAULT_FLUSH_TIMEOUT_MS = 5e3;
|
|
26032
26035
|
const DEFAULT_PARK_DELAY_MS = 2e3;
|
|
26036
|
+
const DEFAULT_MAX_CONCURRENT_BOOTS = 2;
|
|
26037
|
+
const BOOT_SLOT_WATCHDOG_MS = 9e4;
|
|
26033
26038
|
const records = /* @__PURE__ */ new Map();
|
|
26034
26039
|
let visibleOrderCounter = 0;
|
|
26035
26040
|
let effectiveMaxLive = null;
|
|
26036
26041
|
let warnedMixedBudgets = false;
|
|
26037
26042
|
let evaluateTimerId = null;
|
|
26038
26043
|
let evaluateTimerAt = Infinity;
|
|
26044
|
+
let effectiveMaxBoots = null;
|
|
26045
|
+
const bootSlotHolders = /* @__PURE__ */ new Set();
|
|
26046
|
+
let bootRequestCounter = 0;
|
|
26047
|
+
const bootQueue = [];
|
|
26039
26048
|
function registerWindowedViewer({
|
|
26040
26049
|
id: id2,
|
|
26041
26050
|
maxLiveViewers,
|
|
26051
|
+
maxConcurrentBoots,
|
|
26042
26052
|
parkDelayMs,
|
|
26043
26053
|
canPark,
|
|
26044
26054
|
requestPark
|
|
26045
26055
|
}) {
|
|
26056
|
+
effectiveMaxBoots = effectiveMaxBoots === null ? maxConcurrentBoots : Math.min(effectiveMaxBoots, maxConcurrentBoots);
|
|
26046
26057
|
if (effectiveMaxLive !== null && effectiveMaxLive !== maxLiveViewers && !warnedMixedBudgets) {
|
|
26047
26058
|
warnedMixedBudgets = true;
|
|
26048
26059
|
console.warn(
|
|
@@ -26063,9 +26074,47 @@ function registerWindowedViewer({
|
|
|
26063
26074
|
scheduleEvaluate(parkDelayMs);
|
|
26064
26075
|
return () => {
|
|
26065
26076
|
records.delete(id2);
|
|
26077
|
+
cancelBootRequest(id2);
|
|
26078
|
+
releaseBootSlot(id2);
|
|
26066
26079
|
scheduleEvaluate(0);
|
|
26067
26080
|
};
|
|
26068
26081
|
}
|
|
26082
|
+
function requestBootSlot(id2, grant) {
|
|
26083
|
+
if (bootSlotHolders.has(id2) || bootQueue.some((r2) => r2.id === id2)) {
|
|
26084
|
+
return;
|
|
26085
|
+
}
|
|
26086
|
+
bootQueue.push({ id: id2, requestOrder: ++bootRequestCounter, grant });
|
|
26087
|
+
pumpBootQueue();
|
|
26088
|
+
}
|
|
26089
|
+
function cancelBootRequest(id2) {
|
|
26090
|
+
const idx = bootQueue.findIndex((r2) => r2.id === id2);
|
|
26091
|
+
if (idx !== -1) {
|
|
26092
|
+
bootQueue.splice(idx, 1);
|
|
26093
|
+
}
|
|
26094
|
+
}
|
|
26095
|
+
function releaseBootSlot(id2) {
|
|
26096
|
+
if (bootSlotHolders.delete(id2)) {
|
|
26097
|
+
pumpBootQueue();
|
|
26098
|
+
}
|
|
26099
|
+
}
|
|
26100
|
+
function pumpBootQueue() {
|
|
26101
|
+
const max2 = effectiveMaxBoots ?? Infinity;
|
|
26102
|
+
while (bootQueue.length > 0 && bootSlotHolders.size < max2) {
|
|
26103
|
+
let best = 0;
|
|
26104
|
+
for (let i2 = 1; i2 < bootQueue.length; i2++) {
|
|
26105
|
+
const bestVisible = Boolean(
|
|
26106
|
+
records.get(bootQueue[best].id)?.visible
|
|
26107
|
+
);
|
|
26108
|
+
const thisVisible = Boolean(records.get(bootQueue[i2].id)?.visible);
|
|
26109
|
+
if (thisVisible && !bestVisible || thisVisible === bestVisible && bootQueue[i2].requestOrder < bootQueue[best].requestOrder) {
|
|
26110
|
+
best = i2;
|
|
26111
|
+
}
|
|
26112
|
+
}
|
|
26113
|
+
const [request] = bootQueue.splice(best, 1);
|
|
26114
|
+
bootSlotHolders.add(request.id);
|
|
26115
|
+
request.grant();
|
|
26116
|
+
}
|
|
26117
|
+
}
|
|
26069
26118
|
function setViewerVisibility(id2, visible) {
|
|
26070
26119
|
const record = records.get(id2);
|
|
26071
26120
|
if (!record || record.visible === visible) {
|
|
@@ -26099,7 +26148,14 @@ function getViewerLifecycleStats() {
|
|
|
26099
26148
|
parked++;
|
|
26100
26149
|
}
|
|
26101
26150
|
}
|
|
26102
|
-
return {
|
|
26151
|
+
return {
|
|
26152
|
+
registered: records.size,
|
|
26153
|
+
live,
|
|
26154
|
+
parking,
|
|
26155
|
+
parked,
|
|
26156
|
+
booting: bootSlotHolders.size,
|
|
26157
|
+
bootQueue: bootQueue.length
|
|
26158
|
+
};
|
|
26103
26159
|
}
|
|
26104
26160
|
function scheduleEvaluate(delayMs) {
|
|
26105
26161
|
const at = Date.now() + delayMs;
|
|
@@ -63306,7 +63362,7 @@ function ExternalVirtualKeyboard({
|
|
|
63306
63362
|
}
|
|
63307
63363
|
);
|
|
63308
63364
|
}
|
|
63309
|
-
const version = "0.7.20-dev.
|
|
63365
|
+
const version = "0.7.20-dev.327";
|
|
63310
63366
|
const latestDoenetmlVersion = version;
|
|
63311
63367
|
function subscribeToPinnedTheme() {
|
|
63312
63368
|
return () => {
|
|
@@ -63361,9 +63417,9 @@ function DoenetViewer({
|
|
|
63361
63417
|
const doenetMLRef = React__default.useRef(doenetML);
|
|
63362
63418
|
doenetMLRef.current = doenetML;
|
|
63363
63419
|
const windowed = mountPolicy?.mode === "windowed";
|
|
63364
|
-
const [parked, setParked] = React__default.useState(
|
|
63420
|
+
const [parked, setParked] = React__default.useState(windowed);
|
|
63365
63421
|
const [parkGeneration, setParkGeneration] = React__default.useState(0);
|
|
63366
|
-
const parkedRef = React__default.useRef(
|
|
63422
|
+
const parkedRef = React__default.useRef(windowed);
|
|
63367
63423
|
const parkingRef = React__default.useRef(false);
|
|
63368
63424
|
const visibleRef = React__default.useRef(false);
|
|
63369
63425
|
const containerRef = React__default.useRef(null);
|
|
@@ -63379,6 +63435,40 @@ function DoenetViewer({
|
|
|
63379
63435
|
if (windowed && pinnedVariantIndexRef.current === null) {
|
|
63380
63436
|
pinnedVariantIndexRef.current = doenetViewerProps.requestedVariantIndex ?? Math.floor(Math.random() * 1e6) + 1;
|
|
63381
63437
|
}
|
|
63438
|
+
const bootWatchdogRef = React__default.useRef(
|
|
63439
|
+
null
|
|
63440
|
+
);
|
|
63441
|
+
function clearBootWatchdog() {
|
|
63442
|
+
if (bootWatchdogRef.current !== null) {
|
|
63443
|
+
clearTimeout(bootWatchdogRef.current);
|
|
63444
|
+
bootWatchdogRef.current = null;
|
|
63445
|
+
}
|
|
63446
|
+
}
|
|
63447
|
+
function relinquishBootSlot() {
|
|
63448
|
+
clearBootWatchdog();
|
|
63449
|
+
releaseBootSlot(id2);
|
|
63450
|
+
}
|
|
63451
|
+
const composedInitializedCallbackRef = React__default.useRef(null);
|
|
63452
|
+
if (composedInitializedCallbackRef.current === null) {
|
|
63453
|
+
composedInitializedCallbackRef.current = (arg) => {
|
|
63454
|
+
relinquishBootSlot();
|
|
63455
|
+
doenetViewerPropsRef.current.initializedCallback?.(arg);
|
|
63456
|
+
};
|
|
63457
|
+
}
|
|
63458
|
+
function functionPropToProxy(key, prop) {
|
|
63459
|
+
if (windowed && key === "initializedCallback") {
|
|
63460
|
+
return composedInitializedCallbackRef.current;
|
|
63461
|
+
}
|
|
63462
|
+
return prop;
|
|
63463
|
+
}
|
|
63464
|
+
function appendComposedInitializedCallback(proxiedFunctions, hostFunctions) {
|
|
63465
|
+
if (windowed && !("initializedCallback" in hostFunctions)) {
|
|
63466
|
+
proxiedFunctions.push("initializedCallback");
|
|
63467
|
+
proxiedFunctions.push(
|
|
63468
|
+
proxy(composedInitializedCallbackRef.current)
|
|
63469
|
+
);
|
|
63470
|
+
}
|
|
63471
|
+
}
|
|
63382
63472
|
const [height, setHeight] = React__default.useState("500px");
|
|
63383
63473
|
const [inErrorState, setInErrorState] = React__default.useState(null);
|
|
63384
63474
|
const [ignoreDetectedVersion, setIgnoreDetectedVersion] = React__default.useState(false);
|
|
@@ -63540,6 +63630,9 @@ function DoenetViewer({
|
|
|
63540
63630
|
});
|
|
63541
63631
|
}
|
|
63542
63632
|
if (data.error) {
|
|
63633
|
+
if (windowed) {
|
|
63634
|
+
relinquishBootSlot();
|
|
63635
|
+
}
|
|
63543
63636
|
return setInErrorState(data.error);
|
|
63544
63637
|
} else if (data.iframeReady) {
|
|
63545
63638
|
if (ref.current) {
|
|
@@ -63553,9 +63646,15 @@ function DoenetViewer({
|
|
|
63553
63646
|
if (typeof prop === "function") {
|
|
63554
63647
|
registeredFunctions[key] = prop;
|
|
63555
63648
|
proxiedFunctions.push(key);
|
|
63556
|
-
proxiedFunctions.push(
|
|
63649
|
+
proxiedFunctions.push(
|
|
63650
|
+
proxy(functionPropToProxy(key, prop))
|
|
63651
|
+
);
|
|
63557
63652
|
}
|
|
63558
63653
|
}
|
|
63654
|
+
appendComposedInitializedCallback(
|
|
63655
|
+
proxiedFunctions,
|
|
63656
|
+
registeredFunctions
|
|
63657
|
+
);
|
|
63559
63658
|
viewerIframe.renderViewerWithFunctionProps(...proxiedFunctions).catch(
|
|
63560
63659
|
logComlinkError(
|
|
63561
63660
|
"DoenetViewer",
|
|
@@ -63572,9 +63671,7 @@ function DoenetViewer({
|
|
|
63572
63671
|
}
|
|
63573
63672
|
}
|
|
63574
63673
|
};
|
|
63575
|
-
|
|
63576
|
-
window.addEventListener("message", listener);
|
|
63577
|
-
}
|
|
63674
|
+
window.addEventListener("message", listener);
|
|
63578
63675
|
return () => {
|
|
63579
63676
|
window.removeEventListener("message", listener);
|
|
63580
63677
|
};
|
|
@@ -63632,8 +63729,9 @@ function DoenetViewer({
|
|
|
63632
63729
|
const proxiedFunctions = [];
|
|
63633
63730
|
for (const [key, val] of Object.entries(current)) {
|
|
63634
63731
|
proxiedFunctions.push(key);
|
|
63635
|
-
proxiedFunctions.push(proxy(val));
|
|
63732
|
+
proxiedFunctions.push(proxy(functionPropToProxy(key, val)));
|
|
63636
63733
|
}
|
|
63734
|
+
appendComposedInitializedCallback(proxiedFunctions, current);
|
|
63637
63735
|
const action = (remote) => {
|
|
63638
63736
|
remote.updateViewerFunctionProps(...proxiedFunctions).catch(
|
|
63639
63737
|
logComlinkError(
|
|
@@ -63717,6 +63815,7 @@ function DoenetViewer({
|
|
|
63717
63815
|
function finishPark() {
|
|
63718
63816
|
viewerIframeRef.current = null;
|
|
63719
63817
|
destroySharedCoresForViewer(id2);
|
|
63818
|
+
relinquishBootSlot();
|
|
63720
63819
|
parkedRef.current = true;
|
|
63721
63820
|
setParked(true);
|
|
63722
63821
|
notifyViewerState(id2, "parked");
|
|
@@ -63725,10 +63824,21 @@ function DoenetViewer({
|
|
|
63725
63824
|
if (!parkedRef.current) {
|
|
63726
63825
|
return;
|
|
63727
63826
|
}
|
|
63728
|
-
|
|
63729
|
-
|
|
63730
|
-
|
|
63731
|
-
|
|
63827
|
+
requestBootSlot(id2, () => {
|
|
63828
|
+
if (!parkedRef.current || !visibleRef.current) {
|
|
63829
|
+
releaseBootSlot(id2);
|
|
63830
|
+
return;
|
|
63831
|
+
}
|
|
63832
|
+
parkedRef.current = false;
|
|
63833
|
+
setParked(false);
|
|
63834
|
+
setParkGeneration((generation) => generation + 1);
|
|
63835
|
+
notifyViewerState(id2, "live");
|
|
63836
|
+
clearBootWatchdog();
|
|
63837
|
+
bootWatchdogRef.current = setTimeout(() => {
|
|
63838
|
+
bootWatchdogRef.current = null;
|
|
63839
|
+
releaseBootSlot(id2);
|
|
63840
|
+
}, BOOT_SLOT_WATCHDOG_MS);
|
|
63841
|
+
});
|
|
63732
63842
|
}
|
|
63733
63843
|
React__default.useEffect(() => {
|
|
63734
63844
|
if (!windowed || !containerRef.current) {
|
|
@@ -63741,6 +63851,8 @@ function DoenetViewer({
|
|
|
63741
63851
|
setViewerVisibility(id2, entry.isIntersecting);
|
|
63742
63852
|
if (entry.isIntersecting && parkedRef.current) {
|
|
63743
63853
|
unpark();
|
|
63854
|
+
} else if (!entry.isIntersecting) {
|
|
63855
|
+
cancelBootRequest(id2);
|
|
63744
63856
|
}
|
|
63745
63857
|
}
|
|
63746
63858
|
},
|
|
@@ -63765,12 +63877,17 @@ function DoenetViewer({
|
|
|
63765
63877
|
const unregister = registerWindowedViewer({
|
|
63766
63878
|
id: id2,
|
|
63767
63879
|
maxLiveViewers: mountPolicy?.maxLiveViewers ?? DEFAULT_MAX_LIVE_VIEWERS,
|
|
63880
|
+
maxConcurrentBoots: mountPolicy?.maxConcurrentBoots ?? DEFAULT_MAX_CONCURRENT_BOOTS,
|
|
63768
63881
|
parkDelayMs: mountPolicy?.parkDelayMs ?? DEFAULT_PARK_DELAY_MS,
|
|
63769
63882
|
canPark,
|
|
63770
63883
|
requestPark: beginPark
|
|
63771
63884
|
});
|
|
63885
|
+
if (parkedRef.current) {
|
|
63886
|
+
notifyViewerState(id2, "parked");
|
|
63887
|
+
}
|
|
63772
63888
|
return () => {
|
|
63773
63889
|
parkFlushCleanupRef.current?.();
|
|
63890
|
+
clearBootWatchdog();
|
|
63774
63891
|
unregister();
|
|
63775
63892
|
};
|
|
63776
63893
|
}, [windowed]);
|