@playcademy/sdk 0.16.1-beta.8 → 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 +350 -111
- package/dist/internal.d.ts +59 -3
- package/dist/internal.js +373 -114
- package/dist/server/edge.d.ts +17 -1
- package/dist/server/edge.js +10 -3
- package/dist/server.d.ts +17 -1
- package/dist/server.js +10 -3
- package/dist/types.d.ts +42 -1
- package/package.json +7 -1
package/dist/internal.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",
|
|
@@ -823,10 +825,10 @@ var TIMEBACK_SUBJECTS = [
|
|
|
823
825
|
"Math",
|
|
824
826
|
"None"
|
|
825
827
|
];
|
|
826
|
-
var ASSESSMENT_PURPOSES = ["end_of_course", "diagnostic", "review"];
|
|
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
|
-
|
|
2199
|
+
return beginActivity(rawMetadata, options, owner);
|
|
2200
|
+
},
|
|
2201
|
+
tryStartAssessmentTracking(metadata, attemptId) {
|
|
2202
|
+
if (!isValidUUID(attemptId)) {
|
|
2203
|
+
return { status: "untrackable", reason: "invalid-attempt-id" };
|
|
2097
2204
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
if (
|
|
2104
|
-
|
|
2105
|
-
runId,
|
|
2106
|
-
resumeId: currentActivity.resumeId,
|
|
2107
|
-
activityData: metadata
|
|
2108
|
-
});
|
|
2205
|
+
const owner = { kind: "assessment", attemptId };
|
|
2206
|
+
const availability = clockAvailability(owner);
|
|
2207
|
+
if (typeof availability === "object") {
|
|
2208
|
+
return { status: "unavailable", reason: availability.unavailable };
|
|
2209
|
+
}
|
|
2210
|
+
if (availability === "held-by-this-owner") {
|
|
2211
|
+
return { status: "already-attached", runId: currentActivity.runId };
|
|
2109
2212
|
}
|
|
2110
|
-
return {
|
|
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,9 +3042,10 @@ 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
|
-
throw new Error("purpose must be end_of_course, diagnostic, or
|
|
3048
|
+
throw new Error("purpose must be end_of_course, diagnostic, review, or mastery");
|
|
2961
3049
|
}
|
|
2962
3050
|
if (options.grade !== undefined && !isValidGrade(options.grade)) {
|
|
2963
3051
|
throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
|
|
@@ -2965,9 +3053,99 @@ function validateAssessmentFilters(options) {
|
|
|
2965
3053
|
if (options.subject !== undefined && !isValidSubject(options.subject)) {
|
|
2966
3054
|
throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
|
|
2967
3055
|
}
|
|
3056
|
+
if (options.purpose === "mastery") {
|
|
3057
|
+
const { standard } = options;
|
|
3058
|
+
const framework = typeof standard?.framework === "string" ? standard.framework.trim() : "";
|
|
3059
|
+
const identifier = typeof standard?.identifier === "string" ? standard.identifier.trim() : "";
|
|
3060
|
+
if (!framework || !identifier) {
|
|
3061
|
+
throw new Error("mastery standard requires a canonical framework and identifier");
|
|
3062
|
+
}
|
|
3063
|
+
if (framework.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength || identifier.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength) {
|
|
3064
|
+
throw new Error(`mastery standard fields must be at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength} characters`);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2968
3067
|
}
|
|
2969
3068
|
function createTimebackNamespace(client) {
|
|
2970
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
|
+
}
|
|
2971
3149
|
registerPauseProbe(client, () => engine.activity.isManuallyPaused());
|
|
2972
3150
|
return {
|
|
2973
3151
|
assessments: {
|
|
@@ -2977,6 +3155,12 @@ function createTimebackNamespace(client) {
|
|
|
2977
3155
|
throw new Error("activityId is required");
|
|
2978
3156
|
}
|
|
2979
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
|
+
}
|
|
2980
3164
|
if (input.purpose === "review") {
|
|
2981
3165
|
if (!Array.isArray(input.standards) || input.standards.length === 0) {
|
|
2982
3166
|
throw new Error("standards must contain at least one standard for review");
|
|
@@ -2992,11 +3176,12 @@ function createTimebackNamespace(client) {
|
|
|
2992
3176
|
throw new Error(`review standard fields must be at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength} characters`);
|
|
2993
3177
|
}
|
|
2994
3178
|
}
|
|
2995
|
-
if (input.
|
|
2996
|
-
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}`);
|
|
2997
3181
|
}
|
|
2998
3182
|
}
|
|
2999
|
-
|
|
3183
|
+
const snapshot = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/start`, "POST", input);
|
|
3184
|
+
return syncAssessmentTracking(snapshot);
|
|
3000
3185
|
},
|
|
3001
3186
|
latest: async (options) => {
|
|
3002
3187
|
assertPlatformMode(client, "timeback.assessments.latest()");
|
|
@@ -3008,6 +3193,10 @@ function createTimebackNamespace(client) {
|
|
|
3008
3193
|
if (options.grade !== undefined) {
|
|
3009
3194
|
params.set("grade", String(options.grade));
|
|
3010
3195
|
}
|
|
3196
|
+
if (options.purpose === "mastery") {
|
|
3197
|
+
params.set("standardFramework", options.standard.framework);
|
|
3198
|
+
params.set("standardIdentifier", options.standard.identifier);
|
|
3199
|
+
}
|
|
3011
3200
|
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/latest?${params.toString()}`, "GET");
|
|
3012
3201
|
},
|
|
3013
3202
|
get: async (attemptId) => {
|
|
@@ -3015,7 +3204,20 @@ function createTimebackNamespace(client) {
|
|
|
3015
3204
|
if (!attemptId?.trim()) {
|
|
3016
3205
|
throw new Error("attemptId is required");
|
|
3017
3206
|
}
|
|
3018
|
-
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 };
|
|
3019
3221
|
},
|
|
3020
3222
|
save: async (attemptId, input) => {
|
|
3021
3223
|
assertPlatformMode(client, "timeback.assessments.save()");
|
|
@@ -3027,6 +3229,25 @@ function createTimebackNamespace(client) {
|
|
|
3027
3229
|
}
|
|
3028
3230
|
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/save`, "POST", input);
|
|
3029
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
|
+
},
|
|
3030
3251
|
submit: async (attemptId, input) => {
|
|
3031
3252
|
assertPlatformMode(client, "timeback.assessments.submit()");
|
|
3032
3253
|
if (!attemptId?.trim()) {
|
|
@@ -3038,7 +3259,25 @@ function createTimebackNamespace(client) {
|
|
|
3038
3259
|
if (!input.submissionId?.trim()) {
|
|
3039
3260
|
throw new Error("submissionId is required");
|
|
3040
3261
|
}
|
|
3041
|
-
|
|
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
|
+
}
|
|
3042
3281
|
}
|
|
3043
3282
|
},
|
|
3044
3283
|
get user() {
|
|
@@ -3502,7 +3741,8 @@ function createDevNamespace(client) {
|
|
|
3502
3741
|
if (options.delimiter) {
|
|
3503
3742
|
params.set("delimiter", options.delimiter);
|
|
3504
3743
|
}
|
|
3505
|
-
const
|
|
3744
|
+
const search = params.toString();
|
|
3745
|
+
const query = search === "" ? "" : `?${search}`;
|
|
3506
3746
|
return client["request"](`/games/${slug}/bucket${query}`, "GET");
|
|
3507
3747
|
}
|
|
3508
3748
|
return {
|
|
@@ -3854,6 +4094,20 @@ function createScoresNamespace2(client) {
|
|
|
3854
4094
|
}
|
|
3855
4095
|
};
|
|
3856
4096
|
}
|
|
4097
|
+
// src/namespaces/platform/assessment-assets.ts
|
|
4098
|
+
var ASSESSMENT_ASSET_UPLOAD_RETRY_POLICY = {
|
|
4099
|
+
retryableMethods: ["POST"]
|
|
4100
|
+
};
|
|
4101
|
+
function createAssessmentAssetsNamespace(client) {
|
|
4102
|
+
return {
|
|
4103
|
+
upload: (body, sha256) => client["request"]("/timeback/assessment-assets", "POST", {
|
|
4104
|
+
body,
|
|
4105
|
+
headers: { [PLAYCADEMY_ASSESSMENT_ASSET_SHA256_HEADER]: sha256 },
|
|
4106
|
+
retryPolicy: ASSESSMENT_ASSET_UPLOAD_RETRY_POLICY
|
|
4107
|
+
})
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
|
|
3857
4111
|
// src/namespaces/platform/assessments.ts
|
|
3858
4112
|
function assessmentPath(...segments) {
|
|
3859
4113
|
return `/timeback/assessments/${segments.map(encodeURIComponent).join("/")}`;
|
|
@@ -3879,16 +4133,20 @@ function createAssessmentNamespace(client) {
|
|
|
3879
4133
|
return {
|
|
3880
4134
|
list: (gameId, courseId) => client["request"](assessmentPath(gameId, courseId), "GET"),
|
|
3881
4135
|
create: (gameId, courseId, data) => client["request"](assessmentPath(gameId, courseId), "POST", { body: data }),
|
|
4136
|
+
attachExisting: (gameId, courseId, manifest) => client["request"](assessmentPath(gameId, courseId, "attachments"), "POST", {
|
|
4137
|
+
body: manifest
|
|
4138
|
+
}),
|
|
3882
4139
|
update: (gameId, courseId, testIdentifier, data) => client["request"](assessmentPath(gameId, courseId, testIdentifier), "PATCH", {
|
|
3883
4140
|
body: data
|
|
3884
4141
|
}),
|
|
4142
|
+
updateReviewMapping: (gameId, courseId, testIdentifier) => client["request"](assessmentPath(gameId, courseId, testIdentifier, "review-mapping"), "POST"),
|
|
3885
4143
|
reorder: (gameId, courseId, purpose, testIdentifiers) => client["request"](assessmentPath(gameId, courseId, "order"), "PUT", {
|
|
3886
4144
|
body: { purpose, testIdentifiers }
|
|
3887
4145
|
}),
|
|
3888
4146
|
remove: (gameId, courseId, testIdentifier) => client["request"](assessmentPath(gameId, courseId, testIdentifier), "DELETE"),
|
|
3889
4147
|
listTestLibrary: (gameId, courseId, options = {}) => client["request"](`${assessmentPath(gameId, courseId, "test-library")}${buildQtiLibraryQueryString(options)}`, "GET"),
|
|
3890
|
-
copy: (gameId, courseId,
|
|
3891
|
-
body:
|
|
4148
|
+
copy: (gameId, courseId, data) => client["request"](assessmentPath(gameId, courseId, "copies"), "POST", {
|
|
4149
|
+
body: data
|
|
3892
4150
|
}),
|
|
3893
4151
|
listQuestions: (gameId, courseId, testIdentifier) => client["request"](assessmentPath(gameId, courseId, testIdentifier, "questions"), "GET"),
|
|
3894
4152
|
listQuestionLibrary: (gameId, courseId, options = {}) => client["request"](`${assessmentPath(gameId, courseId, "question-library")}${buildQtiLibraryQueryString(options)}`, "GET"),
|
|
@@ -4078,7 +4336,8 @@ function createTimebackNamespace2(client) {
|
|
|
4078
4336
|
body: request
|
|
4079
4337
|
})
|
|
4080
4338
|
},
|
|
4081
|
-
assessments: createAssessmentNamespace(client)
|
|
4339
|
+
assessments: createAssessmentNamespace(client),
|
|
4340
|
+
assessmentAssets: createAssessmentAssetsNamespace(client)
|
|
4082
4341
|
};
|
|
4083
4342
|
}
|
|
4084
4343
|
// src/core/auth/strategies.ts
|
|
@@ -4320,7 +4579,7 @@ async function request({
|
|
|
4320
4579
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
4321
4580
|
}
|
|
4322
4581
|
// src/version.ts
|
|
4323
|
-
var SDK_VERSION = "0.
|
|
4582
|
+
var SDK_VERSION = "0.17.0";
|
|
4324
4583
|
|
|
4325
4584
|
// src/clients/base.ts
|
|
4326
4585
|
class PlaycademyBaseClient {
|