@doenet/doenetml-iframe 0.7.20-dev.323 → 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.js CHANGED
@@ -63172,7 +63172,7 @@ function ExternalVirtualKeyboard({
63172
63172
  }
63173
63173
  );
63174
63174
  }
63175
- const version = "0.7.20-dev.323";
63175
+ const version = "0.7.20-dev.324";
63176
63176
  const latestDoenetmlVersion = version;
63177
63177
  function subscribeToPinnedTheme() {
63178
63178
  return () => {
@@ -63241,7 +63241,7 @@ function DoenetViewer({
63241
63241
  if (event.origin !== window.location.origin) {
63242
63242
  return;
63243
63243
  }
63244
- 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") {
63245
63245
  ref.current?.contentWindow?.postMessage(event.data);
63246
63246
  return;
63247
63247
  }