@doenet/doenetml-iframe 0.7.20-dev.324 → 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 CHANGED
@@ -71,6 +71,91 @@ latest editor buffer:
71
71
 
72
72
  ## DoenetViewer
73
73
 
74
+ ### Prop changes and iframe reloads
75
+
76
+ Changing props on a mounted `<DoenetViewer>` does **not** reload the iframe:
77
+ the wrapper pushes changes into the already-loaded iframe as messages, and
78
+ the inner viewer applies them with the same semantics as the in-process
79
+ `<DoenetViewer>` from `@doenet/doenetml`. In particular:
80
+
81
+ | Prop change | Effect |
82
+ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
83
+ | `render`, `darkMode`, `flags`, `answerResponseCounts`, callbacks, … | applied live, no reload (flipping `render` false→true starts the document in the already-loaded realm) |
84
+ | `doenetML`, `activityId`, `docId`, `attemptNumber`, `requestedVariantIndex` | the document's core re-initializes **inside the same iframe realm** — the multi-MB bundle is not re-parsed |
85
+ | `initialState`, `forceDisable` and the other `force*` props, `userId` | read at (re-)initialization only, exactly like the in-process viewer — change `docId`/`attemptNumber` or remount via `key=` to apply |
86
+ | `standaloneUrl`, `cssUrl`, `doenetmlVersion` (or a version change detected in `doenetML`), `useSharedCoreWorker` | a different bundle/realm is required, so the iframe reloads |
87
+
88
+ To force a full remount (fresh realm and worker), change the component's
89
+ React `key`.
90
+
91
+ Function props (callbacks) are forwarded across the iframe boundary via
92
+ Comlink proxies and always follow the latest identity passed — parents may
93
+ pass inline arrow functions freely; identity churn does not re-render the
94
+ inner viewer.
95
+
96
+ > **Note:** in-place updates require a standalone bundle ≥ 0.7.18. When an
97
+ > older version is pinned (via `doenetmlVersion` or detected from the
98
+ > document's `xmlns`), the wrapper falls back to its historical behavior of
99
+ > reloading the iframe on any prop change.
100
+
101
+ ### Windowed mounting (`mountPolicy`)
102
+
103
+ Pages that embed many viewers (assignment pages, textbook chapters) pay
104
+ memory for every mounted iframe, whether or not the student can see it. The
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.
114
+
115
+ ```tsx
116
+ <DoenetViewer
117
+ doenetML={doenetML}
118
+ flags={{ allowSaveState: true }}
119
+ mountPolicy={{ mode: "windowed", maxLiveViewers: 3 }}
120
+ />
121
+ ```
122
+
123
+ - `maxLiveViewers` (default 3) — page-wide budget, shared by every windowed
124
+ viewer (the smallest value wins when viewers disagree). The budget is
125
+ soft: currently-visible viewers are never parked, even over budget.
126
+ - `visibleMargin` (default `"1000px"`) — how far outside the viewport a
127
+ viewer still counts as visible (the `IntersectionObserver` rootMargin).
128
+ - `parkDelayMs` (default 2000) — how long a viewer must stay off-screen
129
+ before it may be parked (debounce against scroll flicker).
130
+ - `flushTimeoutMs` (default 5000) — how long to wait for the pre-park state
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.
137
+
138
+ **Parking requires a persistence path** so no student work can be lost:
139
+ either `flags.allowSaveState` (the wrapper snapshots the flushed
140
+ `reportScoreAndState` and seeds `initialState` on restore) or
141
+ `flags.allowLocalState` (IndexedDB restores on reboot). Windowed viewers
142
+ with neither flag still mount lazily but, once booted, always stay live
143
+ (a console warning points this out).
144
+
145
+ Notes:
146
+
147
+ - Eviction is least-recently-visible first.
148
+ - While parked, a viewer emits no reports (its state was flushed at park
149
+ time). A host `SPLICE.flushState` broadcast is answered by the wrapper on
150
+ the parked viewer's behalf, so pre-navigation flush round-trips don't
151
+ hang.
152
+ - When `requestedVariantIndex` is not specified, windowed viewers pin a
153
+ random variant once per mount so a restore cannot reroll the document.
154
+ - Restoring pays a fresh iframe boot (bundle evaluation + core boot, ~1–2 s
155
+ warm). Pair with `useSharedCoreWorker` to make both live and restored
156
+ viewers cheaper.
157
+ - `mountPolicy` is read at mount; changing it afterwards is not supported.
158
+
74
159
  ### Host message protocol (SPLICE)
75
160
 
76
161
  The viewer exchanges JSON messages with the host page via `postMessage`.
package/index.d.ts CHANGED
@@ -70,12 +70,23 @@ declare type DoenetEditorProps = Omit<React.ComponentProps<typeof DoenetEditor_2
70
70
  * Render Doenet viewer constrained to an iframe. A URL pointing to a version of DoenetML
71
71
  * standalone must be provided (along with a URL to the corresponding CSS file).
72
72
  *
73
- * Parameters being passed to the underlying DoenetML component are passed via the `DoenetViewerProps` prop.
74
- * However, only serializable parameters may be passed. E.g., callbacks **cannot** be passed to the underlying
75
- * DoenetML component. Instead you must use the message passing interface of `DoenetViewer` to communicate
76
- * with the underlying DoenetML component.
73
+ * Parameters for the underlying `DoenetViewer` component are passed via props.
74
+ * Serializable prop changes after mount are pushed into the iframe as
75
+ * messages and applied in place with the same semantics as the in-process
76
+ * `<DoenetViewer>`: e.g. flipping `render` starts the document without a
77
+ * reload, and changing `doenetML` (or `activityId`/`docId`/`attemptNumber`/
78
+ * `requestedVariantIndex`) re-initializes the document's core inside the
79
+ * same iframe realm — the multi-MB standalone bundle is not re-parsed. Only
80
+ * a change of the bundle itself (`standaloneUrl`/`cssUrl`/`doenetmlVersion`,
81
+ * a version change detected in `doenetML`, or `useSharedCoreWorker`) reloads
82
+ * the iframe. To force a full remount instead, change the component's `key`.
83
+ *
84
+ * Function props are forwarded across the iframe boundary via Comlink
85
+ * proxies and follow the latest identity passed. (Bundles older than
86
+ * v0.7.18 cannot re-render in place; for those the wrapper falls back to
87
+ * reloading the iframe on any prop change, its historical behavior.)
77
88
  */
78
- export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
89
+ export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, mountPolicy, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
79
90
 
80
91
  export declare type DoenetViewerIframeProps = DoenetViewerProps & {
81
92
  doenetML: string;
@@ -111,6 +122,24 @@ export declare type DoenetViewerIframeProps = DoenetViewerProps & {
111
122
  * quarantined so retries boot fresh ones). Default off.
112
123
  */
113
124
  useSharedCoreWorker?: boolean;
125
+ /**
126
+ * Opt-in windowed mounting (#1441, stream B): keep at most
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
137
+ * `flags.allowLocalState` (IndexedDB restores on reboot); otherwise the
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).
141
+ */
142
+ mountPolicy?: MountPolicy;
114
143
  };
115
144
 
116
145
  declare type DoenetViewerProps = Omit<React.ComponentProps<typeof DoenetViewer_2>, "doenetML" | "externalVirtualKeyboardProvided">;
@@ -121,6 +150,16 @@ export { getMediaLicenseDisplay }
121
150
 
122
151
  export { getMediaLicenseInfo }
123
152
 
153
+ /** Snapshot for tests and diagnostics. */
154
+ export declare function getViewerLifecycleStats(): {
155
+ registered: number;
156
+ live: number;
157
+ parking: number;
158
+ parked: number;
159
+ booting: number;
160
+ bootQueue: number;
161
+ };
162
+
124
163
  export { mathjaxConfig }
125
164
 
126
165
  export { MediaLicenseDisplay }
@@ -131,6 +170,71 @@ export { MediaLicenseKind }
131
170
 
132
171
  export { mediaLicenses }
133
172
 
173
+ /**
174
+ * Page-wide lifecycle manager for windowed `<DoenetViewer>` mounting
175
+ * (#1441, stream B).
176
+ *
177
+ * Viewers that opt in via the `mountPolicy` prop register here. The manager
178
+ * enforces a page-wide budget of live viewers: when more than
179
+ * `maxLiveViewers` are live at once, the least-recently-visible off-screen
180
+ * viewers are asked to park (flush their state and replace their iframe with
181
+ * a placeholder), so idle memory tracks what the user can see rather than
182
+ * how many documents the page embeds.
183
+ *
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
191
+ * the `DoenetViewer` wrapper, which registers callbacks here. Module-level
192
+ * state is intentional — the budget is shared by every windowed viewer on
193
+ * the page (same pattern as `shared-core-pool.ts`).
194
+ *
195
+ * Rules:
196
+ * - A currently-visible viewer is never parked (the budget is soft: if more
197
+ * than `maxLiveViewers` are visible at once, all of them stay live).
198
+ * - A viewer only becomes eligible to park after it has been off-screen for
199
+ * its `parkDelayMs` (debounce against scroll flicker).
200
+ * - Viewers whose `canPark` returns false (no persistence path — parking
201
+ * would lose student work) are never parked.
202
+ * - Eviction order is least-recently-visible first.
203
+ */
204
+ export declare type MountPolicy = {
205
+ /** The only mode currently defined. */
206
+ mode: "windowed";
207
+ /**
208
+ * Page-wide budget of simultaneously live (iframe-mounted) viewers.
209
+ * When viewers on the same page specify different values, the smallest
210
+ * wins. Default 3.
211
+ */
212
+ maxLiveViewers?: number;
213
+ /**
214
+ * `rootMargin` for the visibility observer — how far outside the
215
+ * viewport a viewer still counts as "visible". Default "1000px".
216
+ */
217
+ visibleMargin?: string;
218
+ /**
219
+ * How long to wait for the viewer to acknowledge the pre-park state
220
+ * flush before parking anyway. Default 5000.
221
+ */
222
+ flushTimeoutMs?: number;
223
+ /**
224
+ * How long a viewer must be continuously off-screen before it may be
225
+ * parked. Default 2000.
226
+ */
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;
236
+ };
237
+
134
238
  export declare const version: string;
135
239
 
136
240
  export { WarningRecord }