@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 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.321";
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%",