@doenet/doenetml-iframe 0.7.20 → 0.7.21
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 +300 -0
- package/index.d.ts +162 -5
- package/index.js +46690 -45925
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/style.css +130 -4
package/README.md
CHANGED
|
@@ -69,6 +69,306 @@ latest editor buffer:
|
|
|
69
69
|
> surface a completion signal or error to the caller — failures from the
|
|
70
70
|
> underlying ComLink RPC are logged to the console rather than thrown.
|
|
71
71
|
|
|
72
|
+
## DoenetViewer
|
|
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 — once per page — points this out). Hosts that consume
|
|
144
|
+
reports through the `reportScoreAndStateCallback` prop are fully supported:
|
|
145
|
+
the wrapper captures the flushed report for its park snapshot before
|
|
146
|
+
forwarding it to the callback.
|
|
147
|
+
|
|
148
|
+
Parking also requires a standalone bundle new enough to acknowledge the
|
|
149
|
+
flush (v0.7.21+). A viewer pinned to an older `doenetmlVersion` mounts
|
|
150
|
+
lazily and obeys the boot cap, but is never parked. A host-specified
|
|
151
|
+
`standaloneUrl` is assumed modern (hosts shipping a custom URL control both
|
|
152
|
+
sides — note a `-dev.N` prerelease *version string* compares as its base
|
|
153
|
+
release, so dev-channel hosts should pin `standaloneUrl` rather than
|
|
154
|
+
`doenetmlVersion`).
|
|
155
|
+
|
|
156
|
+
#### `keepLive`: prefetching hosts
|
|
157
|
+
|
|
158
|
+
A windowed viewer inside a hidden (`display:none`) or far-off-screen
|
|
159
|
+
container never intersects the viewport, so it would stay parked forever.
|
|
160
|
+
A host that knows the viewer is about to be shown — e.g. a paginator
|
|
161
|
+
prefetching the pages adjacent to the current one — sets the dynamic
|
|
162
|
+
`keepLive` prop to treat it as visible: it boots eagerly (still subject to
|
|
163
|
+
`maxConcurrentBoots`, visible-first) and is never parked while the hint is
|
|
164
|
+
set. Clear the hint and the viewer becomes an ordinary windowed citizen
|
|
165
|
+
again (parked when off-screen and over budget).
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<DoenetViewer
|
|
169
|
+
doenetML={doenetML}
|
|
170
|
+
flags={{ allowSaveState: true }}
|
|
171
|
+
mountPolicy={{ mode: "windowed", maxLiveViewers: 3 }}
|
|
172
|
+
keepLive={Math.abs(itemIndex - currentIndex) <= 1}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Notes:
|
|
177
|
+
|
|
178
|
+
- Eviction is least-recently-visible first.
|
|
179
|
+
- While parked, a viewer emits no reports (its state was flushed at park
|
|
180
|
+
time). A host `SPLICE.flushState` broadcast is answered by the wrapper on
|
|
181
|
+
the parked viewer's behalf, so pre-navigation flush round-trips don't
|
|
182
|
+
hang.
|
|
183
|
+
- When `requestedVariantIndex` is not specified, windowed viewers pin a
|
|
184
|
+
random variant once per mount so a restore cannot reroll the document.
|
|
185
|
+
- Restoring pays a fresh iframe boot (bundle evaluation + core boot, ~1–2 s
|
|
186
|
+
warm). Pair with `useSharedCoreWorker` to make both live and restored
|
|
187
|
+
viewers cheaper.
|
|
188
|
+
- `mountPolicy` is read at mount; changing it afterwards is not supported.
|
|
189
|
+
|
|
190
|
+
### Host message protocol (SPLICE)
|
|
191
|
+
|
|
192
|
+
The viewer exchanges JSON messages with the host page via `postMessage`.
|
|
193
|
+
Viewer → host messages arrive on **your window** (the iframe posts to
|
|
194
|
+
`window.parent`, which is your page). Host → viewer requests are posted on
|
|
195
|
+
**your own window** too — the wrapper forwards these subjects into the
|
|
196
|
+
iframe: `SPLICE.getState.response`, `SPLICE.requestSolutionView.response`,
|
|
197
|
+
`SPLICE.submitAllAnswers`, and `SPLICE.flushState`. On a page with several
|
|
198
|
+
viewers, every viewer receives a forwarded request; correlate responses by
|
|
199
|
+
`activity_id`/`doc_id`/`message_id`.
|
|
200
|
+
|
|
201
|
+
The messages are documented in the subsections below:
|
|
202
|
+
|
|
203
|
+
| Subject | Direction | Purpose |
|
|
204
|
+
| ---------------------------------------------------------- | ------------- | ---------------------------------------------- |
|
|
205
|
+
| `SPLICE.reportScoreAndState` | viewer → host | periodic score/state saves |
|
|
206
|
+
| `SPLICE.getState` / `.response` | viewer ⇄ host | load saved state at boot |
|
|
207
|
+
| `SPLICE.flushState` / `.response` | host ⇄ viewer | on-demand state flush (lossless unmount) |
|
|
208
|
+
| `SPLICE.submitAllAnswers` / `.response` | host ⇄ viewer | submit every answer in the document |
|
|
209
|
+
| `SPLICE.requestSolutionView` / `.response` | viewer ⇄ host | permission gate for viewing solutions |
|
|
210
|
+
| `SPLICE.sendEvent` | viewer → host | analytics/event stream |
|
|
211
|
+
|
|
212
|
+
### Saving and restoring state (lossless unmount)
|
|
213
|
+
|
|
214
|
+
The viewer reports the student's serialized document state to the host as
|
|
215
|
+
they work: it posts `SPLICE.reportScoreAndState` messages to the host
|
|
216
|
+
window (the wrapper's iframe posts to `window.parent`, which is your page):
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
window.addEventListener("message", (e) => {
|
|
220
|
+
if (e.data?.subject === "SPLICE.reportScoreAndState") {
|
|
221
|
+
// e.data.state — serialized document state (opaque; store as-is)
|
|
222
|
+
// e.data.score, e.data.activity_id, e.data.doc_id
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
To restore, remount the viewer with the saved state:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<DoenetViewer
|
|
231
|
+
doenetML={doenetML}
|
|
232
|
+
flags={{ allowLoadState: true }}
|
|
233
|
+
initialState={savedState}
|
|
234
|
+
/>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**The gap — and `SPLICE.flushState`.** Reports are throttled (one per 60
|
|
238
|
+
seconds per viewer), so at any moment the student may have committed work
|
|
239
|
+
that no report has delivered yet. A host that unmounts a viewer based on
|
|
240
|
+
save events alone silently loses that work. Before unmounting, request a
|
|
241
|
+
flush — post on your own window; the wrapper forwards it into the iframe:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
window.postMessage(
|
|
245
|
+
{ subject: "SPLICE.flushState", message_id: "my-id-123" },
|
|
246
|
+
"*",
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The flush settles in-flight updates and pushes any pending state out through
|
|
251
|
+
the **normal `SPLICE.reportScoreAndState` message** (which reaches your page
|
|
252
|
+
as shown above) — so a host that already persists those reports saves the
|
|
253
|
+
just-flushed state with no extra code. (No report is emitted when nothing is
|
|
254
|
+
pending, or when state saving is disabled — there is then nothing to lose.)
|
|
255
|
+
The viewer then replies with a stateless acknowledgement (again on your
|
|
256
|
+
window):
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
{
|
|
260
|
+
subject: "SPLICE.flushState.response",
|
|
261
|
+
message_id: "my-id-123", // echoed from the request
|
|
262
|
+
activity_id, doc_id, // to correlate on multi-viewer pages
|
|
263
|
+
success: true,
|
|
264
|
+
hadState: true, // false ⇒ nothing beyond initialization
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The acknowledgement is the completion signal: once it arrives, every saved
|
|
269
|
+
`reportScoreAndState` is current, so unmounting loses nothing — remounting
|
|
270
|
+
later with `initialState: <the last saved state>` (and
|
|
271
|
+
`flags: { allowLoadState: true }`) restores the document exactly, including
|
|
272
|
+
work an earlier report never delivered. `hadState: false` means the viewer
|
|
273
|
+
held no state beyond what it was initialized with (e.g. its core has not
|
|
274
|
+
booted yet) — equally safe to unmount.
|
|
275
|
+
|
|
276
|
+
> **Note:** Wrap the round-trip in a retry/timeout — the viewer's listener
|
|
277
|
+
> registers on mount, so a request posted moments after mounting can land
|
|
278
|
+
> before anyone is listening, and flushing is idempotent so re-posting is
|
|
279
|
+
> safe. Every viewer on the page receives a broadcast request and responds
|
|
280
|
+
> (correlate by `activity_id`/`doc_id`/`message_id`).
|
|
281
|
+
|
|
282
|
+
### Loading saved state at boot (`SPLICE.getState`)
|
|
283
|
+
|
|
284
|
+
With `flags: { allowLoadState: true }` and no `initialState` prop, the
|
|
285
|
+
viewer asks the host for saved state when it boots:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
{
|
|
289
|
+
subject: "SPLICE.getState",
|
|
290
|
+
message_id,
|
|
291
|
+
cid, // content id of the DoenetML source
|
|
292
|
+
domain_id: "Doenet",
|
|
293
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The viewer does not block on a reply — it boots fresh immediately and
|
|
298
|
+
**reboots seeded with the state** if a response arrives. If you have saved
|
|
299
|
+
state for this document (an object previously received from
|
|
300
|
+
`reportScoreAndState`, whose `cid` matches the request),
|
|
301
|
+
respond:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
{ subject: "SPLICE.getState.response", message_id, state }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
If there is no saved state, no response is needed. To surface a load
|
|
308
|
+
failure to the student instead, respond with
|
|
309
|
+
`{ subject: "SPLICE.getState.response", error: { code, message } }`
|
|
310
|
+
(and no `message_id`).
|
|
311
|
+
|
|
312
|
+
Passing `initialState` yourself (or `initialState: null` for "start
|
|
313
|
+
fresh") skips this request entirely.
|
|
314
|
+
|
|
315
|
+
### Submitting all answers (`SPLICE.submitAllAnswers`)
|
|
316
|
+
|
|
317
|
+
Post `{ subject: "SPLICE.submitAllAnswers" }` and the viewer submits every
|
|
318
|
+
answer in the document, then responds with
|
|
319
|
+
`{ subject: "SPLICE.submitAllAnswers.response", success }`.
|
|
320
|
+
|
|
321
|
+
> **Note:** this pair carries no correlation id — on a page with several
|
|
322
|
+
> viewers, every viewer submits and responds, and the responses cannot be
|
|
323
|
+
> told apart. Use it with a single viewer per page (its original use case)
|
|
324
|
+
> or treat it as fire-and-forget.
|
|
325
|
+
|
|
326
|
+
### Solution-view permission (`SPLICE.requestSolutionView`)
|
|
327
|
+
|
|
328
|
+
With `flags: { solutionDisplayMode: "buttonRequirePermission" }`, a student
|
|
329
|
+
opening a solution triggers a permission request to the host:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
{
|
|
333
|
+
subject: "SPLICE.requestSolutionView",
|
|
334
|
+
message_id,
|
|
335
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
336
|
+
component_idx, // the solution component being opened
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Decide and respond — note the response echoes the id as **`messageId`**
|
|
341
|
+
(camelCase), unlike the snake_case request field:
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
{ subject: "SPLICE.requestSolutionView.response", messageId, allowView: true }
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The solution is revealed only when `allowView` is `true`.
|
|
348
|
+
|
|
349
|
+
### Event stream (`SPLICE.sendEvent`)
|
|
350
|
+
|
|
351
|
+
With `flags: { allowSaveEvents: true }`, the viewer emits an analytics
|
|
352
|
+
event for student interactions (answers submitted, solutions viewed,
|
|
353
|
+
content experienced, …). Fire-and-forget; no response is expected:
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
{
|
|
357
|
+
subject: "SPLICE.sendEvent",
|
|
358
|
+
message_id,
|
|
359
|
+
name, // mirrors data.verb
|
|
360
|
+
data: {
|
|
361
|
+
activityId, cid, docId, attemptNumber, variantIndex,
|
|
362
|
+
verb, // e.g. "answered", "experienced"
|
|
363
|
+
object, // JSON string: the component acted on
|
|
364
|
+
result, // JSON string: the outcome
|
|
365
|
+
context, // JSON string: additional context
|
|
366
|
+
timestamp, // "YYYY-MM-DD HH:MM:SS"
|
|
367
|
+
version,
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
72
372
|
## Development
|
|
73
373
|
|
|
74
374
|
Source code in `src/iframe-viewer-index.ts` and `src/iframe-editor-index.ts`
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
import { CreativeCommonsVersion } from '../packages/utils/dist';
|
|
2
|
+
import { creativeCommonsVersions } from '../packages/utils/dist';
|
|
1
3
|
import { default as default_2 } from 'react';
|
|
4
|
+
import { defaultCreativeCommonsVersion } from '../packages/utils/dist';
|
|
2
5
|
import { DiagnosticRecord } from '../packages/utils/dist';
|
|
3
6
|
import { DiagnosticsTabId } from '../packages/doenetml/dist';
|
|
4
7
|
import { DoenetEditor as DoenetEditor_2 } from '../packages/doenetml/dist';
|
|
5
8
|
import { DoenetEditorHandle } from '../packages/doenetml/dist';
|
|
6
9
|
import { DoenetViewer as DoenetViewer_2 } from '../packages/doenetml/dist';
|
|
7
10
|
import { ErrorRecord } from '../packages/utils/dist';
|
|
11
|
+
import { getMediaLicenseDisplay } from '../packages/utils/dist';
|
|
12
|
+
import { getMediaLicenseInfo } from '../packages/utils/dist';
|
|
8
13
|
import { mathjaxConfig } from '../packages/utils/dist';
|
|
14
|
+
import { MediaLicenseDisplay } from '../packages/utils/dist';
|
|
15
|
+
import { MediaLicenseInfo } from '../packages/utils/dist';
|
|
16
|
+
import { MediaLicenseKind } from '../packages/utils/dist';
|
|
17
|
+
import { mediaLicenses } from '../packages/utils/dist';
|
|
9
18
|
import { WarningRecord } from '../packages/utils/dist';
|
|
10
19
|
|
|
20
|
+
export { CreativeCommonsVersion }
|
|
21
|
+
|
|
22
|
+
export { creativeCommonsVersions }
|
|
23
|
+
|
|
24
|
+
export { defaultCreativeCommonsVersion }
|
|
25
|
+
|
|
11
26
|
export { DiagnosticRecord }
|
|
12
27
|
|
|
13
28
|
export { DiagnosticsTabId }
|
|
@@ -55,12 +70,23 @@ declare type DoenetEditorProps = Omit<React.ComponentProps<typeof DoenetEditor_2
|
|
|
55
70
|
* Render Doenet viewer constrained to an iframe. A URL pointing to a version of DoenetML
|
|
56
71
|
* standalone must be provided (along with a URL to the corresponding CSS file).
|
|
57
72
|
*
|
|
58
|
-
* Parameters
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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.)
|
|
62
88
|
*/
|
|
63
|
-
export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
|
|
89
|
+
export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, mountPolicy, keepLive, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
|
|
64
90
|
|
|
65
91
|
export declare type DoenetViewerIframeProps = DoenetViewerProps & {
|
|
66
92
|
doenetML: string;
|
|
@@ -85,14 +111,145 @@ export declare type DoenetViewerIframeProps = DoenetViewerProps & {
|
|
|
85
111
|
* overwriting any doenetmlVersion or urls provided
|
|
86
112
|
*/
|
|
87
113
|
autodetectVersion?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Opt-in (#1466): multiplex this viewer's core onto a shared core worker
|
|
116
|
+
* owned by this (parent) page, instead of the iframe booting its own
|
|
117
|
+
* ~100 MB dedicated worker. Viewers on the same page with the same
|
|
118
|
+
* standalone version share workers (up to a pool cap per worker), which
|
|
119
|
+
* is the dominant memory saving on pages embedding many documents.
|
|
120
|
+
* Trade-off: a worker-level hang or crash affects every document on that
|
|
121
|
+
* worker (per-core teardown stays individual; suspect workers are
|
|
122
|
+
* quarantined so retries boot fresh ones). Default off.
|
|
123
|
+
*/
|
|
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), and only when
|
|
138
|
+
* the selected bundle is new enough to acknowledge the flush (v0.7.21+,
|
|
139
|
+
* or a host-specified `standaloneUrl`); otherwise the viewer boots on
|
|
140
|
+
* visibility but then always stays live. The policy is read at mount;
|
|
141
|
+
* changing it afterwards is not supported. Default off (no prop =
|
|
142
|
+
* today's behavior).
|
|
143
|
+
*/
|
|
144
|
+
mountPolicy?: MountPolicy;
|
|
145
|
+
/**
|
|
146
|
+
* Windowed-mounting hint: treat this viewer as visible regardless of
|
|
147
|
+
* viewport intersection — boot it eagerly (still subject to the
|
|
148
|
+
* page-wide `maxConcurrentBoots` slot queue) and never park it while
|
|
149
|
+
* set. For hosts that know an off-screen or `display:none` viewer is
|
|
150
|
+
* about to be shown, e.g. a paginator prefetching the pages adjacent to
|
|
151
|
+
* the current one. Dynamic, unlike `mountPolicy`; no effect without a
|
|
152
|
+
* windowed `mountPolicy`. The hint counts as visibility for boot
|
|
153
|
+
* ordering and the least-recently-visible eviction order, so keep it on
|
|
154
|
+
* a few viewers at a time (a page-wide `keepLive` would defeat the
|
|
155
|
+
* budget).
|
|
156
|
+
*/
|
|
157
|
+
keepLive?: boolean;
|
|
88
158
|
};
|
|
89
159
|
|
|
90
160
|
declare type DoenetViewerProps = Omit<React.ComponentProps<typeof DoenetViewer_2>, "doenetML" | "externalVirtualKeyboardProvided">;
|
|
91
161
|
|
|
92
162
|
export { ErrorRecord }
|
|
93
163
|
|
|
164
|
+
export { getMediaLicenseDisplay }
|
|
165
|
+
|
|
166
|
+
export { getMediaLicenseInfo }
|
|
167
|
+
|
|
168
|
+
/** Snapshot for tests and diagnostics. */
|
|
169
|
+
export declare function getViewerLifecycleStats(): {
|
|
170
|
+
registered: number;
|
|
171
|
+
live: number;
|
|
172
|
+
parking: number;
|
|
173
|
+
parked: number;
|
|
174
|
+
booting: number;
|
|
175
|
+
bootQueue: number;
|
|
176
|
+
};
|
|
177
|
+
|
|
94
178
|
export { mathjaxConfig }
|
|
95
179
|
|
|
180
|
+
export { MediaLicenseDisplay }
|
|
181
|
+
|
|
182
|
+
export { MediaLicenseInfo }
|
|
183
|
+
|
|
184
|
+
export { MediaLicenseKind }
|
|
185
|
+
|
|
186
|
+
export { mediaLicenses }
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Page-wide lifecycle manager for windowed `<DoenetViewer>` mounting
|
|
190
|
+
* (#1441, stream B).
|
|
191
|
+
*
|
|
192
|
+
* Viewers that opt in via the `mountPolicy` prop register here. The manager
|
|
193
|
+
* enforces a page-wide budget of live viewers: when more than
|
|
194
|
+
* `maxLiveViewers` are live at once, the least-recently-visible off-screen
|
|
195
|
+
* viewers are asked to park (flush their state and replace their iframe with
|
|
196
|
+
* a placeholder), so idle memory tracks what the user can see rather than
|
|
197
|
+
* how many documents the page embeds.
|
|
198
|
+
*
|
|
199
|
+
* It also gates *when* a lazily-mounted viewer may boot its iframe realm: a
|
|
200
|
+
* page-wide boot-slot semaphore (`maxConcurrentBoots`) caps how many viewers
|
|
201
|
+
* evaluate the multi-MB standalone bundle at once, serving visible viewers
|
|
202
|
+
* first (see `requestBootSlot`).
|
|
203
|
+
*
|
|
204
|
+
* This module is pure policy: *when* to park and *when* to boot. The
|
|
205
|
+
* mechanics (flushing state, creating/swapping the iframe, restoring) live in
|
|
206
|
+
* the `DoenetViewer` wrapper, which registers callbacks here. Module-level
|
|
207
|
+
* state is intentional — the budget is shared by every windowed viewer on
|
|
208
|
+
* the page (same pattern as `shared-core-pool.ts`).
|
|
209
|
+
*
|
|
210
|
+
* Rules:
|
|
211
|
+
* - A currently-visible viewer is never parked (the budget is soft: if more
|
|
212
|
+
* than `maxLiveViewers` are visible at once, all of them stay live).
|
|
213
|
+
* - A viewer only becomes eligible to park after it has been off-screen for
|
|
214
|
+
* its `parkDelayMs` (debounce against scroll flicker).
|
|
215
|
+
* - Viewers whose `canPark` returns false (no persistence path — parking
|
|
216
|
+
* would lose student work) are never parked.
|
|
217
|
+
* - Eviction order is least-recently-visible first.
|
|
218
|
+
*/
|
|
219
|
+
export declare type MountPolicy = {
|
|
220
|
+
/** The only mode currently defined. */
|
|
221
|
+
mode: "windowed";
|
|
222
|
+
/**
|
|
223
|
+
* Page-wide budget of simultaneously live (iframe-mounted) viewers.
|
|
224
|
+
* When viewers on the same page specify different values, the smallest
|
|
225
|
+
* wins. Default 3.
|
|
226
|
+
*/
|
|
227
|
+
maxLiveViewers?: number;
|
|
228
|
+
/**
|
|
229
|
+
* `rootMargin` for the visibility observer — how far outside the
|
|
230
|
+
* viewport a viewer still counts as "visible". Default "1000px".
|
|
231
|
+
*/
|
|
232
|
+
visibleMargin?: string;
|
|
233
|
+
/**
|
|
234
|
+
* How long to wait for the viewer to acknowledge the pre-park state
|
|
235
|
+
* flush before parking anyway. Default 5000.
|
|
236
|
+
*/
|
|
237
|
+
flushTimeoutMs?: number;
|
|
238
|
+
/**
|
|
239
|
+
* How long a viewer must be continuously off-screen before it may be
|
|
240
|
+
* parked. Default 2000.
|
|
241
|
+
*/
|
|
242
|
+
parkDelayMs?: number;
|
|
243
|
+
/**
|
|
244
|
+
* Page-wide cap on how many windowed viewers may be *booting* their
|
|
245
|
+
* iframe realm at once (each boot parses the multi-MB standalone bundle
|
|
246
|
+
* and starts a core worker — the initialization stampede #1439
|
|
247
|
+
* targets). Additional viewers wait for a slot, visible-first. As with
|
|
248
|
+
* `maxLiveViewers`, the smallest value across viewers wins. Default 2.
|
|
249
|
+
*/
|
|
250
|
+
maxConcurrentBoots?: number;
|
|
251
|
+
};
|
|
252
|
+
|
|
96
253
|
export declare const version: string;
|
|
97
254
|
|
|
98
255
|
export { WarningRecord }
|