@playcademy/sdk 0.16.1-beta.9 → 0.17.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 +31 -8
- package/dist/contracts.d.ts +47 -0
- package/dist/contracts.js +23 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +333 -109
- package/dist/internal.d.ts +57 -3
- package/dist/internal.js +356 -112
- package/dist/server/edge.d.ts +17 -1
- package/dist/server/edge.js +5 -2
- package/dist/server.d.ts +17 -1
- package/dist/server.js +5 -2
- package/dist/types.d.ts +42 -1
- package/package.json +7 -1
package/dist/index.js
CHANGED
|
@@ -800,6 +800,8 @@ var DEFAULT_PERSONAL_API_KEY_PERMISSIONS = {
|
|
|
800
800
|
};
|
|
801
801
|
// ../constants/src/platform.ts
|
|
802
802
|
var PLAYCADEMY_BROWSER_TIME_ZONE_HEADER = "x-playcademy-browser-time-zone";
|
|
803
|
+
var PLAYCADEMY_ASSESSMENT_ASSET_SHA256_HEADER = "x-playcademy-content-sha256";
|
|
804
|
+
var ASSESSMENT_ASSET_MAX_BYTES = 4 * 1024 * 1024;
|
|
803
805
|
// ../constants/src/timeback.ts
|
|
804
806
|
var TIMEBACK_ROUTES = {
|
|
805
807
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
@@ -826,7 +828,7 @@ var TIMEBACK_SUBJECTS = [
|
|
|
826
828
|
var ASSESSMENT_PURPOSES = ["end_of_course", "diagnostic", "review", "mastery"];
|
|
827
829
|
var TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS = {
|
|
828
830
|
standards: 20,
|
|
829
|
-
|
|
831
|
+
candidateItemsPerStandard: 2,
|
|
830
832
|
standardFieldLength: 128
|
|
831
833
|
};
|
|
832
834
|
var VALID_E_LEVELS = ["E1", "E2", "E3", "E4"];
|
|
@@ -1119,6 +1121,9 @@ class ChildSession {
|
|
|
1119
1121
|
this.#abandon();
|
|
1120
1122
|
this.#teardown();
|
|
1121
1123
|
}
|
|
1124
|
+
get runId() {
|
|
1125
|
+
return this.#parentRunId;
|
|
1126
|
+
}
|
|
1122
1127
|
get checkpoint() {
|
|
1123
1128
|
if (!this.#hasCheckpoint) {
|
|
1124
1129
|
return null;
|
|
@@ -1203,8 +1208,11 @@ class ChildSession {
|
|
|
1203
1208
|
}
|
|
1204
1209
|
this.#markOpenWindowFlushable();
|
|
1205
1210
|
this.#forwardDirtyWindows();
|
|
1206
|
-
window
|
|
1207
|
-
|
|
1211
|
+
const win = globalThis.window;
|
|
1212
|
+
if (win) {
|
|
1213
|
+
win.removeEventListener("message", this.#onChildMessage);
|
|
1214
|
+
win.removeEventListener("pagehide", this.#onPageHide);
|
|
1215
|
+
}
|
|
1208
1216
|
messaging.unlisten("PLAYCADEMY_TOKEN_REFRESH" /* TOKEN_REFRESH */, this.#onTokenRefresh);
|
|
1209
1217
|
messaging.unlisten("PLAYCADEMY_PAUSE" /* PAUSE */, this.#onLauncherPause);
|
|
1210
1218
|
messaging.unlisten("PLAYCADEMY_RESUME" /* RESUME */, this.#onLauncherResume);
|
|
@@ -1227,6 +1235,9 @@ class ChildSession {
|
|
|
1227
1235
|
status: "abandoned",
|
|
1228
1236
|
timing: this.#relayedTiming()
|
|
1229
1237
|
};
|
|
1238
|
+
if (this.#parentRunId) {
|
|
1239
|
+
activity.runId = this.#parentRunId;
|
|
1240
|
+
}
|
|
1230
1241
|
const resume = this.checkpoint;
|
|
1231
1242
|
if (resume) {
|
|
1232
1243
|
activity.resume = resume;
|
|
@@ -1251,9 +1262,11 @@ class ChildSession {
|
|
|
1251
1262
|
}
|
|
1252
1263
|
this.#settled = true;
|
|
1253
1264
|
this.#clearResume();
|
|
1265
|
+
this.#ensureBridgeRun();
|
|
1254
1266
|
let endMemo = null;
|
|
1255
1267
|
this.#resolveFinished({
|
|
1256
1268
|
status: "completed",
|
|
1269
|
+
runId: this.#parentRunId ?? undefined,
|
|
1257
1270
|
correct: childReport.scoreData.correctQuestions,
|
|
1258
1271
|
total: childReport.scoreData.totalQuestions,
|
|
1259
1272
|
timing: this.#completedTiming(childReport),
|
|
@@ -1722,6 +1735,29 @@ function createTimebackActivityTracker(client) {
|
|
|
1722
1735
|
const resumeRunId = client["initPayload"]?.parent?.resumeRunId;
|
|
1723
1736
|
return typeof resumeRunId === "string" && isValidUUID(resumeRunId) ? resumeRunId : undefined;
|
|
1724
1737
|
}
|
|
1738
|
+
function clockAvailability(owner) {
|
|
1739
|
+
if (owner.kind === "assessment" && currentActivity?.owner.kind === "assessment" && currentActivity.owner.attemptId === owner.attemptId) {
|
|
1740
|
+
return "held-by-this-owner";
|
|
1741
|
+
}
|
|
1742
|
+
if (hasLiveSession(client)) {
|
|
1743
|
+
return { unavailable: "embedded-child" };
|
|
1744
|
+
}
|
|
1745
|
+
if (!currentActivity) {
|
|
1746
|
+
return "available";
|
|
1747
|
+
}
|
|
1748
|
+
if (owner.kind === "activity") {
|
|
1749
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1750
|
+
unavailable: "other-assessment",
|
|
1751
|
+
runId: currentActivity.runId,
|
|
1752
|
+
attemptId: currentActivity.owner.attemptId
|
|
1753
|
+
} : "available";
|
|
1754
|
+
}
|
|
1755
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1756
|
+
unavailable: "other-assessment",
|
|
1757
|
+
runId: currentActivity.runId,
|
|
1758
|
+
attemptId: currentActivity.owner.attemptId
|
|
1759
|
+
} : { unavailable: "ordinary-activity", runId: currentActivity.runId };
|
|
1760
|
+
}
|
|
1725
1761
|
function startHeartbeatInterval(activity) {
|
|
1726
1762
|
if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
|
|
1727
1763
|
return;
|
|
@@ -2033,6 +2069,110 @@ function createTimebackActivityTracker(client) {
|
|
|
2033
2069
|
}
|
|
2034
2070
|
stopRelayInterval();
|
|
2035
2071
|
}
|
|
2072
|
+
async function finishCurrentActivity(discardRelayedTiming) {
|
|
2073
|
+
const activity = currentActivity;
|
|
2074
|
+
if (activity.finishInFlight) {
|
|
2075
|
+
return activity.finishInFlight;
|
|
2076
|
+
}
|
|
2077
|
+
activity.finishInFlight = (async () => {
|
|
2078
|
+
applyOverdueInactivity();
|
|
2079
|
+
cleanupListeners();
|
|
2080
|
+
const relayedWithoutReconciliation = discardRelayedTiming && relaysToParent();
|
|
2081
|
+
await flushHeartbeat(true);
|
|
2082
|
+
if (activity.pauseStartTime !== null) {
|
|
2083
|
+
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2084
|
+
activity.pauseStartTime = null;
|
|
2085
|
+
}
|
|
2086
|
+
const endTime = Date.now();
|
|
2087
|
+
const totalElapsed = endTime - activity.startTime;
|
|
2088
|
+
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2089
|
+
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2090
|
+
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2091
|
+
if (currentActivity === activity) {
|
|
2092
|
+
currentActivity = null;
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
runId: activity.runId,
|
|
2096
|
+
resumeId: activity.resumeId,
|
|
2097
|
+
activityData: activity.metadata,
|
|
2098
|
+
durationSeconds: Math.floor(activeTime / 1000),
|
|
2099
|
+
sessionTimingData: relayedWithoutReconciliation ? { activeSeconds: 0 } : {
|
|
2100
|
+
activeSeconds: unreportedActiveMs / 1000,
|
|
2101
|
+
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
})();
|
|
2105
|
+
return activity.finishInFlight;
|
|
2106
|
+
}
|
|
2107
|
+
function assertValidRunId(options) {
|
|
2108
|
+
if (options?.runId !== undefined && !isValidUUID(options.runId)) {
|
|
2109
|
+
throw new Error(`startActivity: \`runId\` must be a UUID (received \`${JSON.stringify(options.runId)}\`). Use crypto.randomUUID() or persist a previously-generated UUID.`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function beginActivity(rawMetadata, options, owner) {
|
|
2113
|
+
const metadata = jsonProjection(rawMetadata);
|
|
2114
|
+
cleanupListeners();
|
|
2115
|
+
const now = Date.now();
|
|
2116
|
+
const runId = owner.kind === "assessment" ? owner.attemptId : options?.runId ?? adoptResumedRunId() ?? crypto.randomUUID();
|
|
2117
|
+
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2118
|
+
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2119
|
+
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2120
|
+
currentActivity = {
|
|
2121
|
+
runId,
|
|
2122
|
+
resumeId: crypto.randomUUID(),
|
|
2123
|
+
owner,
|
|
2124
|
+
startTime: now,
|
|
2125
|
+
metadata,
|
|
2126
|
+
pausedTime: 0,
|
|
2127
|
+
pauseStartTime: null,
|
|
2128
|
+
pauseReasons: new Set,
|
|
2129
|
+
pausedHeartbeatTimeoutId: null,
|
|
2130
|
+
pausedHeartbeatTimedOut: false,
|
|
2131
|
+
pausedHeartbeatTimeoutMs,
|
|
2132
|
+
windowStartTime: now,
|
|
2133
|
+
windowPausedAtStart: 0,
|
|
2134
|
+
heartbeatIntervalId: null,
|
|
2135
|
+
heartbeatIntervalMs,
|
|
2136
|
+
inactivityTimeoutId: null,
|
|
2137
|
+
inactivityTimeoutMs,
|
|
2138
|
+
inactivityTimerStartedAt: null,
|
|
2139
|
+
remainingInactivityMs: inactivityTimeoutMs,
|
|
2140
|
+
flushInFlight: null,
|
|
2141
|
+
finishInFlight: null,
|
|
2142
|
+
totalPersistedActiveMs: 0,
|
|
2143
|
+
totalPersistedPausedMs: 0
|
|
2144
|
+
};
|
|
2145
|
+
if (typeof document !== "undefined") {
|
|
2146
|
+
boundVisibilityHandler = handleVisibilityChange;
|
|
2147
|
+
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2148
|
+
boundUserInteractionHandler = handleUserInteraction;
|
|
2149
|
+
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2150
|
+
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2151
|
+
}
|
|
2152
|
+
if (document.visibilityState === "hidden") {
|
|
2153
|
+
handleVisibilityChange();
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
startHeartbeatInterval(currentActivity);
|
|
2157
|
+
startRelayInterval();
|
|
2158
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2159
|
+
boundPageHideHandler = handlePageHide;
|
|
2160
|
+
globalThis.window.addEventListener("pagehide", boundPageHideHandler);
|
|
2161
|
+
}
|
|
2162
|
+
boundShellPauseHandler = handleShellPause;
|
|
2163
|
+
boundShellResumeHandler = handleShellResume;
|
|
2164
|
+
messaging.listen("PLAYCADEMY_PAUSE" /* PAUSE */, boundShellPauseHandler);
|
|
2165
|
+
messaging.listen("PLAYCADEMY_RESUME" /* RESUME */, boundShellResumeHandler);
|
|
2166
|
+
syncInactivityTracking();
|
|
2167
|
+
if (relaysToParent()) {
|
|
2168
|
+
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_START" /* TIMEBACK_ACTIVITY_START */, {
|
|
2169
|
+
runId,
|
|
2170
|
+
resumeId: currentActivity.resumeId,
|
|
2171
|
+
activityData: metadata
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
return { runId };
|
|
2175
|
+
}
|
|
2036
2176
|
return {
|
|
2037
2177
|
currentRunId() {
|
|
2038
2178
|
return currentActivity?.runId;
|
|
@@ -2040,74 +2180,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2040
2180
|
isActivityManuallyPaused() {
|
|
2041
2181
|
return currentActivity?.pauseReasons.has("manual") ?? false;
|
|
2042
2182
|
},
|
|
2043
|
-
startActivity(rawMetadata, options) {
|
|
2044
|
-
|
|
2183
|
+
startActivity(rawMetadata, options, owner = { kind: "activity" }) {
|
|
2184
|
+
const availability = clockAvailability(owner);
|
|
2185
|
+
if (typeof availability === "object" && availability.unavailable === "embedded-child") {
|
|
2045
2186
|
throw new PlaycademyError("startActivity() is unavailable while an embedded child session is live: the child owns the clock, and a second clock would double-count the lesson. Await the session's finished (or close() it) first.");
|
|
2046
2187
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2188
|
+
assertValidRunId(options);
|
|
2189
|
+
if (availability === "held-by-this-owner") {
|
|
2190
|
+
return { runId: currentActivity.runId };
|
|
2049
2191
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const resumedRunId = adoptResumedRunId();
|
|
2054
|
-
const runId = options?.runId ?? resumedRunId ?? crypto.randomUUID();
|
|
2055
|
-
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2056
|
-
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2057
|
-
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2058
|
-
currentActivity = {
|
|
2059
|
-
runId,
|
|
2060
|
-
resumeId: crypto.randomUUID(),
|
|
2061
|
-
startTime: now,
|
|
2062
|
-
metadata,
|
|
2063
|
-
pausedTime: 0,
|
|
2064
|
-
pauseStartTime: null,
|
|
2065
|
-
pauseReasons: new Set,
|
|
2066
|
-
pausedHeartbeatTimeoutId: null,
|
|
2067
|
-
pausedHeartbeatTimedOut: false,
|
|
2068
|
-
pausedHeartbeatTimeoutMs,
|
|
2069
|
-
windowStartTime: now,
|
|
2070
|
-
windowPausedAtStart: 0,
|
|
2071
|
-
heartbeatIntervalId: null,
|
|
2072
|
-
heartbeatIntervalMs,
|
|
2073
|
-
inactivityTimeoutId: null,
|
|
2074
|
-
inactivityTimeoutMs,
|
|
2075
|
-
inactivityTimerStartedAt: null,
|
|
2076
|
-
remainingInactivityMs: inactivityTimeoutMs,
|
|
2077
|
-
flushInFlight: null,
|
|
2078
|
-
totalPersistedActiveMs: 0,
|
|
2079
|
-
totalPersistedPausedMs: 0
|
|
2080
|
-
};
|
|
2081
|
-
if (typeof document !== "undefined") {
|
|
2082
|
-
boundVisibilityHandler = handleVisibilityChange;
|
|
2083
|
-
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2084
|
-
boundUserInteractionHandler = handleUserInteraction;
|
|
2085
|
-
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2086
|
-
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2087
|
-
}
|
|
2088
|
-
if (document.visibilityState === "hidden") {
|
|
2089
|
-
handleVisibilityChange();
|
|
2192
|
+
if (typeof availability === "object") {
|
|
2193
|
+
if (owner.kind === "assessment") {
|
|
2194
|
+
throw new Error(`Cannot track assessment ${owner.attemptId} while activity run ${availability.runId} is active. End the current activity first.`);
|
|
2090
2195
|
}
|
|
2196
|
+
const blockingAssessment = availability;
|
|
2197
|
+
throw new Error(`Cannot start an activity while assessment ${blockingAssessment.attemptId} is in progress. Submit the assessment first.`);
|
|
2091
2198
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
syncInactivityTracking();
|
|
2103
|
-
if (relaysToParent()) {
|
|
2104
|
-
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_START" /* TIMEBACK_ACTIVITY_START */, {
|
|
2105
|
-
runId,
|
|
2106
|
-
resumeId: currentActivity.resumeId,
|
|
2107
|
-
activityData: metadata
|
|
2108
|
-
});
|
|
2199
|
+
return beginActivity(rawMetadata, options, owner);
|
|
2200
|
+
},
|
|
2201
|
+
tryStartAssessmentTracking(metadata, attemptId) {
|
|
2202
|
+
if (!isValidUUID(attemptId)) {
|
|
2203
|
+
return { status: "untrackable", reason: "invalid-attempt-id" };
|
|
2204
|
+
}
|
|
2205
|
+
const owner = { kind: "assessment", attemptId };
|
|
2206
|
+
const availability = clockAvailability(owner);
|
|
2207
|
+
if (typeof availability === "object") {
|
|
2208
|
+
return { status: "unavailable", reason: availability.unavailable };
|
|
2109
2209
|
}
|
|
2110
|
-
|
|
2210
|
+
if (availability === "held-by-this-owner") {
|
|
2211
|
+
return { status: "already-attached", runId: currentActivity.runId };
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
status: "attached",
|
|
2215
|
+
runId: beginActivity(metadata, { runId: attemptId }, owner).runId
|
|
2216
|
+
};
|
|
2111
2217
|
},
|
|
2112
2218
|
pauseActivity() {
|
|
2113
2219
|
if (!currentActivity) {
|
|
@@ -2130,47 +2236,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2130
2236
|
}
|
|
2131
2237
|
removePauseReason("manual");
|
|
2132
2238
|
},
|
|
2239
|
+
async finishAssessmentTracking(expectedAttemptId) {
|
|
2240
|
+
if (!currentActivity || currentActivity.owner.kind !== "assessment" || currentActivity.owner.attemptId !== expectedAttemptId) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
return finishCurrentActivity(true);
|
|
2244
|
+
},
|
|
2133
2245
|
async endActivity(rawData) {
|
|
2134
2246
|
if (!currentActivity) {
|
|
2135
2247
|
throw new Error("No activity in progress. Call startActivity() before endActivity().");
|
|
2136
2248
|
}
|
|
2249
|
+
if (currentActivity.owner.kind === "assessment") {
|
|
2250
|
+
throw new Error(`endActivity() cannot finalize assessment ${currentActivity.owner.attemptId}. Submit it with timeback.assessments.submit() instead.`);
|
|
2251
|
+
}
|
|
2137
2252
|
const data = jsonProjection(rawData);
|
|
2138
2253
|
if (!relaysToParent() && typeof data.xpAwarded !== "number") {
|
|
2139
2254
|
throw new Error("endActivity() requires xpAwarded when reporting directly. It is optional only in child mode, where the parent game decides the award.");
|
|
2140
2255
|
}
|
|
2141
|
-
const activity = currentActivity;
|
|
2142
|
-
applyOverdueInactivity();
|
|
2143
|
-
cleanupListeners();
|
|
2144
|
-
await flushHeartbeat(true);
|
|
2145
|
-
if (activity.pauseStartTime !== null) {
|
|
2146
|
-
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2147
|
-
activity.pauseStartTime = null;
|
|
2148
|
-
}
|
|
2149
|
-
const endTime = Date.now();
|
|
2150
|
-
const totalElapsed = endTime - activity.startTime;
|
|
2151
|
-
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2152
|
-
const durationSeconds = Math.floor(activeTime / 1000);
|
|
2153
|
-
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2154
|
-
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2155
2256
|
const { correctQuestions, totalQuestions } = data;
|
|
2156
2257
|
if (data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined) {
|
|
2157
2258
|
throw new Error("Cannot provide both masteredUnits and masteredUnitsAbsolute — use one or the other");
|
|
2158
2259
|
}
|
|
2260
|
+
const tracking = await finishCurrentActivity(false);
|
|
2159
2261
|
const request = {
|
|
2160
|
-
runId:
|
|
2161
|
-
resumeId:
|
|
2162
|
-
activityData:
|
|
2262
|
+
runId: tracking.runId,
|
|
2263
|
+
resumeId: tracking.resumeId,
|
|
2264
|
+
activityData: tracking.activityData,
|
|
2163
2265
|
scoreData: {
|
|
2164
2266
|
correctQuestions,
|
|
2165
2267
|
totalQuestions
|
|
2166
2268
|
},
|
|
2167
2269
|
timingData: {
|
|
2168
|
-
durationSeconds
|
|
2169
|
-
},
|
|
2170
|
-
sessionTimingData: {
|
|
2171
|
-
activeSeconds: unreportedActiveMs / 1000,
|
|
2172
|
-
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2270
|
+
durationSeconds: tracking.durationSeconds
|
|
2173
2271
|
},
|
|
2272
|
+
sessionTimingData: tracking.sessionTimingData,
|
|
2174
2273
|
xpEarned: data.xpAwarded,
|
|
2175
2274
|
masteredUnits: data.masteredUnits,
|
|
2176
2275
|
masteredUnitsAbsolute: data.masteredUnitsAbsolute,
|
|
@@ -2178,23 +2277,9 @@ function createTimebackActivityTracker(client) {
|
|
|
2178
2277
|
};
|
|
2179
2278
|
if (relaysToParent()) {
|
|
2180
2279
|
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_END" /* TIMEBACK_ACTIVITY_END */, request);
|
|
2181
|
-
|
|
2182
|
-
currentActivity = null;
|
|
2183
|
-
}
|
|
2184
|
-
return { status: "relayed", runId: activity.runId };
|
|
2185
|
-
}
|
|
2186
|
-
try {
|
|
2187
|
-
const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2188
|
-
if (currentActivity === activity) {
|
|
2189
|
-
currentActivity = null;
|
|
2190
|
-
}
|
|
2191
|
-
return response;
|
|
2192
|
-
} catch (error) {
|
|
2193
|
-
if (currentActivity === activity) {
|
|
2194
|
-
currentActivity = null;
|
|
2195
|
-
}
|
|
2196
|
-
throw error;
|
|
2280
|
+
return { status: "relayed", runId: tracking.runId };
|
|
2197
2281
|
}
|
|
2282
|
+
return client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2198
2283
|
}
|
|
2199
2284
|
};
|
|
2200
2285
|
}
|
|
@@ -2927,8 +3012,10 @@ function createTimebackEngine(client) {
|
|
|
2927
3012
|
currentRunId: activityTracker.currentRunId,
|
|
2928
3013
|
isManuallyPaused: activityTracker.isActivityManuallyPaused,
|
|
2929
3014
|
start: activityTracker.startActivity,
|
|
3015
|
+
tryStartAssessment: activityTracker.tryStartAssessmentTracking,
|
|
2930
3016
|
pause: activityTracker.pauseActivity,
|
|
2931
3017
|
resume: activityTracker.resumeActivity,
|
|
3018
|
+
finishAssessment: activityTracker.finishAssessmentTracking,
|
|
2932
3019
|
end: activityTracker.endActivity
|
|
2933
3020
|
},
|
|
2934
3021
|
course: {
|
|
@@ -2955,6 +3042,7 @@ function createTimebackEngine(client) {
|
|
|
2955
3042
|
var VALID_XP_INCLUDE_OPTIONS = ["perCourse", "today"];
|
|
2956
3043
|
var VALID_MASTERY_INCLUDE_OPTIONS = ["perCourse"];
|
|
2957
3044
|
var ASSESSMENTS_ROUTE = TIMEBACK_ROUTES.ASSESSMENTS;
|
|
3045
|
+
var ASSESSMENT_SUBMIT_RETRY_POLICY = END_ACTIVITY_RETRY_POLICY;
|
|
2958
3046
|
function validateAssessmentFilters(options) {
|
|
2959
3047
|
if (!isAssessmentPurpose(options?.purpose)) {
|
|
2960
3048
|
throw new Error("purpose must be end_of_course, diagnostic, review, or mastery");
|
|
@@ -2979,6 +3067,85 @@ function validateAssessmentFilters(options) {
|
|
|
2979
3067
|
}
|
|
2980
3068
|
function createTimebackNamespace(client) {
|
|
2981
3069
|
const engine = createTimebackEngine(client);
|
|
3070
|
+
const pendingAssessmentSubmissions = new Map;
|
|
3071
|
+
const warnedAssessmentTrackingSkips = new Map;
|
|
3072
|
+
function clearAssessmentTrackingWarnings(attemptId) {
|
|
3073
|
+
warnedAssessmentTrackingSkips.delete(attemptId);
|
|
3074
|
+
}
|
|
3075
|
+
function warnAssessmentTrackingSkipped(attemptId, reason) {
|
|
3076
|
+
const warnedReasons = warnedAssessmentTrackingSkips.get(attemptId) ?? new Set;
|
|
3077
|
+
if (warnedReasons.has(reason)) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
warnedReasons.add(reason);
|
|
3081
|
+
warnedAssessmentTrackingSkips.set(attemptId, warnedReasons);
|
|
3082
|
+
const remedy = reason === "invalid-attempt-id" ? "The canonical attempt remains usable and submittable, but this sitting cannot be timed. Report the malformed id." : "The canonical attempt remains available; release the current clock owner, then call timeback.assessments.get(attemptId) to retry tracking.";
|
|
3083
|
+
console.warn(`[Playcademy SDK] timeback.assessments: active-time tracking was not attached to assessment ${attemptId} (${reason}). ${remedy}`);
|
|
3084
|
+
}
|
|
3085
|
+
function releasePendingSubmission(attemptId, submissionId) {
|
|
3086
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId === submissionId) {
|
|
3087
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function loadAttemptSnapshot(attemptId) {
|
|
3091
|
+
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}`, "GET");
|
|
3092
|
+
}
|
|
3093
|
+
async function syncAssessmentTracking(snapshot) {
|
|
3094
|
+
if (pendingAssessmentSubmissions.has(snapshot.attemptId)) {
|
|
3095
|
+
return snapshot;
|
|
3096
|
+
}
|
|
3097
|
+
if (snapshot.status === "completed") {
|
|
3098
|
+
await engine.activity.finishAssessment(snapshot.attemptId);
|
|
3099
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3100
|
+
return snapshot;
|
|
3101
|
+
}
|
|
3102
|
+
if (!snapshot.activityData) {
|
|
3103
|
+
return snapshot;
|
|
3104
|
+
}
|
|
3105
|
+
const attachment = engine.activity.tryStartAssessment(snapshot.activityData, snapshot.attemptId);
|
|
3106
|
+
if (attachment.status === "unavailable" || attachment.status === "untrackable") {
|
|
3107
|
+
warnAssessmentTrackingSkipped(snapshot.attemptId, attachment.reason);
|
|
3108
|
+
} else {
|
|
3109
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3110
|
+
}
|
|
3111
|
+
return snapshot;
|
|
3112
|
+
}
|
|
3113
|
+
async function resumeAfterDefinitiveFailure(attemptId, submissionId) {
|
|
3114
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId !== submissionId) {
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3118
|
+
try {
|
|
3119
|
+
await syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3120
|
+
} catch {}
|
|
3121
|
+
}
|
|
3122
|
+
async function assessmentSubmissionContext(attemptId, submissionId) {
|
|
3123
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3124
|
+
if (pending) {
|
|
3125
|
+
if (pending.submissionId !== submissionId && !pending.detached) {
|
|
3126
|
+
throw new Error(`Assessment ${attemptId} already has a pending submission. Retry with the original submissionId.`);
|
|
3127
|
+
}
|
|
3128
|
+
pending.detached = false;
|
|
3129
|
+
return {
|
|
3130
|
+
submissionId: pending.submissionId,
|
|
3131
|
+
session: await pending.session
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
const session = engine.activity.finishAssessment(attemptId).then((finished) => finished ? {
|
|
3135
|
+
runId: finished.runId,
|
|
3136
|
+
resumeId: finished.resumeId,
|
|
3137
|
+
...finished.sessionTimingData
|
|
3138
|
+
} : undefined).catch((error) => {
|
|
3139
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3140
|
+
throw error;
|
|
3141
|
+
});
|
|
3142
|
+
pendingAssessmentSubmissions.set(attemptId, {
|
|
3143
|
+
submissionId,
|
|
3144
|
+
session,
|
|
3145
|
+
detached: false
|
|
3146
|
+
});
|
|
3147
|
+
return { submissionId, session: await session };
|
|
3148
|
+
}
|
|
2982
3149
|
registerPauseProbe(client, () => engine.activity.isManuallyPaused());
|
|
2983
3150
|
return {
|
|
2984
3151
|
assessments: {
|
|
@@ -2988,6 +3155,12 @@ function createTimebackNamespace(client) {
|
|
|
2988
3155
|
throw new Error("activityId is required");
|
|
2989
3156
|
}
|
|
2990
3157
|
validateAssessmentFilters(input);
|
|
3158
|
+
if (input.purpose === "diagnostic" && !input.diagnosticKey?.trim()) {
|
|
3159
|
+
throw new Error("diagnosticKey is required for diagnostic assessments");
|
|
3160
|
+
}
|
|
3161
|
+
if (input.purpose === "diagnostic" && input.diagnosticKey.trim().length > 200) {
|
|
3162
|
+
throw new Error("diagnosticKey must contain at most 200 characters");
|
|
3163
|
+
}
|
|
2991
3164
|
if (input.purpose === "review") {
|
|
2992
3165
|
if (!Array.isArray(input.standards) || input.standards.length === 0) {
|
|
2993
3166
|
throw new Error("standards must contain at least one standard for review");
|
|
@@ -3003,11 +3176,12 @@ function createTimebackNamespace(client) {
|
|
|
3003
3176
|
throw new Error(`review standard fields must be at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength} characters`);
|
|
3004
3177
|
}
|
|
3005
3178
|
}
|
|
3006
|
-
if (input.
|
|
3007
|
-
throw new Error(`
|
|
3179
|
+
if (input.candidateItemsPerStandard !== undefined && (!Number.isInteger(input.candidateItemsPerStandard) || input.candidateItemsPerStandard <= 0 || input.candidateItemsPerStandard > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard)) {
|
|
3180
|
+
throw new Error(`candidateItemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard}`);
|
|
3008
3181
|
}
|
|
3009
3182
|
}
|
|
3010
|
-
|
|
3183
|
+
const snapshot = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/start`, "POST", input);
|
|
3184
|
+
return syncAssessmentTracking(snapshot);
|
|
3011
3185
|
},
|
|
3012
3186
|
latest: async (options) => {
|
|
3013
3187
|
assertPlatformMode(client, "timeback.assessments.latest()");
|
|
@@ -3030,7 +3204,20 @@ function createTimebackNamespace(client) {
|
|
|
3030
3204
|
if (!attemptId?.trim()) {
|
|
3031
3205
|
throw new Error("attemptId is required");
|
|
3032
3206
|
}
|
|
3033
|
-
return
|
|
3207
|
+
return syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3208
|
+
},
|
|
3209
|
+
stop: async (attemptId) => {
|
|
3210
|
+
assertPlatformMode(client, "timeback.assessments.stop()");
|
|
3211
|
+
if (!attemptId?.trim()) {
|
|
3212
|
+
throw new Error("attemptId is required");
|
|
3213
|
+
}
|
|
3214
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3215
|
+
if (pending) {
|
|
3216
|
+
pending.detached = true;
|
|
3217
|
+
}
|
|
3218
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3219
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3220
|
+
return { attemptId };
|
|
3034
3221
|
},
|
|
3035
3222
|
save: async (attemptId, input) => {
|
|
3036
3223
|
assertPlatformMode(client, "timeback.assessments.save()");
|
|
@@ -3042,6 +3229,25 @@ function createTimebackNamespace(client) {
|
|
|
3042
3229
|
}
|
|
3043
3230
|
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/save`, "POST", input);
|
|
3044
3231
|
},
|
|
3232
|
+
submitItem: async (attemptId, input) => {
|
|
3233
|
+
assertPlatformMode(client, "timeback.assessments.submitItem()");
|
|
3234
|
+
if (!attemptId?.trim()) {
|
|
3235
|
+
throw new Error("attemptId is required");
|
|
3236
|
+
}
|
|
3237
|
+
if (!Number.isInteger(input.expectedResponseVersion) || input.expectedResponseVersion < 0) {
|
|
3238
|
+
throw new Error("expectedResponseVersion must be a non-negative integer");
|
|
3239
|
+
}
|
|
3240
|
+
if (!input.submissionId?.trim()) {
|
|
3241
|
+
throw new Error("submissionId is required");
|
|
3242
|
+
}
|
|
3243
|
+
if (!input.itemIdentifier?.trim()) {
|
|
3244
|
+
throw new Error("itemIdentifier is required");
|
|
3245
|
+
}
|
|
3246
|
+
if (input.routingNodeKey !== undefined && !input.routingNodeKey.trim()) {
|
|
3247
|
+
throw new Error("routingNodeKey must be non-empty when provided");
|
|
3248
|
+
}
|
|
3249
|
+
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/submit-item`, "POST", input);
|
|
3250
|
+
},
|
|
3045
3251
|
submit: async (attemptId, input) => {
|
|
3046
3252
|
assertPlatformMode(client, "timeback.assessments.submit()");
|
|
3047
3253
|
if (!attemptId?.trim()) {
|
|
@@ -3053,7 +3259,25 @@ function createTimebackNamespace(client) {
|
|
|
3053
3259
|
if (!input.submissionId?.trim()) {
|
|
3054
3260
|
throw new Error("submissionId is required");
|
|
3055
3261
|
}
|
|
3056
|
-
|
|
3262
|
+
const { expectedResponseVersion } = input;
|
|
3263
|
+
const { submissionId, session } = await assessmentSubmissionContext(attemptId, input.submissionId);
|
|
3264
|
+
const request = {
|
|
3265
|
+
expectedResponseVersion,
|
|
3266
|
+
submissionId,
|
|
3267
|
+
...session ? { session } : {}
|
|
3268
|
+
};
|
|
3269
|
+
try {
|
|
3270
|
+
const result = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/submit`, "POST", request, undefined, { retryPolicy: ASSESSMENT_SUBMIT_RETRY_POLICY });
|
|
3271
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3272
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3273
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3274
|
+
return result;
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
if (error instanceof ApiError && !error.isRetryable()) {
|
|
3277
|
+
await resumeAfterDefinitiveFailure(attemptId, submissionId);
|
|
3278
|
+
}
|
|
3279
|
+
throw error;
|
|
3280
|
+
}
|
|
3057
3281
|
}
|
|
3058
3282
|
},
|
|
3059
3283
|
get user() {
|
|
@@ -3460,7 +3684,7 @@ async function request({
|
|
|
3460
3684
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
3461
3685
|
}
|
|
3462
3686
|
// src/version.ts
|
|
3463
|
-
var SDK_VERSION = "0.
|
|
3687
|
+
var SDK_VERSION = "0.17.0";
|
|
3464
3688
|
|
|
3465
3689
|
// src/clients/base.ts
|
|
3466
3690
|
class PlaycademyBaseClient {
|