@doenet/doenetml-iframe 0.7.20-dev.321 → 0.7.20-dev.324
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 +184 -0
- package/index.d.ts +12 -1
- package/index.js +152 -5
- package/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,190 @@ 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
|
+
### Host message protocol (SPLICE)
|
|
75
|
+
|
|
76
|
+
The viewer exchanges JSON messages with the host page via `postMessage`.
|
|
77
|
+
Viewer → host messages arrive on **your window** (the iframe posts to
|
|
78
|
+
`window.parent`, which is your page). Host → viewer requests are posted on
|
|
79
|
+
**your own window** too — the wrapper forwards these subjects into the
|
|
80
|
+
iframe: `SPLICE.getState.response`, `SPLICE.requestSolutionView.response`,
|
|
81
|
+
`SPLICE.submitAllAnswers`, and `SPLICE.flushState`. On a page with several
|
|
82
|
+
viewers, every viewer receives a forwarded request; correlate responses by
|
|
83
|
+
`activity_id`/`doc_id`/`message_id`.
|
|
84
|
+
|
|
85
|
+
The messages are documented in the subsections below:
|
|
86
|
+
|
|
87
|
+
| Subject | Direction | Purpose |
|
|
88
|
+
| ---------------------------------------------------------- | ------------- | ---------------------------------------------- |
|
|
89
|
+
| `SPLICE.reportScoreAndState` | viewer → host | periodic score/state saves |
|
|
90
|
+
| `SPLICE.getState` / `.response` | viewer ⇄ host | load saved state at boot |
|
|
91
|
+
| `SPLICE.flushState` / `.response` | host ⇄ viewer | on-demand state flush (lossless unmount) |
|
|
92
|
+
| `SPLICE.submitAllAnswers` / `.response` | host ⇄ viewer | submit every answer in the document |
|
|
93
|
+
| `SPLICE.requestSolutionView` / `.response` | viewer ⇄ host | permission gate for viewing solutions |
|
|
94
|
+
| `SPLICE.sendEvent` | viewer → host | analytics/event stream |
|
|
95
|
+
|
|
96
|
+
### Saving and restoring state (lossless unmount)
|
|
97
|
+
|
|
98
|
+
The viewer reports the student's serialized document state to the host as
|
|
99
|
+
they work: it posts `SPLICE.reportScoreAndState` messages to the host
|
|
100
|
+
window (the wrapper's iframe posts to `window.parent`, which is your page):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
window.addEventListener("message", (e) => {
|
|
104
|
+
if (e.data?.subject === "SPLICE.reportScoreAndState") {
|
|
105
|
+
// e.data.state — serialized document state (opaque; store as-is)
|
|
106
|
+
// e.data.score, e.data.activity_id, e.data.doc_id
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
To restore, remount the viewer with the saved state:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<DoenetViewer
|
|
115
|
+
doenetML={doenetML}
|
|
116
|
+
flags={{ allowLoadState: true }}
|
|
117
|
+
initialState={savedState}
|
|
118
|
+
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**The gap — and `SPLICE.flushState`.** Reports are throttled (one per 60
|
|
122
|
+
seconds per viewer), so at any moment the student may have committed work
|
|
123
|
+
that no report has delivered yet. A host that unmounts a viewer based on
|
|
124
|
+
save events alone silently loses that work. Before unmounting, request a
|
|
125
|
+
flush — post on your own window; the wrapper forwards it into the iframe:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
window.postMessage(
|
|
129
|
+
{ subject: "SPLICE.flushState", message_id: "my-id-123" },
|
|
130
|
+
"*",
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The flush settles in-flight updates and pushes any pending state out through
|
|
135
|
+
the **normal `SPLICE.reportScoreAndState` message** (which reaches your page
|
|
136
|
+
as shown above) — so a host that already persists those reports saves the
|
|
137
|
+
just-flushed state with no extra code. (No report is emitted when nothing is
|
|
138
|
+
pending, or when state saving is disabled — there is then nothing to lose.)
|
|
139
|
+
The viewer then replies with a stateless acknowledgement (again on your
|
|
140
|
+
window):
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
{
|
|
144
|
+
subject: "SPLICE.flushState.response",
|
|
145
|
+
message_id: "my-id-123", // echoed from the request
|
|
146
|
+
activity_id, doc_id, // to correlate on multi-viewer pages
|
|
147
|
+
success: true,
|
|
148
|
+
hadState: true, // false ⇒ nothing beyond initialization
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The acknowledgement is the completion signal: once it arrives, every saved
|
|
153
|
+
`reportScoreAndState` is current, so unmounting loses nothing — remounting
|
|
154
|
+
later with `initialState: <the last saved state>` (and
|
|
155
|
+
`flags: { allowLoadState: true }`) restores the document exactly, including
|
|
156
|
+
work an earlier report never delivered. `hadState: false` means the viewer
|
|
157
|
+
held no state beyond what it was initialized with (e.g. its core has not
|
|
158
|
+
booted yet) — equally safe to unmount.
|
|
159
|
+
|
|
160
|
+
> **Note:** Wrap the round-trip in a retry/timeout — the viewer's listener
|
|
161
|
+
> registers on mount, so a request posted moments after mounting can land
|
|
162
|
+
> before anyone is listening, and flushing is idempotent so re-posting is
|
|
163
|
+
> safe. Every viewer on the page receives a broadcast request and responds
|
|
164
|
+
> (correlate by `activity_id`/`doc_id`/`message_id`).
|
|
165
|
+
|
|
166
|
+
### Loading saved state at boot (`SPLICE.getState`)
|
|
167
|
+
|
|
168
|
+
With `flags: { allowLoadState: true }` and no `initialState` prop, the
|
|
169
|
+
viewer asks the host for saved state when it boots:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
{
|
|
173
|
+
subject: "SPLICE.getState",
|
|
174
|
+
message_id,
|
|
175
|
+
cid, // content id of the DoenetML source
|
|
176
|
+
domain_id: "Doenet",
|
|
177
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The viewer does not block on a reply — it boots fresh immediately and
|
|
182
|
+
**reboots seeded with the state** if a response arrives. If you have saved
|
|
183
|
+
state for this document (an object previously received from
|
|
184
|
+
`reportScoreAndState`, whose `cid` matches the request),
|
|
185
|
+
respond:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
{ subject: "SPLICE.getState.response", message_id, state }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If there is no saved state, no response is needed. To surface a load
|
|
192
|
+
failure to the student instead, respond with
|
|
193
|
+
`{ subject: "SPLICE.getState.response", error: { code, message } }`
|
|
194
|
+
(and no `message_id`).
|
|
195
|
+
|
|
196
|
+
Passing `initialState` yourself (or `initialState: null` for "start
|
|
197
|
+
fresh") skips this request entirely.
|
|
198
|
+
|
|
199
|
+
### Submitting all answers (`SPLICE.submitAllAnswers`)
|
|
200
|
+
|
|
201
|
+
Post `{ subject: "SPLICE.submitAllAnswers" }` and the viewer submits every
|
|
202
|
+
answer in the document, then responds with
|
|
203
|
+
`{ subject: "SPLICE.submitAllAnswers.response", success }`.
|
|
204
|
+
|
|
205
|
+
> **Note:** this pair carries no correlation id — on a page with several
|
|
206
|
+
> viewers, every viewer submits and responds, and the responses cannot be
|
|
207
|
+
> told apart. Use it with a single viewer per page (its original use case)
|
|
208
|
+
> or treat it as fire-and-forget.
|
|
209
|
+
|
|
210
|
+
### Solution-view permission (`SPLICE.requestSolutionView`)
|
|
211
|
+
|
|
212
|
+
With `flags: { solutionDisplayMode: "buttonRequirePermission" }`, a student
|
|
213
|
+
opening a solution triggers a permission request to the host:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
{
|
|
217
|
+
subject: "SPLICE.requestSolutionView",
|
|
218
|
+
message_id,
|
|
219
|
+
activity_id, doc_id, attempt_number, user_id,
|
|
220
|
+
component_idx, // the solution component being opened
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Decide and respond — note the response echoes the id as **`messageId`**
|
|
225
|
+
(camelCase), unlike the snake_case request field:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
{ subject: "SPLICE.requestSolutionView.response", messageId, allowView: true }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The solution is revealed only when `allowView` is `true`.
|
|
232
|
+
|
|
233
|
+
### Event stream (`SPLICE.sendEvent`)
|
|
234
|
+
|
|
235
|
+
With `flags: { allowSaveEvents: true }`, the viewer emits an analytics
|
|
236
|
+
event for student interactions (answers submitted, solutions viewed,
|
|
237
|
+
content experienced, …). Fire-and-forget; no response is expected:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
{
|
|
241
|
+
subject: "SPLICE.sendEvent",
|
|
242
|
+
message_id,
|
|
243
|
+
name, // mirrors data.verb
|
|
244
|
+
data: {
|
|
245
|
+
activityId, cid, docId, attemptNumber, variantIndex,
|
|
246
|
+
verb, // e.g. "answered", "experienced"
|
|
247
|
+
object, // JSON string: the component acted on
|
|
248
|
+
result, // JSON string: the outcome
|
|
249
|
+
context, // JSON string: additional context
|
|
250
|
+
timestamp, // "YYYY-MM-DD HH:MM:SS"
|
|
251
|
+
version,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
72
256
|
## Development
|
|
73
257
|
|
|
74
258
|
Source code in `src/iframe-viewer-index.ts` and `src/iframe-editor-index.ts`
|
package/index.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ declare type DoenetEditorProps = Omit<React.ComponentProps<typeof DoenetEditor_2
|
|
|
75
75
|
* DoenetML component. Instead you must use the message passing interface of `DoenetViewer` to communicate
|
|
76
76
|
* with the underlying DoenetML component.
|
|
77
77
|
*/
|
|
78
|
-
export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
|
|
78
|
+
export declare function DoenetViewer({ doenetML, standaloneUrl: specifiedStandaloneUrl, cssUrl: specifiedCssUrl, doenetmlVersion: specifiedDoenetmlVersion, autodetectVersion, useSharedCoreWorker, ...doenetViewerProps }: DoenetViewerIframeProps): default_2.JSX.Element | null;
|
|
79
79
|
|
|
80
80
|
export declare type DoenetViewerIframeProps = DoenetViewerProps & {
|
|
81
81
|
doenetML: string;
|
|
@@ -100,6 +100,17 @@ export declare type DoenetViewerIframeProps = DoenetViewerProps & {
|
|
|
100
100
|
* overwriting any doenetmlVersion or urls provided
|
|
101
101
|
*/
|
|
102
102
|
autodetectVersion?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Opt-in (#1466): multiplex this viewer's core onto a shared core worker
|
|
105
|
+
* owned by this (parent) page, instead of the iframe booting its own
|
|
106
|
+
* ~100 MB dedicated worker. Viewers on the same page with the same
|
|
107
|
+
* standalone version share workers (up to a pool cap per worker), which
|
|
108
|
+
* is the dominant memory saving on pages embedding many documents.
|
|
109
|
+
* Trade-off: a worker-level hang or crash affects every document on that
|
|
110
|
+
* worker (per-core teardown stays individual; suspect workers are
|
|
111
|
+
* quarantined so retries boot fresh ones). Default off.
|
|
112
|
+
*/
|
|
113
|
+
useSharedCoreWorker?: boolean;
|
|
103
114
|
};
|
|
104
115
|
|
|
105
116
|
declare type DoenetViewerProps = Omit<React.ComponentProps<typeof DoenetViewer_2>, "doenetML" | "externalVirtualKeyboardProvided">;
|
package/index.js
CHANGED
|
@@ -25618,7 +25618,7 @@ function requireCssesc$1() {
|
|
|
25618
25618
|
return cssesc_1$1;
|
|
25619
25619
|
}
|
|
25620
25620
|
requireCssesc$1();
|
|
25621
|
-
const viewerIframeJsSource = '(function() {\n "use strict";\n async function waitForStandaloneBundle(timeoutMs) {\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n }\n return false;\n }\n ComlinkViewer.expose(\n { renderViewerWithFunctionProps },\n ComlinkViewer.windowEndpoint(globalThis.parent)\n );\n function renderViewerWithFunctionProps(...args) {\n const augmentedDoenetViewerProps = { ...doenetViewerProps };\n augmentedDoenetViewerProps.externalVirtualKeyboardProvided = true;\n for (const propName of doenetViewerPropsSpecified) {\n if (!(propName in doenetViewerProps)) {\n const idx = args.indexOf(propName);\n if (idx !== -1) {\n augmentedDoenetViewerProps[propName] = args[idx + 1];\n }\n }\n }\n if (!doenetViewerPropsSpecified.includes("requestScrollTo")) {\n augmentedDoenetViewerProps.requestScrollTo = (offset) => {\n messageParentFromViewer({ type: "scrollTo", offset });\n };\n }\n window.renderDoenetViewerToContainer(\n document.getElementById("root"),\n void 0,\n augmentedDoenetViewerProps\n );\n }\n (async () => {\n if (await waitForStandaloneBundle(6e4)) {\n messageParentFromViewer({ iframeReady: true });\n } else {\n messageParentFromViewer({\n error: "Invalid DoenetML version or DoenetML package not found"\n });\n }\n })().catch((err) => {\n console.error(\n "iframe DoenetViewer: unexpected failure while signalling iframeReady",\n err\n );\n try {\n messageParentFromViewer({\n error: "iframe viewer failed to initialize"\n });\n } catch {\n }\n });\n function messageParentFromViewer(data) {\n window.parent.postMessage(\n {\n origin: viewerId,\n data\n },\n window.parent.origin\n );\n }\n})();\n';
|
|
25621
|
+
const viewerIframeJsSource = '(function() {\n "use strict";\n async function waitForStandaloneBundle(timeoutMs) {\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n if (typeof window.renderDoenetViewerToContainer === "function") {\n return true;\n }\n }\n return false;\n }\n ComlinkViewer.expose(\n { renderViewerWithFunctionProps },\n ComlinkViewer.windowEndpoint(globalThis.parent)\n );\n function renderViewerWithFunctionProps(...args) {\n const augmentedDoenetViewerProps = { ...doenetViewerProps };\n augmentedDoenetViewerProps.externalVirtualKeyboardProvided = true;\n for (const propName of doenetViewerPropsSpecified) {\n if (!(propName in doenetViewerProps)) {\n const idx = args.indexOf(propName);\n if (idx !== -1) {\n augmentedDoenetViewerProps[propName] = args[idx + 1];\n }\n }\n }\n if (!doenetViewerPropsSpecified.includes("requestScrollTo")) {\n augmentedDoenetViewerProps.requestScrollTo = (offset) => {\n messageParentFromViewer({ type: "scrollTo", offset });\n };\n }\n window.renderDoenetViewerToContainer(\n document.getElementById("root"),\n void 0,\n augmentedDoenetViewerProps\n );\n }\n function installSharedCorePortProvider() {\n let coreCounter = 0;\n window.doenetGlobalConfig.createExternalCoreWorkerPort = () => {\n const coreId = `${viewerId}-core-${++coreCounter}`;\n const channel = new MessageChannel();\n window.parent.postMessage(\n { origin: viewerId, data: { type: "createSharedCore", coreId } },\n window.parent.origin,\n [channel.port2]\n );\n return {\n port: channel.port1,\n destroy: (suspectWedge) => {\n messageParentFromViewer({\n type: "destroySharedCore",\n coreId,\n suspectWedge: Boolean(suspectWedge)\n });\n }\n };\n };\n }\n (async () => {\n if (await waitForStandaloneBundle(6e4)) {\n if (typeof doenetSharedCoreWorker !== "undefined" && doenetSharedCoreWorker) {\n installSharedCorePortProvider();\n }\n messageParentFromViewer({ iframeReady: true });\n } else {\n messageParentFromViewer({\n error: "Invalid DoenetML version or DoenetML package not found"\n });\n }\n })().catch((err) => {\n console.error(\n "iframe DoenetViewer: unexpected failure while signalling iframeReady",\n err\n );\n try {\n messageParentFromViewer({\n error: "iframe viewer failed to initialize"\n });\n } catch {\n }\n });\n function messageParentFromViewer(data) {\n window.parent.postMessage(\n {\n origin: viewerId,\n data\n },\n window.parent.origin\n );\n }\n})();\n';
|
|
25622
25622
|
const editorIframeJsSource = `(function() {
|
|
25623
25623
|
"use strict";
|
|
25624
25624
|
let editorControlHandle = null;
|
|
@@ -25856,7 +25856,7 @@ function createIframeHead(standaloneUrl, cssUrl) {
|
|
|
25856
25856
|
function createIframeBodyOpenTag(darkMode) {
|
|
25857
25857
|
return `<body style="margin:0" ${BODY_BACKGROUND_ATTRIBUTE}="${bodyBackgroundMode(darkMode)}">`;
|
|
25858
25858
|
}
|
|
25859
|
-
function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneUrl, cssUrl) {
|
|
25859
|
+
function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneUrl, cssUrl, useSharedCoreWorker = false) {
|
|
25860
25860
|
const doenetViewerPropsSpecified = Object.keys(doenetViewerProps);
|
|
25861
25861
|
return `
|
|
25862
25862
|
<html style="overflow:hidden">
|
|
@@ -25866,6 +25866,7 @@ function createHtmlForDoenetViewer(id2, doenetML, doenetViewerProps, standaloneU
|
|
|
25866
25866
|
const viewerId = "${id2}";
|
|
25867
25867
|
const doenetViewerProps = ${JSON.stringify(doenetViewerProps)};
|
|
25868
25868
|
const doenetViewerPropsSpecified = ${JSON.stringify(doenetViewerPropsSpecified)};
|
|
25869
|
+
const doenetSharedCoreWorker = ${JSON.stringify(!!useSharedCoreWorker)};
|
|
25869
25870
|
import * as ComlinkViewer from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
|
|
25870
25871
|
|
|
25871
25872
|
// This source code has been compiled by vite and should be directly included.
|
|
@@ -25907,6 +25908,124 @@ function createHtmlForDoenetEditor(id2, doenetML, width, doenetEditorProps, stan
|
|
|
25907
25908
|
</html>
|
|
25908
25909
|
`;
|
|
25909
25910
|
}
|
|
25911
|
+
const MAX_CORES_PER_WORKER = 12;
|
|
25912
|
+
const hostsByWorkerUrl = /* @__PURE__ */ new Map();
|
|
25913
|
+
const coreRecords = /* @__PURE__ */ new Map();
|
|
25914
|
+
function resolveWorkerUrl(standaloneUrl) {
|
|
25915
|
+
try {
|
|
25916
|
+
return new URL("./doenetml-worker/index.js", standaloneUrl).href;
|
|
25917
|
+
} catch {
|
|
25918
|
+
}
|
|
25919
|
+
try {
|
|
25920
|
+
return new URL("/doenetml-worker/index.js", document.baseURI).href;
|
|
25921
|
+
} catch {
|
|
25922
|
+
return null;
|
|
25923
|
+
}
|
|
25924
|
+
}
|
|
25925
|
+
function workerCreationUrl(workerUrl) {
|
|
25926
|
+
try {
|
|
25927
|
+
if (new URL(workerUrl).origin === window.location.origin) {
|
|
25928
|
+
return workerUrl;
|
|
25929
|
+
}
|
|
25930
|
+
} catch {
|
|
25931
|
+
}
|
|
25932
|
+
const bootstrap = `importScripts(${JSON.stringify(workerUrl)});`;
|
|
25933
|
+
return URL.createObjectURL(
|
|
25934
|
+
new Blob([bootstrap], { type: "text/javascript" })
|
|
25935
|
+
);
|
|
25936
|
+
}
|
|
25937
|
+
function quarantineHost(workerUrl, host) {
|
|
25938
|
+
host.quarantined = true;
|
|
25939
|
+
if (host.liveCores <= 0) {
|
|
25940
|
+
try {
|
|
25941
|
+
host.worker.terminate();
|
|
25942
|
+
} catch {
|
|
25943
|
+
}
|
|
25944
|
+
const pool = hostsByWorkerUrl.get(workerUrl);
|
|
25945
|
+
const index2 = pool?.indexOf(host) ?? -1;
|
|
25946
|
+
if (pool && index2 !== -1) {
|
|
25947
|
+
pool.splice(index2, 1);
|
|
25948
|
+
}
|
|
25949
|
+
}
|
|
25950
|
+
}
|
|
25951
|
+
function getHost(workerUrl) {
|
|
25952
|
+
let pool = hostsByWorkerUrl.get(workerUrl);
|
|
25953
|
+
if (!pool) {
|
|
25954
|
+
pool = [];
|
|
25955
|
+
hostsByWorkerUrl.set(workerUrl, pool);
|
|
25956
|
+
}
|
|
25957
|
+
let host = pool.find(
|
|
25958
|
+
(h2) => !h2.quarantined && h2.liveCores < MAX_CORES_PER_WORKER
|
|
25959
|
+
);
|
|
25960
|
+
if (!host) {
|
|
25961
|
+
const worker = new Worker(workerCreationUrl(workerUrl), {
|
|
25962
|
+
type: "classic"
|
|
25963
|
+
});
|
|
25964
|
+
const newHost = {
|
|
25965
|
+
worker,
|
|
25966
|
+
remote: wrap$3(worker),
|
|
25967
|
+
liveCores: 0,
|
|
25968
|
+
quarantined: false
|
|
25969
|
+
};
|
|
25970
|
+
worker.addEventListener("error", () => {
|
|
25971
|
+
quarantineHost(workerUrl, newHost);
|
|
25972
|
+
});
|
|
25973
|
+
pool.push(newHost);
|
|
25974
|
+
host = newHost;
|
|
25975
|
+
}
|
|
25976
|
+
return host;
|
|
25977
|
+
}
|
|
25978
|
+
function handleCreateSharedCore({
|
|
25979
|
+
viewerId,
|
|
25980
|
+
coreId,
|
|
25981
|
+
standaloneUrl,
|
|
25982
|
+
port
|
|
25983
|
+
}) {
|
|
25984
|
+
const workerUrl = resolveWorkerUrl(standaloneUrl);
|
|
25985
|
+
if (workerUrl === null) {
|
|
25986
|
+
console.warn(
|
|
25987
|
+
"doenetml-iframe: could not resolve a shared core worker URL for",
|
|
25988
|
+
standaloneUrl
|
|
25989
|
+
);
|
|
25990
|
+
return;
|
|
25991
|
+
}
|
|
25992
|
+
const host = getHost(workerUrl);
|
|
25993
|
+
host.liveCores++;
|
|
25994
|
+
const idPromise = host.remote.createCore(transfer(port, [port]));
|
|
25995
|
+
idPromise.catch(() => {
|
|
25996
|
+
});
|
|
25997
|
+
coreRecords.set(coreId, {
|
|
25998
|
+
viewerId,
|
|
25999
|
+
host,
|
|
26000
|
+
workerUrl,
|
|
26001
|
+
idPromise,
|
|
26002
|
+
destroyed: false
|
|
26003
|
+
});
|
|
26004
|
+
}
|
|
26005
|
+
function handleDestroySharedCore({
|
|
26006
|
+
coreId,
|
|
26007
|
+
suspectWedge
|
|
26008
|
+
}) {
|
|
26009
|
+
const record = coreRecords.get(coreId);
|
|
26010
|
+
if (!record || record.destroyed) {
|
|
26011
|
+
return;
|
|
26012
|
+
}
|
|
26013
|
+
record.destroyed = true;
|
|
26014
|
+
coreRecords.delete(coreId);
|
|
26015
|
+
record.host.liveCores--;
|
|
26016
|
+
record.idPromise.then((id2) => record.host.remote.destroyCore(id2)).catch(() => {
|
|
26017
|
+
});
|
|
26018
|
+
if (suspectWedge || record.host.quarantined) {
|
|
26019
|
+
quarantineHost(record.workerUrl, record.host);
|
|
26020
|
+
}
|
|
26021
|
+
}
|
|
26022
|
+
function destroySharedCoresForViewer(viewerId) {
|
|
26023
|
+
for (const [coreId, record] of [...coreRecords]) {
|
|
26024
|
+
if (record.viewerId === viewerId) {
|
|
26025
|
+
handleDestroySharedCore({ coreId, suspectWedge: false });
|
|
26026
|
+
}
|
|
26027
|
+
}
|
|
26028
|
+
}
|
|
25910
26029
|
function findNodesWithPositionInfo(nodes) {
|
|
25911
26030
|
if (typeof nodes !== "object" || nodes === null) {
|
|
25912
26031
|
return [];
|
|
@@ -63053,7 +63172,7 @@ function ExternalVirtualKeyboard({
|
|
|
63053
63172
|
}
|
|
63054
63173
|
);
|
|
63055
63174
|
}
|
|
63056
|
-
const version = "0.7.20-dev.
|
|
63175
|
+
const version = "0.7.20-dev.324";
|
|
63057
63176
|
const latestDoenetmlVersion = version;
|
|
63058
63177
|
function subscribeToPinnedTheme() {
|
|
63059
63178
|
return () => {
|
|
@@ -63074,6 +63193,7 @@ function DoenetViewer({
|
|
|
63074
63193
|
cssUrl: specifiedCssUrl,
|
|
63075
63194
|
doenetmlVersion: specifiedDoenetmlVersion,
|
|
63076
63195
|
autodetectVersion = true,
|
|
63196
|
+
useSharedCoreWorker = false,
|
|
63077
63197
|
...doenetViewerProps
|
|
63078
63198
|
}) {
|
|
63079
63199
|
const [id2, _2] = React__default__default.useState(() => Math.random().toString(36).slice(2));
|
|
@@ -63106,12 +63226,22 @@ function DoenetViewer({
|
|
|
63106
63226
|
} else {
|
|
63107
63227
|
cssUrl = `https://cdn.jsdelivr.net/npm/@doenet/standalone@${selectedDoenetmlVersion}/style.css`;
|
|
63108
63228
|
}
|
|
63229
|
+
const standaloneUrlRef = React__default__default.useRef(standaloneUrl);
|
|
63230
|
+
standaloneUrlRef.current = standaloneUrl;
|
|
63231
|
+
React__default__default.useEffect(() => {
|
|
63232
|
+
if (!useSharedCoreWorker) {
|
|
63233
|
+
return;
|
|
63234
|
+
}
|
|
63235
|
+
return () => {
|
|
63236
|
+
destroySharedCoresForViewer(id2);
|
|
63237
|
+
};
|
|
63238
|
+
}, []);
|
|
63109
63239
|
React__default__default.useEffect(() => {
|
|
63110
63240
|
const listener = (event) => {
|
|
63111
63241
|
if (event.origin !== window.location.origin) {
|
|
63112
63242
|
return;
|
|
63113
63243
|
}
|
|
63114
|
-
if (event.data.subject === "SPLICE.getState.response" || event.data.subject === "SPLICE.requestSolutionView.response" || event.data.subject == "SPLICE.submitAllAnswers") {
|
|
63244
|
+
if (event.data.subject === "SPLICE.getState.response" || event.data.subject === "SPLICE.requestSolutionView.response" || event.data.subject == "SPLICE.submitAllAnswers" || event.data.subject === "SPLICE.flushState") {
|
|
63115
63245
|
ref.current?.contentWindow?.postMessage(event.data);
|
|
63116
63246
|
return;
|
|
63117
63247
|
}
|
|
@@ -63127,6 +63257,22 @@ function DoenetViewer({
|
|
|
63127
63257
|
return;
|
|
63128
63258
|
}
|
|
63129
63259
|
const data = event.data.data;
|
|
63260
|
+
if (data.type === "createSharedCore" && event.ports[0]) {
|
|
63261
|
+
handleCreateSharedCore({
|
|
63262
|
+
viewerId: id2,
|
|
63263
|
+
coreId: String(data.coreId),
|
|
63264
|
+
standaloneUrl: standaloneUrlRef.current,
|
|
63265
|
+
port: event.ports[0]
|
|
63266
|
+
});
|
|
63267
|
+
return;
|
|
63268
|
+
}
|
|
63269
|
+
if (data.type === "destroySharedCore") {
|
|
63270
|
+
handleDestroySharedCore({
|
|
63271
|
+
coreId: String(data.coreId),
|
|
63272
|
+
suspectWedge: Boolean(data.suspectWedge)
|
|
63273
|
+
});
|
|
63274
|
+
return;
|
|
63275
|
+
}
|
|
63130
63276
|
if (ref.current && data.type === "scrollTo" && typeof data.offset === "number") {
|
|
63131
63277
|
const iframeTop = ref.current.getBoundingClientRect().top + window.scrollY;
|
|
63132
63278
|
const targetAbsoluteTop = iframeTop + data.offset;
|
|
@@ -63208,7 +63354,8 @@ function DoenetViewer({
|
|
|
63208
63354
|
doenetML,
|
|
63209
63355
|
doenetViewerProps,
|
|
63210
63356
|
standaloneUrl,
|
|
63211
|
-
cssUrl
|
|
63357
|
+
cssUrl,
|
|
63358
|
+
useSharedCoreWorker
|
|
63212
63359
|
),
|
|
63213
63360
|
style: {
|
|
63214
63361
|
width: "100%",
|