@superbuilders/primer-tives 0.4.0 → 0.6.0
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 +605 -153
- package/dist/choice-state.d.ts.map +1 -1
- package/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/extended-text-state.d.ts.map +1 -1
- package/dist/feedback-state.d.ts +2 -2
- package/dist/feedback-state.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +327 -168
- package/dist/index.js.map +15 -14
- package/dist/match-state.d.ts.map +1 -1
- package/dist/order-state.d.ts +3 -10
- package/dist/order-state.d.ts.map +1 -1
- package/dist/pci-state.d.ts.map +1 -1
- package/dist/session.d.ts +2 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/subject.d.ts +6 -0
- package/dist/subject.d.ts.map +1 -0
- package/dist/text-entry-state.d.ts.map +1 -1
- package/dist/transport.d.ts +5 -4
- package/dist/transport.d.ts.map +1 -1
- package/dist/types.d.ts +61 -28
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as errors10 from "@superbuilders/errors";
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
import * as errors from "@superbuilders/errors";
|
|
6
6
|
var ErrNetwork = errors.new("network");
|
|
7
7
|
var ErrJsonParse = errors.new("json parse");
|
|
8
8
|
var ErrUnsupportedPci = errors.new("unsupported pci");
|
|
9
|
-
var
|
|
9
|
+
var ErrInvalidAccessToken = errors.new("invalid access token");
|
|
10
|
+
var ErrMalformedAccessToken = errors.new("malformed access token");
|
|
11
|
+
var ErrTokenExpired = errors.new("access token expired");
|
|
10
12
|
var ErrBadRequest = errors.new("bad request");
|
|
11
13
|
var ErrServerError = errors.new("server error");
|
|
12
14
|
var ErrTimeout = errors.new("timeout");
|
|
@@ -17,7 +19,6 @@ var ErrRateLimited = errors.new("rate limited");
|
|
|
17
19
|
var ErrServiceUnavailable = errors.new("service unavailable");
|
|
18
20
|
var ErrNotSerializable = errors.new("PrimerState is live in-memory state and must not be serialized or stored");
|
|
19
21
|
var ErrInvalidSubmission = errors.new("invalid submission");
|
|
20
|
-
var ErrMalformedPublishableKey = errors.new("malformed publishable key");
|
|
21
22
|
|
|
22
23
|
// src/transport.ts
|
|
23
24
|
import * as errors2 from "@superbuilders/errors";
|
|
@@ -27,7 +28,7 @@ function httpSentinel(status) {
|
|
|
27
28
|
return ErrBadRequest;
|
|
28
29
|
}
|
|
29
30
|
if (status === 401) {
|
|
30
|
-
return
|
|
31
|
+
return ErrInvalidAccessToken;
|
|
31
32
|
}
|
|
32
33
|
if (status === 403) {
|
|
33
34
|
return ErrForbidden;
|
|
@@ -66,15 +67,15 @@ function createTransport(tc) {
|
|
|
66
67
|
}
|
|
67
68
|
async function transport(body) {
|
|
68
69
|
log?.debug("transport request", {
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
intentKind: body.intent.kind,
|
|
71
|
+
subject: body.subject
|
|
71
72
|
});
|
|
72
73
|
const url = `${tc.origin}${ADVANCE_PATH}`;
|
|
73
74
|
const fetchResult = await fetchFn(url, {
|
|
74
75
|
method: "POST",
|
|
75
76
|
headers: {
|
|
76
77
|
"Content-Type": "application/json",
|
|
77
|
-
Authorization: `Bearer ${tc.
|
|
78
|
+
Authorization: `Bearer ${tc.accessToken}`
|
|
78
79
|
},
|
|
79
80
|
body: JSON.stringify(body),
|
|
80
81
|
signal: transportSignal()
|
|
@@ -86,13 +87,11 @@ function createTransport(tc) {
|
|
|
86
87
|
if (!fetchResult.ok) {
|
|
87
88
|
if (isAbortError(fetchResult.error)) {
|
|
88
89
|
log?.error("transport timeout", {
|
|
89
|
-
studentId: body.studentId,
|
|
90
90
|
intentKind: body.intent.kind
|
|
91
91
|
});
|
|
92
92
|
return { ok: false, error: errors2.wrap(ErrTimeout, fetchResult.error.message) };
|
|
93
93
|
}
|
|
94
94
|
log?.error("transport network error", {
|
|
95
|
-
studentId: body.studentId,
|
|
96
95
|
error: fetchResult.error
|
|
97
96
|
});
|
|
98
97
|
return { ok: false, error: errors2.wrap(ErrNetwork, fetchResult.error.message) };
|
|
@@ -104,7 +103,6 @@ function createTransport(tc) {
|
|
|
104
103
|
});
|
|
105
104
|
const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status);
|
|
106
105
|
log?.error("transport http error", {
|
|
107
|
-
studentId: body.studentId,
|
|
108
106
|
status: res.status
|
|
109
107
|
});
|
|
110
108
|
return { ok: false, error: errors2.wrap(sentinel, text) };
|
|
@@ -116,13 +114,11 @@ function createTransport(tc) {
|
|
|
116
114
|
});
|
|
117
115
|
if (!jsonResult.ok) {
|
|
118
116
|
log?.error("transport json parse failed", {
|
|
119
|
-
studentId: body.studentId,
|
|
120
117
|
error: jsonResult.error
|
|
121
118
|
});
|
|
122
119
|
return { ok: false, error: errors2.wrap(ErrJsonParse, jsonResult.error.message) };
|
|
123
120
|
}
|
|
124
121
|
log?.debug("transport success", {
|
|
125
|
-
studentId: body.studentId,
|
|
126
122
|
intentKind: body.intent.kind
|
|
127
123
|
});
|
|
128
124
|
return { ok: true, data: jsonResult.data };
|
|
@@ -131,7 +127,7 @@ function createTransport(tc) {
|
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
// src/session.ts
|
|
134
|
-
import * as
|
|
130
|
+
import * as errors9 from "@superbuilders/errors";
|
|
135
131
|
|
|
136
132
|
// src/consumed.ts
|
|
137
133
|
function poisonToJSON() {
|
|
@@ -173,7 +169,45 @@ function validateChoiceSubmission(log, selectedKeys, options, maxChoices, minCho
|
|
|
173
169
|
return null;
|
|
174
170
|
}
|
|
175
171
|
function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices) {
|
|
176
|
-
let
|
|
172
|
+
let submitPending;
|
|
173
|
+
let submitKey;
|
|
174
|
+
let timeoutPending;
|
|
175
|
+
function beginSubmit(selectedKeys) {
|
|
176
|
+
const submission = { type: "choice", selectedKeys };
|
|
177
|
+
const key = JSON.stringify(submission);
|
|
178
|
+
if (timeoutPending) {
|
|
179
|
+
return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
180
|
+
}
|
|
181
|
+
if (submitPending) {
|
|
182
|
+
if (submitKey === key) {
|
|
183
|
+
return submitPending;
|
|
184
|
+
}
|
|
185
|
+
return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
186
|
+
}
|
|
187
|
+
const validationError = validateChoiceSubmission(ctx.log, selectedKeys, options, maxChoices, minChoices);
|
|
188
|
+
if (validationError) {
|
|
189
|
+
return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
|
|
190
|
+
}
|
|
191
|
+
submitKey = key;
|
|
192
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
193
|
+
submitPending = undefined;
|
|
194
|
+
submitKey = undefined;
|
|
195
|
+
});
|
|
196
|
+
return submitPending;
|
|
197
|
+
}
|
|
198
|
+
function beginTimeout() {
|
|
199
|
+
const intent = { kind: "timeout" };
|
|
200
|
+
if (submitPending) {
|
|
201
|
+
return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
202
|
+
}
|
|
203
|
+
if (timeoutPending) {
|
|
204
|
+
return timeoutPending;
|
|
205
|
+
}
|
|
206
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
207
|
+
timeoutPending = undefined;
|
|
208
|
+
});
|
|
209
|
+
return timeoutPending;
|
|
210
|
+
}
|
|
177
211
|
return {
|
|
178
212
|
phase: "interaction",
|
|
179
213
|
kind: "choice",
|
|
@@ -182,27 +216,8 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
|
|
|
182
216
|
options,
|
|
183
217
|
maxChoices,
|
|
184
218
|
minChoices,
|
|
185
|
-
submitChoice:
|
|
186
|
-
|
|
187
|
-
return pending;
|
|
188
|
-
}
|
|
189
|
-
const validationError = validateChoiceSubmission(ctx.log, selectedKeys, options, maxChoices, minChoices);
|
|
190
|
-
if (validationError) {
|
|
191
|
-
return Promise.resolve(ctx.errored(validationError, "interaction", {
|
|
192
|
-
kind: "interaction",
|
|
193
|
-
submission: { type: "choice", selectedKeys }
|
|
194
|
-
}));
|
|
195
|
-
}
|
|
196
|
-
pending = ctx.execute({ kind: "interaction", submission: { type: "choice", selectedKeys } }, "interaction");
|
|
197
|
-
return pending;
|
|
198
|
-
},
|
|
199
|
-
timeout: function timeout() {
|
|
200
|
-
if (pending) {
|
|
201
|
-
return pending;
|
|
202
|
-
}
|
|
203
|
-
pending = ctx.execute({ kind: "timeout" }, "timeout");
|
|
204
|
-
return pending;
|
|
205
|
-
},
|
|
219
|
+
submitChoice: beginSubmit,
|
|
220
|
+
timeout: beginTimeout,
|
|
206
221
|
toJSON: poisonToJSON
|
|
207
222
|
};
|
|
208
223
|
}
|
|
@@ -222,66 +237,107 @@ function validateExtendedTextSubmission(log, values, minStrings, maxStrings) {
|
|
|
222
237
|
}
|
|
223
238
|
function extendedTextState(ctx, stimulus, interaction) {
|
|
224
239
|
if (interaction.cardinality === "single") {
|
|
225
|
-
let
|
|
240
|
+
let submitText = function(value) {
|
|
241
|
+
const submission = { type: "extended-text", values: [value] };
|
|
242
|
+
const key = JSON.stringify(submission);
|
|
243
|
+
if (timeoutPending2) {
|
|
244
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
245
|
+
}
|
|
246
|
+
if (submitPending2) {
|
|
247
|
+
if (submitKey2 === key) {
|
|
248
|
+
return submitPending2;
|
|
249
|
+
}
|
|
250
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
251
|
+
}
|
|
252
|
+
submitKey2 = key;
|
|
253
|
+
submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
254
|
+
submitPending2 = undefined;
|
|
255
|
+
submitKey2 = undefined;
|
|
256
|
+
});
|
|
257
|
+
return submitPending2;
|
|
258
|
+
}, timeout2 = function() {
|
|
259
|
+
const intent = { kind: "timeout" };
|
|
260
|
+
if (submitPending2) {
|
|
261
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
262
|
+
}
|
|
263
|
+
if (timeoutPending2) {
|
|
264
|
+
return timeoutPending2;
|
|
265
|
+
}
|
|
266
|
+
timeoutPending2 = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
267
|
+
timeoutPending2 = undefined;
|
|
268
|
+
});
|
|
269
|
+
return timeoutPending2;
|
|
270
|
+
};
|
|
271
|
+
let submitPending2;
|
|
272
|
+
let submitKey2;
|
|
273
|
+
let timeoutPending2;
|
|
226
274
|
return {
|
|
227
275
|
phase: "interaction",
|
|
228
276
|
kind: "extended-text",
|
|
229
277
|
cardinality: "single",
|
|
230
278
|
stimulus,
|
|
231
279
|
interaction,
|
|
232
|
-
submitText
|
|
233
|
-
|
|
234
|
-
return pending2;
|
|
235
|
-
}
|
|
236
|
-
pending2 = ctx.execute({ kind: "interaction", submission: { type: "extended-text", values: [value] } }, "interaction");
|
|
237
|
-
return pending2;
|
|
238
|
-
},
|
|
239
|
-
timeout: function timeout() {
|
|
240
|
-
if (pending2) {
|
|
241
|
-
return pending2;
|
|
242
|
-
}
|
|
243
|
-
pending2 = ctx.execute({ kind: "timeout" }, "timeout");
|
|
244
|
-
return pending2;
|
|
245
|
-
},
|
|
280
|
+
submitText,
|
|
281
|
+
timeout: timeout2,
|
|
246
282
|
toJSON: poisonToJSON
|
|
247
283
|
};
|
|
248
284
|
}
|
|
249
|
-
|
|
285
|
+
const multi = interaction;
|
|
286
|
+
let submitPending;
|
|
287
|
+
let submitKey;
|
|
288
|
+
let timeoutPending;
|
|
289
|
+
function submitTexts(values) {
|
|
290
|
+
const submission = { type: "extended-text", values };
|
|
291
|
+
const key = JSON.stringify(submission);
|
|
292
|
+
if (timeoutPending) {
|
|
293
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
294
|
+
}
|
|
295
|
+
if (submitPending) {
|
|
296
|
+
if (submitKey === key) {
|
|
297
|
+
return submitPending;
|
|
298
|
+
}
|
|
299
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
300
|
+
}
|
|
301
|
+
const validationError = validateExtendedTextSubmission(ctx.log, values, multi.minStrings, multi.maxStrings);
|
|
302
|
+
if (validationError) {
|
|
303
|
+
return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
|
|
304
|
+
}
|
|
305
|
+
submitKey = key;
|
|
306
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
307
|
+
submitPending = undefined;
|
|
308
|
+
submitKey = undefined;
|
|
309
|
+
});
|
|
310
|
+
return submitPending;
|
|
311
|
+
}
|
|
312
|
+
function timeout() {
|
|
313
|
+
const intent = { kind: "timeout" };
|
|
314
|
+
if (submitPending) {
|
|
315
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
316
|
+
}
|
|
317
|
+
if (timeoutPending) {
|
|
318
|
+
return timeoutPending;
|
|
319
|
+
}
|
|
320
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
321
|
+
timeoutPending = undefined;
|
|
322
|
+
});
|
|
323
|
+
return timeoutPending;
|
|
324
|
+
}
|
|
250
325
|
return {
|
|
251
326
|
phase: "interaction",
|
|
252
327
|
kind: "extended-text",
|
|
253
328
|
cardinality: "multiple",
|
|
254
329
|
stimulus,
|
|
255
|
-
interaction,
|
|
256
|
-
maxStrings:
|
|
257
|
-
minStrings:
|
|
258
|
-
submitTexts
|
|
259
|
-
|
|
260
|
-
return pending;
|
|
261
|
-
}
|
|
262
|
-
const validationError = validateExtendedTextSubmission(ctx.log, values, interaction.minStrings, interaction.maxStrings);
|
|
263
|
-
if (validationError) {
|
|
264
|
-
return Promise.resolve(ctx.errored(validationError, "interaction", {
|
|
265
|
-
kind: "interaction",
|
|
266
|
-
submission: { type: "extended-text", values }
|
|
267
|
-
}));
|
|
268
|
-
}
|
|
269
|
-
pending = ctx.execute({ kind: "interaction", submission: { type: "extended-text", values } }, "interaction");
|
|
270
|
-
return pending;
|
|
271
|
-
},
|
|
272
|
-
timeout: function timeout() {
|
|
273
|
-
if (pending) {
|
|
274
|
-
return pending;
|
|
275
|
-
}
|
|
276
|
-
pending = ctx.execute({ kind: "timeout" }, "timeout");
|
|
277
|
-
return pending;
|
|
278
|
-
},
|
|
330
|
+
interaction: multi,
|
|
331
|
+
maxStrings: multi.maxStrings,
|
|
332
|
+
minStrings: multi.minStrings,
|
|
333
|
+
submitTexts,
|
|
334
|
+
timeout,
|
|
279
335
|
toJSON: poisonToJSON
|
|
280
336
|
};
|
|
281
337
|
}
|
|
282
338
|
|
|
283
339
|
// src/feedback-state.ts
|
|
284
|
-
function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedbackContent,
|
|
340
|
+
function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedbackContent, review) {
|
|
285
341
|
let pending;
|
|
286
342
|
return {
|
|
287
343
|
phase: "feedback",
|
|
@@ -290,7 +346,7 @@ function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedba
|
|
|
290
346
|
submission,
|
|
291
347
|
isCorrect,
|
|
292
348
|
feedbackContent,
|
|
293
|
-
|
|
349
|
+
review,
|
|
294
350
|
advance: function advance() {
|
|
295
351
|
if (pending) {
|
|
296
352
|
return pending;
|
|
@@ -391,7 +447,45 @@ function validateMatchSubmission(log, pairs, sourceChoices, targetChoices, minAs
|
|
|
391
447
|
return null;
|
|
392
448
|
}
|
|
393
449
|
function matchState(ctx, stimulus, interaction) {
|
|
394
|
-
let
|
|
450
|
+
let submitPending;
|
|
451
|
+
let submitKey;
|
|
452
|
+
let timeoutPending;
|
|
453
|
+
function submitMatch(pairs) {
|
|
454
|
+
const submission = { type: "match", pairs };
|
|
455
|
+
const key = JSON.stringify(submission);
|
|
456
|
+
if (timeoutPending) {
|
|
457
|
+
return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
458
|
+
}
|
|
459
|
+
if (submitPending) {
|
|
460
|
+
if (submitKey === key) {
|
|
461
|
+
return submitPending;
|
|
462
|
+
}
|
|
463
|
+
return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
464
|
+
}
|
|
465
|
+
const validationError = validateMatchSubmission(ctx.log, pairs, interaction.sourceChoices, interaction.targetChoices, interaction.minAssociations, interaction.maxAssociations);
|
|
466
|
+
if (validationError) {
|
|
467
|
+
return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
|
|
468
|
+
}
|
|
469
|
+
submitKey = key;
|
|
470
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
471
|
+
submitPending = undefined;
|
|
472
|
+
submitKey = undefined;
|
|
473
|
+
});
|
|
474
|
+
return submitPending;
|
|
475
|
+
}
|
|
476
|
+
function timeout() {
|
|
477
|
+
const intent = { kind: "timeout" };
|
|
478
|
+
if (submitPending) {
|
|
479
|
+
return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
480
|
+
}
|
|
481
|
+
if (timeoutPending) {
|
|
482
|
+
return timeoutPending;
|
|
483
|
+
}
|
|
484
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
485
|
+
timeoutPending = undefined;
|
|
486
|
+
});
|
|
487
|
+
return timeoutPending;
|
|
488
|
+
}
|
|
395
489
|
return {
|
|
396
490
|
phase: "interaction",
|
|
397
491
|
kind: "match",
|
|
@@ -401,27 +495,8 @@ function matchState(ctx, stimulus, interaction) {
|
|
|
401
495
|
targetChoices: interaction.targetChoices,
|
|
402
496
|
minAssociations: interaction.minAssociations,
|
|
403
497
|
maxAssociations: interaction.maxAssociations,
|
|
404
|
-
submitMatch
|
|
405
|
-
|
|
406
|
-
return pending;
|
|
407
|
-
}
|
|
408
|
-
const validationError = validateMatchSubmission(ctx.log, pairs, interaction.sourceChoices, interaction.targetChoices, interaction.minAssociations, interaction.maxAssociations);
|
|
409
|
-
if (validationError) {
|
|
410
|
-
return Promise.resolve(ctx.errored(validationError, "interaction", {
|
|
411
|
-
kind: "interaction",
|
|
412
|
-
submission: { type: "match", pairs }
|
|
413
|
-
}));
|
|
414
|
-
}
|
|
415
|
-
pending = ctx.execute({ kind: "interaction", submission: { type: "match", pairs } }, "interaction");
|
|
416
|
-
return pending;
|
|
417
|
-
},
|
|
418
|
-
timeout: function timeout() {
|
|
419
|
-
if (pending) {
|
|
420
|
-
return pending;
|
|
421
|
-
}
|
|
422
|
-
pending = ctx.execute({ kind: "timeout" }, "timeout");
|
|
423
|
-
return pending;
|
|
424
|
-
},
|
|
498
|
+
submitMatch,
|
|
499
|
+
timeout,
|
|
425
500
|
toJSON: poisonToJSON
|
|
426
501
|
};
|
|
427
502
|
}
|
|
@@ -478,7 +553,45 @@ function validateOrderSubmission(log, orderedKeys, choices, minChoices, maxChoic
|
|
|
478
553
|
return null;
|
|
479
554
|
}
|
|
480
555
|
function orderState(ctx, stimulus, interaction) {
|
|
481
|
-
let
|
|
556
|
+
let submitPending;
|
|
557
|
+
let submitKey;
|
|
558
|
+
let timeoutPending;
|
|
559
|
+
function submitOrder(orderedKeys) {
|
|
560
|
+
const submission = { type: "order", orderedKeys };
|
|
561
|
+
const key = JSON.stringify(submission);
|
|
562
|
+
if (timeoutPending) {
|
|
563
|
+
return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
564
|
+
}
|
|
565
|
+
if (submitPending) {
|
|
566
|
+
if (submitKey === key) {
|
|
567
|
+
return submitPending;
|
|
568
|
+
}
|
|
569
|
+
return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
570
|
+
}
|
|
571
|
+
const validationError = validateOrderSubmission(ctx.log, orderedKeys, interaction.choices, interaction.minChoices, interaction.maxChoices);
|
|
572
|
+
if (validationError) {
|
|
573
|
+
return Promise.resolve(ctx.errored(validationError, "interaction", { kind: "interaction", submission }));
|
|
574
|
+
}
|
|
575
|
+
submitKey = key;
|
|
576
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
577
|
+
submitPending = undefined;
|
|
578
|
+
submitKey = undefined;
|
|
579
|
+
});
|
|
580
|
+
return submitPending;
|
|
581
|
+
}
|
|
582
|
+
function timeout() {
|
|
583
|
+
const intent = { kind: "timeout" };
|
|
584
|
+
if (submitPending) {
|
|
585
|
+
return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
586
|
+
}
|
|
587
|
+
if (timeoutPending) {
|
|
588
|
+
return timeoutPending;
|
|
589
|
+
}
|
|
590
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
591
|
+
timeoutPending = undefined;
|
|
592
|
+
});
|
|
593
|
+
return timeoutPending;
|
|
594
|
+
}
|
|
482
595
|
return {
|
|
483
596
|
phase: "interaction",
|
|
484
597
|
kind: "order",
|
|
@@ -487,51 +600,52 @@ function orderState(ctx, stimulus, interaction) {
|
|
|
487
600
|
choices: interaction.choices,
|
|
488
601
|
minChoices: interaction.minChoices,
|
|
489
602
|
maxChoices: interaction.maxChoices,
|
|
490
|
-
submitOrder
|
|
491
|
-
|
|
492
|
-
return pending;
|
|
493
|
-
}
|
|
494
|
-
const validationError = validateOrderSubmission(ctx.log, orderedKeys, interaction.choices, interaction.minChoices, interaction.maxChoices);
|
|
495
|
-
if (validationError) {
|
|
496
|
-
return Promise.resolve(ctx.errored(validationError, "interaction", {
|
|
497
|
-
kind: "interaction",
|
|
498
|
-
submission: { type: "order", orderedKeys }
|
|
499
|
-
}));
|
|
500
|
-
}
|
|
501
|
-
pending = ctx.execute({ kind: "interaction", submission: { type: "order", orderedKeys } }, "interaction");
|
|
502
|
-
return pending;
|
|
503
|
-
},
|
|
504
|
-
timeout: function timeout() {
|
|
505
|
-
if (pending) {
|
|
506
|
-
return pending;
|
|
507
|
-
}
|
|
508
|
-
pending = ctx.execute({ kind: "timeout" }, "timeout");
|
|
509
|
-
return pending;
|
|
510
|
-
},
|
|
603
|
+
submitOrder,
|
|
604
|
+
timeout,
|
|
511
605
|
toJSON: poisonToJSON
|
|
512
606
|
};
|
|
513
607
|
}
|
|
514
608
|
|
|
515
609
|
// src/pci-state.ts
|
|
610
|
+
import * as errors7 from "@superbuilders/errors";
|
|
516
611
|
function pciInteractionState(ctx, stimulus, interaction) {
|
|
517
|
-
let
|
|
612
|
+
let submitPending;
|
|
613
|
+
let submitKey;
|
|
614
|
+
let timeoutPending;
|
|
518
615
|
const { pciId, properties } = interaction;
|
|
519
616
|
function submit(value) {
|
|
520
|
-
|
|
521
|
-
|
|
617
|
+
const submission = { type: "portable-custom", pciId, value };
|
|
618
|
+
const key = JSON.stringify(submission);
|
|
619
|
+
if (timeoutPending) {
|
|
620
|
+
return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
621
|
+
}
|
|
622
|
+
if (submitPending) {
|
|
623
|
+
if (submitKey === key) {
|
|
624
|
+
return submitPending;
|
|
625
|
+
}
|
|
626
|
+
return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot submit a different pci payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
522
627
|
}
|
|
523
628
|
ctx.log?.debug("pci submit", { pciId });
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
629
|
+
submitKey = key;
|
|
630
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
631
|
+
submitPending = undefined;
|
|
632
|
+
submitKey = undefined;
|
|
633
|
+
});
|
|
634
|
+
return submitPending;
|
|
527
635
|
}
|
|
528
636
|
function timeout() {
|
|
529
|
-
|
|
530
|
-
|
|
637
|
+
const intent = { kind: "timeout" };
|
|
638
|
+
if (submitPending) {
|
|
639
|
+
return Promise.resolve(ctx.errored(errors7.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
640
|
+
}
|
|
641
|
+
if (timeoutPending) {
|
|
642
|
+
return timeoutPending;
|
|
531
643
|
}
|
|
532
644
|
ctx.log?.debug("pci timeout", { pciId });
|
|
533
|
-
|
|
534
|
-
|
|
645
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
646
|
+
timeoutPending = undefined;
|
|
647
|
+
});
|
|
648
|
+
return timeoutPending;
|
|
535
649
|
}
|
|
536
650
|
return {
|
|
537
651
|
phase: "interaction",
|
|
@@ -547,27 +661,50 @@ function pciInteractionState(ctx, stimulus, interaction) {
|
|
|
547
661
|
}
|
|
548
662
|
|
|
549
663
|
// src/text-entry-state.ts
|
|
664
|
+
import * as errors8 from "@superbuilders/errors";
|
|
550
665
|
function textEntryState(ctx, stimulus, interaction) {
|
|
551
|
-
let
|
|
666
|
+
let submitPending;
|
|
667
|
+
let submitKey;
|
|
668
|
+
let timeoutPending;
|
|
669
|
+
function submitText(value) {
|
|
670
|
+
const submission = { type: "text-entry", value };
|
|
671
|
+
const key = JSON.stringify(submission);
|
|
672
|
+
if (timeoutPending) {
|
|
673
|
+
return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit while timeout is in flight"), "interaction", { kind: "interaction", submission }));
|
|
674
|
+
}
|
|
675
|
+
if (submitPending) {
|
|
676
|
+
if (submitKey === key) {
|
|
677
|
+
return submitPending;
|
|
678
|
+
}
|
|
679
|
+
return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
680
|
+
}
|
|
681
|
+
submitKey = key;
|
|
682
|
+
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
683
|
+
submitPending = undefined;
|
|
684
|
+
submitKey = undefined;
|
|
685
|
+
});
|
|
686
|
+
return submitPending;
|
|
687
|
+
}
|
|
688
|
+
function timeout() {
|
|
689
|
+
const intent = { kind: "timeout" };
|
|
690
|
+
if (submitPending) {
|
|
691
|
+
return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot timeout while submission is in flight"), "timeout", intent));
|
|
692
|
+
}
|
|
693
|
+
if (timeoutPending) {
|
|
694
|
+
return timeoutPending;
|
|
695
|
+
}
|
|
696
|
+
timeoutPending = ctx.execute(intent, "timeout").finally(function clearPending() {
|
|
697
|
+
timeoutPending = undefined;
|
|
698
|
+
});
|
|
699
|
+
return timeoutPending;
|
|
700
|
+
}
|
|
552
701
|
return {
|
|
553
702
|
phase: "interaction",
|
|
554
703
|
kind: "text-entry",
|
|
555
704
|
stimulus,
|
|
556
705
|
interaction,
|
|
557
|
-
submitText
|
|
558
|
-
|
|
559
|
-
return pending;
|
|
560
|
-
}
|
|
561
|
-
pending = ctx.execute({ kind: "interaction", submission: { type: "text-entry", value } }, "interaction");
|
|
562
|
-
return pending;
|
|
563
|
-
},
|
|
564
|
-
timeout: function timeout() {
|
|
565
|
-
if (pending) {
|
|
566
|
-
return pending;
|
|
567
|
-
}
|
|
568
|
-
pending = ctx.execute({ kind: "timeout" }, "timeout");
|
|
569
|
-
return pending;
|
|
570
|
-
},
|
|
706
|
+
submitText,
|
|
707
|
+
timeout,
|
|
571
708
|
toJSON: poisonToJSON
|
|
572
709
|
};
|
|
573
710
|
}
|
|
@@ -575,19 +712,23 @@ function textEntryState(ctx, stimulus, interaction) {
|
|
|
575
712
|
// src/session.ts
|
|
576
713
|
var FATAL_SENTINELS = [
|
|
577
714
|
ErrBadRequest,
|
|
578
|
-
|
|
715
|
+
ErrInvalidAccessToken,
|
|
716
|
+
ErrTokenExpired,
|
|
579
717
|
ErrForbidden,
|
|
580
718
|
ErrNotFound,
|
|
581
719
|
ErrUnsupportedPci
|
|
582
720
|
];
|
|
583
721
|
function isFatalError(err) {
|
|
584
722
|
for (const sentinel of FATAL_SENTINELS) {
|
|
585
|
-
if (
|
|
723
|
+
if (errors9.is(err, sentinel)) {
|
|
586
724
|
return true;
|
|
587
725
|
}
|
|
588
726
|
}
|
|
589
727
|
return false;
|
|
590
728
|
}
|
|
729
|
+
function isRetriableError(err) {
|
|
730
|
+
return !errors9.is(err, ErrInvalidSubmission);
|
|
731
|
+
}
|
|
591
732
|
function makeSession(sc) {
|
|
592
733
|
const log = sc.log;
|
|
593
734
|
function resolve(result) {
|
|
@@ -595,18 +736,23 @@ function makeSession(sc) {
|
|
|
595
736
|
case "advanced":
|
|
596
737
|
return fromAdvanced(result.stimulus, result.interaction);
|
|
597
738
|
case "submitted":
|
|
598
|
-
return feedbackState(ctx, result.stimulus, result.interaction, result.submission, result.isCorrect, result.feedbackContent, result.
|
|
739
|
+
return feedbackState(ctx, result.stimulus, result.interaction, result.submission, result.isCorrect, result.feedbackContent, result.review);
|
|
599
740
|
case "completed":
|
|
600
741
|
return { phase: "completed", toJSON: poisonToJSON };
|
|
601
742
|
}
|
|
602
743
|
}
|
|
603
744
|
function errored(error, failedPhase, intent) {
|
|
604
745
|
let pending;
|
|
605
|
-
|
|
746
|
+
const retriable = isRetriableError(error);
|
|
747
|
+
const state = {
|
|
606
748
|
phase: "errored",
|
|
607
749
|
error,
|
|
608
|
-
|
|
750
|
+
retriable,
|
|
609
751
|
retry: function retry() {
|
|
752
|
+
if (!retriable) {
|
|
753
|
+
log?.debug("retry ignored for non-retriable error", { failedPhase });
|
|
754
|
+
return Promise.resolve(state);
|
|
755
|
+
}
|
|
610
756
|
if (pending) {
|
|
611
757
|
return pending;
|
|
612
758
|
}
|
|
@@ -616,18 +762,19 @@ function makeSession(sc) {
|
|
|
616
762
|
},
|
|
617
763
|
toJSON: poisonToJSON
|
|
618
764
|
};
|
|
765
|
+
return state;
|
|
619
766
|
}
|
|
620
767
|
async function execute(intent, phase) {
|
|
621
768
|
const body = {
|
|
622
|
-
studentId: sc.studentId,
|
|
623
769
|
supportedPcis: sc.supportedPcis,
|
|
624
|
-
intent
|
|
770
|
+
intent,
|
|
771
|
+
subject: sc.subject
|
|
625
772
|
};
|
|
626
773
|
const result = await sc.transport(body);
|
|
627
774
|
if (!result.ok) {
|
|
628
775
|
if (isFatalError(result.error)) {
|
|
629
776
|
log?.error("fatal transport error", { error: result.error, phase });
|
|
630
|
-
return { phase: "fatal", error: result.error, toJSON: poisonToJSON };
|
|
777
|
+
return { phase: "fatal", error: result.error, retriable: false, toJSON: poisonToJSON };
|
|
631
778
|
}
|
|
632
779
|
return errored(result.error, phase, intent);
|
|
633
780
|
}
|
|
@@ -650,7 +797,8 @@ function makeSession(sc) {
|
|
|
650
797
|
log?.error("unsupported pci in frame", { pciId: interaction.pciId });
|
|
651
798
|
return {
|
|
652
799
|
phase: "fatal",
|
|
653
|
-
error:
|
|
800
|
+
error: errors9.wrap(ErrUnsupportedPci, `pci '${interaction.pciId}'`),
|
|
801
|
+
retriable: false,
|
|
654
802
|
toJSON: poisonToJSON
|
|
655
803
|
};
|
|
656
804
|
}
|
|
@@ -678,15 +826,22 @@ function makeSession(sc) {
|
|
|
678
826
|
}
|
|
679
827
|
|
|
680
828
|
// src/client.ts
|
|
681
|
-
var
|
|
829
|
+
var ACCESS_TOKEN_PREFIX = "eyJ";
|
|
830
|
+
function isMalformedJws(token) {
|
|
831
|
+
if (!token.startsWith(ACCESS_TOKEN_PREFIX)) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
const dotCount = token.split(".").length - 1;
|
|
835
|
+
return dotCount !== 2;
|
|
836
|
+
}
|
|
682
837
|
function create(config) {
|
|
683
838
|
const log = config.logger;
|
|
684
|
-
if (
|
|
685
|
-
log?.error("malformed
|
|
686
|
-
throw
|
|
839
|
+
if (isMalformedJws(config.accessToken)) {
|
|
840
|
+
log?.error("malformed access token", { prefix: ACCESS_TOKEN_PREFIX });
|
|
841
|
+
throw errors10.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
|
|
687
842
|
}
|
|
688
843
|
const transport = createTransport({
|
|
689
|
-
|
|
844
|
+
accessToken: config.accessToken,
|
|
690
845
|
supportedPcis: config.supportedPcis,
|
|
691
846
|
origin: config.origin,
|
|
692
847
|
fetch: config.fetch,
|
|
@@ -694,27 +849,29 @@ function create(config) {
|
|
|
694
849
|
log
|
|
695
850
|
});
|
|
696
851
|
let startPromise;
|
|
697
|
-
async function doStart(
|
|
698
|
-
log?.debug("start", {
|
|
852
|
+
async function doStart() {
|
|
853
|
+
log?.debug("start", { subject: config.subject });
|
|
699
854
|
const s = makeSession({
|
|
700
|
-
studentId,
|
|
701
855
|
supportedPcis: config.supportedPcis,
|
|
856
|
+
subject: config.subject,
|
|
702
857
|
log,
|
|
703
858
|
transport
|
|
704
859
|
});
|
|
705
860
|
return s.execute({ kind: "observation" }, "observation");
|
|
706
861
|
}
|
|
707
862
|
return {
|
|
708
|
-
start(
|
|
863
|
+
start() {
|
|
709
864
|
if (startPromise) {
|
|
710
865
|
log?.debug("start already called");
|
|
711
866
|
return startPromise;
|
|
712
867
|
}
|
|
713
|
-
startPromise = doStart(
|
|
868
|
+
startPromise = doStart();
|
|
714
869
|
return startPromise;
|
|
715
870
|
}
|
|
716
871
|
};
|
|
717
872
|
}
|
|
873
|
+
// src/subject.ts
|
|
874
|
+
var SUBJECTS = ["math", "vocabulary"];
|
|
718
875
|
// src/content.ts
|
|
719
876
|
function inlinesToPlainText(nodes) {
|
|
720
877
|
const parts = [];
|
|
@@ -743,7 +900,9 @@ export {
|
|
|
743
900
|
inlinesToPlainText,
|
|
744
901
|
create,
|
|
745
902
|
blocksToPlainText,
|
|
903
|
+
SUBJECTS,
|
|
746
904
|
ErrUnsupportedPci,
|
|
905
|
+
ErrTokenExpired,
|
|
747
906
|
ErrTimeout,
|
|
748
907
|
ErrServiceUnavailable,
|
|
749
908
|
ErrServerError,
|
|
@@ -751,14 +910,14 @@ export {
|
|
|
751
910
|
ErrNotSerializable,
|
|
752
911
|
ErrNotFound,
|
|
753
912
|
ErrNetwork,
|
|
754
|
-
|
|
913
|
+
ErrMalformedAccessToken,
|
|
755
914
|
ErrJsonParse,
|
|
756
915
|
ErrInvalidSubmission,
|
|
757
|
-
|
|
916
|
+
ErrInvalidAccessToken,
|
|
758
917
|
ErrForbidden,
|
|
759
918
|
ErrConflict,
|
|
760
919
|
ErrBadRequest,
|
|
761
920
|
ADVANCE_PATH
|
|
762
921
|
};
|
|
763
922
|
|
|
764
|
-
//# debugId=
|
|
923
|
+
//# debugId=7D25EB69305CCB9264756E2164756E21
|