@playcademy/sdk 0.16.1-beta.13 → 0.16.1-beta.15
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/dist/contracts.d.ts +47 -0
- package/dist/contracts.js +23 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +290 -104
- package/dist/internal.d.ts +15 -0
- package/dist/internal.js +289 -103
- package/dist/server/edge.d.ts +16 -1
- package/dist/server/edge.js +1 -1
- package/dist/server.d.ts +16 -1
- package/dist/server.js +1 -1
- package/dist/types.d.ts +18 -0
- package/package.json +7 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child-catalog contract: what a child game publishes about its
|
|
3
|
+
* deliverable lessons, per lesson id and level. A parent course authority
|
|
4
|
+
* resolves its lesson references against this document at compile time and
|
|
5
|
+
* fails closed at runtime on anything not `ready` in production.
|
|
6
|
+
*
|
|
7
|
+
* Ownership and flow: each child repo generates `.playcademy/catalog.json`
|
|
8
|
+
* from its own registry (`playcademy catalog generate`); a sync workflow
|
|
9
|
+
* carries copies into parent repos. Children never read the parent's
|
|
10
|
+
* compiled authority — this file and the launch payload are the entire
|
|
11
|
+
* surface between them.
|
|
12
|
+
*
|
|
13
|
+
* The document deliberately carries no app identity. The consuming repo
|
|
14
|
+
* assigns the namespace key (e.g. 'form', 'math-cakes') from sync
|
|
15
|
+
* provenance via its own repo → app map, so a child cannot misdeclare
|
|
16
|
+
* who it is and a typo cannot mint a phantom namespace.
|
|
17
|
+
*/
|
|
18
|
+
/** Contract identifier carried by every catalog document. */
|
|
19
|
+
declare const CHILD_CATALOG_CONTRACT: 'playcademy-child-catalog-v1';
|
|
20
|
+
/** The completion-evidence contract a catalog declares its runs report. */
|
|
21
|
+
declare const CHILD_ATTEMPT_CONTRACT: 'playcademy-child-attempt-v1';
|
|
22
|
+
/** Delivery levels a catalog may declare (serving-id dialect, lowercase). */
|
|
23
|
+
declare const CATALOG_LEVELS: readonly ['e1', 'e2', 'e3', 'e4'];
|
|
24
|
+
type CatalogLevel = (typeof CATALOG_LEVELS)[number];
|
|
25
|
+
/**
|
|
26
|
+
* ready: real content. simulated: placeholder — playable for preview and
|
|
27
|
+
* simulation, never creditable in production. unavailable: intentionally
|
|
28
|
+
* absent at this level. Consumers must treat unknown values and missing
|
|
29
|
+
* levels as unavailable (fail closed).
|
|
30
|
+
*/
|
|
31
|
+
declare const CATALOG_READINESS: readonly ['ready', 'simulated', 'unavailable'];
|
|
32
|
+
type CatalogReadiness = (typeof CATALOG_READINESS)[number];
|
|
33
|
+
interface ChildCatalog {
|
|
34
|
+
/** The contract identifier. */
|
|
35
|
+
contract: typeof CHILD_CATALOG_CONTRACT;
|
|
36
|
+
/** The completion-evidence contract a catalog declares its runs report. */
|
|
37
|
+
evidence: typeof CHILD_ATTEMPT_CONTRACT;
|
|
38
|
+
/** The tool that generated this document (repo-relative path). */
|
|
39
|
+
generatedBy: string;
|
|
40
|
+
/** The in-repo source of truth the document was derived from. */
|
|
41
|
+
generatedFrom: string;
|
|
42
|
+
/** Lesson id → level → readiness. */
|
|
43
|
+
deliveries: Record<string, Partial<Record<CatalogLevel, CatalogReadiness>>>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { CATALOG_LEVELS, CATALOG_READINESS, CHILD_ATTEMPT_CONTRACT, CHILD_CATALOG_CONTRACT };
|
|
47
|
+
export type { CatalogLevel, CatalogReadiness, ChildCatalog };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
|
|
13
|
+
// src/contracts/child-catalog.ts
|
|
14
|
+
var CHILD_CATALOG_CONTRACT = "playcademy-child-catalog-v1";
|
|
15
|
+
var CHILD_ATTEMPT_CONTRACT = "playcademy-child-attempt-v1";
|
|
16
|
+
var CATALOG_LEVELS = ["e1", "e2", "e3", "e4"];
|
|
17
|
+
var CATALOG_READINESS = ["ready", "simulated", "unavailable"];
|
|
18
|
+
export {
|
|
19
|
+
CHILD_CATALOG_CONTRACT,
|
|
20
|
+
CHILD_ATTEMPT_CONTRACT,
|
|
21
|
+
CATALOG_READINESS,
|
|
22
|
+
CATALOG_LEVELS
|
|
23
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1558,6 +1558,9 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
|
|
|
1558
1558
|
start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
1559
1559
|
latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
|
|
1560
1560
|
get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
1561
|
+
stop: (attemptId: string) => Promise<{
|
|
1562
|
+
attemptId: string;
|
|
1563
|
+
}>;
|
|
1561
1564
|
save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
|
|
1562
1565
|
submitItem: (attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
|
|
1563
1566
|
submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
|
package/dist/index.js
CHANGED
|
@@ -1730,6 +1730,29 @@ function createTimebackActivityTracker(client) {
|
|
|
1730
1730
|
const resumeRunId = client["initPayload"]?.parent?.resumeRunId;
|
|
1731
1731
|
return typeof resumeRunId === "string" && isValidUUID(resumeRunId) ? resumeRunId : undefined;
|
|
1732
1732
|
}
|
|
1733
|
+
function clockAvailability(owner) {
|
|
1734
|
+
if (owner.kind === "assessment" && currentActivity?.owner.kind === "assessment" && currentActivity.owner.attemptId === owner.attemptId) {
|
|
1735
|
+
return "held-by-this-owner";
|
|
1736
|
+
}
|
|
1737
|
+
if (hasLiveSession(client)) {
|
|
1738
|
+
return { unavailable: "embedded-child" };
|
|
1739
|
+
}
|
|
1740
|
+
if (!currentActivity) {
|
|
1741
|
+
return "available";
|
|
1742
|
+
}
|
|
1743
|
+
if (owner.kind === "activity") {
|
|
1744
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1745
|
+
unavailable: "other-assessment",
|
|
1746
|
+
runId: currentActivity.runId,
|
|
1747
|
+
attemptId: currentActivity.owner.attemptId
|
|
1748
|
+
} : "available";
|
|
1749
|
+
}
|
|
1750
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1751
|
+
unavailable: "other-assessment",
|
|
1752
|
+
runId: currentActivity.runId,
|
|
1753
|
+
attemptId: currentActivity.owner.attemptId
|
|
1754
|
+
} : { unavailable: "ordinary-activity", runId: currentActivity.runId };
|
|
1755
|
+
}
|
|
1733
1756
|
function startHeartbeatInterval(activity) {
|
|
1734
1757
|
if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
|
|
1735
1758
|
return;
|
|
@@ -2041,6 +2064,110 @@ function createTimebackActivityTracker(client) {
|
|
|
2041
2064
|
}
|
|
2042
2065
|
stopRelayInterval();
|
|
2043
2066
|
}
|
|
2067
|
+
async function finishCurrentActivity(discardRelayedTiming) {
|
|
2068
|
+
const activity = currentActivity;
|
|
2069
|
+
if (activity.finishInFlight) {
|
|
2070
|
+
return activity.finishInFlight;
|
|
2071
|
+
}
|
|
2072
|
+
activity.finishInFlight = (async () => {
|
|
2073
|
+
applyOverdueInactivity();
|
|
2074
|
+
cleanupListeners();
|
|
2075
|
+
const relayedWithoutReconciliation = discardRelayedTiming && relaysToParent();
|
|
2076
|
+
await flushHeartbeat(true);
|
|
2077
|
+
if (activity.pauseStartTime !== null) {
|
|
2078
|
+
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2079
|
+
activity.pauseStartTime = null;
|
|
2080
|
+
}
|
|
2081
|
+
const endTime = Date.now();
|
|
2082
|
+
const totalElapsed = endTime - activity.startTime;
|
|
2083
|
+
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2084
|
+
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2085
|
+
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2086
|
+
if (currentActivity === activity) {
|
|
2087
|
+
currentActivity = null;
|
|
2088
|
+
}
|
|
2089
|
+
return {
|
|
2090
|
+
runId: activity.runId,
|
|
2091
|
+
resumeId: activity.resumeId,
|
|
2092
|
+
activityData: activity.metadata,
|
|
2093
|
+
durationSeconds: Math.floor(activeTime / 1000),
|
|
2094
|
+
sessionTimingData: relayedWithoutReconciliation ? { activeSeconds: 0 } : {
|
|
2095
|
+
activeSeconds: unreportedActiveMs / 1000,
|
|
2096
|
+
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
})();
|
|
2100
|
+
return activity.finishInFlight;
|
|
2101
|
+
}
|
|
2102
|
+
function assertValidRunId(options) {
|
|
2103
|
+
if (options?.runId !== undefined && !isValidUUID(options.runId)) {
|
|
2104
|
+
throw new Error(`startActivity: \`runId\` must be a UUID (received \`${JSON.stringify(options.runId)}\`). Use crypto.randomUUID() or persist a previously-generated UUID.`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function beginActivity(rawMetadata, options, owner) {
|
|
2108
|
+
const metadata = jsonProjection(rawMetadata);
|
|
2109
|
+
cleanupListeners();
|
|
2110
|
+
const now = Date.now();
|
|
2111
|
+
const runId = owner.kind === "assessment" ? owner.attemptId : options?.runId ?? adoptResumedRunId() ?? crypto.randomUUID();
|
|
2112
|
+
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2113
|
+
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2114
|
+
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2115
|
+
currentActivity = {
|
|
2116
|
+
runId,
|
|
2117
|
+
resumeId: crypto.randomUUID(),
|
|
2118
|
+
owner,
|
|
2119
|
+
startTime: now,
|
|
2120
|
+
metadata,
|
|
2121
|
+
pausedTime: 0,
|
|
2122
|
+
pauseStartTime: null,
|
|
2123
|
+
pauseReasons: new Set,
|
|
2124
|
+
pausedHeartbeatTimeoutId: null,
|
|
2125
|
+
pausedHeartbeatTimedOut: false,
|
|
2126
|
+
pausedHeartbeatTimeoutMs,
|
|
2127
|
+
windowStartTime: now,
|
|
2128
|
+
windowPausedAtStart: 0,
|
|
2129
|
+
heartbeatIntervalId: null,
|
|
2130
|
+
heartbeatIntervalMs,
|
|
2131
|
+
inactivityTimeoutId: null,
|
|
2132
|
+
inactivityTimeoutMs,
|
|
2133
|
+
inactivityTimerStartedAt: null,
|
|
2134
|
+
remainingInactivityMs: inactivityTimeoutMs,
|
|
2135
|
+
flushInFlight: null,
|
|
2136
|
+
finishInFlight: null,
|
|
2137
|
+
totalPersistedActiveMs: 0,
|
|
2138
|
+
totalPersistedPausedMs: 0
|
|
2139
|
+
};
|
|
2140
|
+
if (typeof document !== "undefined") {
|
|
2141
|
+
boundVisibilityHandler = handleVisibilityChange;
|
|
2142
|
+
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2143
|
+
boundUserInteractionHandler = handleUserInteraction;
|
|
2144
|
+
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2145
|
+
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2146
|
+
}
|
|
2147
|
+
if (document.visibilityState === "hidden") {
|
|
2148
|
+
handleVisibilityChange();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
startHeartbeatInterval(currentActivity);
|
|
2152
|
+
startRelayInterval();
|
|
2153
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2154
|
+
boundPageHideHandler = handlePageHide;
|
|
2155
|
+
globalThis.window.addEventListener("pagehide", boundPageHideHandler);
|
|
2156
|
+
}
|
|
2157
|
+
boundShellPauseHandler = handleShellPause;
|
|
2158
|
+
boundShellResumeHandler = handleShellResume;
|
|
2159
|
+
messaging.listen("PLAYCADEMY_PAUSE" /* PAUSE */, boundShellPauseHandler);
|
|
2160
|
+
messaging.listen("PLAYCADEMY_RESUME" /* RESUME */, boundShellResumeHandler);
|
|
2161
|
+
syncInactivityTracking();
|
|
2162
|
+
if (relaysToParent()) {
|
|
2163
|
+
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_START" /* TIMEBACK_ACTIVITY_START */, {
|
|
2164
|
+
runId,
|
|
2165
|
+
resumeId: currentActivity.resumeId,
|
|
2166
|
+
activityData: metadata
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
return { runId };
|
|
2170
|
+
}
|
|
2044
2171
|
return {
|
|
2045
2172
|
currentRunId() {
|
|
2046
2173
|
return currentActivity?.runId;
|
|
@@ -2048,74 +2175,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2048
2175
|
isActivityManuallyPaused() {
|
|
2049
2176
|
return currentActivity?.pauseReasons.has("manual") ?? false;
|
|
2050
2177
|
},
|
|
2051
|
-
startActivity(rawMetadata, options) {
|
|
2052
|
-
|
|
2178
|
+
startActivity(rawMetadata, options, owner = { kind: "activity" }) {
|
|
2179
|
+
const availability = clockAvailability(owner);
|
|
2180
|
+
if (typeof availability === "object" && availability.unavailable === "embedded-child") {
|
|
2053
2181
|
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.");
|
|
2054
2182
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2183
|
+
assertValidRunId(options);
|
|
2184
|
+
if (availability === "held-by-this-owner") {
|
|
2185
|
+
return { runId: currentActivity.runId };
|
|
2057
2186
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
const resumedRunId = adoptResumedRunId();
|
|
2062
|
-
const runId = options?.runId ?? resumedRunId ?? crypto.randomUUID();
|
|
2063
|
-
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2064
|
-
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2065
|
-
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2066
|
-
currentActivity = {
|
|
2067
|
-
runId,
|
|
2068
|
-
resumeId: crypto.randomUUID(),
|
|
2069
|
-
startTime: now,
|
|
2070
|
-
metadata,
|
|
2071
|
-
pausedTime: 0,
|
|
2072
|
-
pauseStartTime: null,
|
|
2073
|
-
pauseReasons: new Set,
|
|
2074
|
-
pausedHeartbeatTimeoutId: null,
|
|
2075
|
-
pausedHeartbeatTimedOut: false,
|
|
2076
|
-
pausedHeartbeatTimeoutMs,
|
|
2077
|
-
windowStartTime: now,
|
|
2078
|
-
windowPausedAtStart: 0,
|
|
2079
|
-
heartbeatIntervalId: null,
|
|
2080
|
-
heartbeatIntervalMs,
|
|
2081
|
-
inactivityTimeoutId: null,
|
|
2082
|
-
inactivityTimeoutMs,
|
|
2083
|
-
inactivityTimerStartedAt: null,
|
|
2084
|
-
remainingInactivityMs: inactivityTimeoutMs,
|
|
2085
|
-
flushInFlight: null,
|
|
2086
|
-
totalPersistedActiveMs: 0,
|
|
2087
|
-
totalPersistedPausedMs: 0
|
|
2088
|
-
};
|
|
2089
|
-
if (typeof document !== "undefined") {
|
|
2090
|
-
boundVisibilityHandler = handleVisibilityChange;
|
|
2091
|
-
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2092
|
-
boundUserInteractionHandler = handleUserInteraction;
|
|
2093
|
-
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2094
|
-
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2095
|
-
}
|
|
2096
|
-
if (document.visibilityState === "hidden") {
|
|
2097
|
-
handleVisibilityChange();
|
|
2187
|
+
if (typeof availability === "object") {
|
|
2188
|
+
if (owner.kind === "assessment") {
|
|
2189
|
+
throw new Error(`Cannot track assessment ${owner.attemptId} while activity run ${availability.runId} is active. End the current activity first.`);
|
|
2098
2190
|
}
|
|
2191
|
+
const blockingAssessment = availability;
|
|
2192
|
+
throw new Error(`Cannot start an activity while assessment ${blockingAssessment.attemptId} is in progress. Submit the assessment first.`);
|
|
2099
2193
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
syncInactivityTracking();
|
|
2111
|
-
if (relaysToParent()) {
|
|
2112
|
-
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_START" /* TIMEBACK_ACTIVITY_START */, {
|
|
2113
|
-
runId,
|
|
2114
|
-
resumeId: currentActivity.resumeId,
|
|
2115
|
-
activityData: metadata
|
|
2116
|
-
});
|
|
2194
|
+
return beginActivity(rawMetadata, options, owner);
|
|
2195
|
+
},
|
|
2196
|
+
tryStartAssessmentTracking(metadata, attemptId) {
|
|
2197
|
+
if (!isValidUUID(attemptId)) {
|
|
2198
|
+
return { status: "untrackable", reason: "invalid-attempt-id" };
|
|
2199
|
+
}
|
|
2200
|
+
const owner = { kind: "assessment", attemptId };
|
|
2201
|
+
const availability = clockAvailability(owner);
|
|
2202
|
+
if (typeof availability === "object") {
|
|
2203
|
+
return { status: "unavailable", reason: availability.unavailable };
|
|
2117
2204
|
}
|
|
2118
|
-
|
|
2205
|
+
if (availability === "held-by-this-owner") {
|
|
2206
|
+
return { status: "already-attached", runId: currentActivity.runId };
|
|
2207
|
+
}
|
|
2208
|
+
return {
|
|
2209
|
+
status: "attached",
|
|
2210
|
+
runId: beginActivity(metadata, { runId: attemptId }, owner).runId
|
|
2211
|
+
};
|
|
2119
2212
|
},
|
|
2120
2213
|
pauseActivity() {
|
|
2121
2214
|
if (!currentActivity) {
|
|
@@ -2138,47 +2231,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2138
2231
|
}
|
|
2139
2232
|
removePauseReason("manual");
|
|
2140
2233
|
},
|
|
2234
|
+
async finishAssessmentTracking(expectedAttemptId) {
|
|
2235
|
+
if (!currentActivity || currentActivity.owner.kind !== "assessment" || currentActivity.owner.attemptId !== expectedAttemptId) {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
return finishCurrentActivity(true);
|
|
2239
|
+
},
|
|
2141
2240
|
async endActivity(rawData) {
|
|
2142
2241
|
if (!currentActivity) {
|
|
2143
2242
|
throw new Error("No activity in progress. Call startActivity() before endActivity().");
|
|
2144
2243
|
}
|
|
2244
|
+
if (currentActivity.owner.kind === "assessment") {
|
|
2245
|
+
throw new Error(`endActivity() cannot finalize assessment ${currentActivity.owner.attemptId}. Submit it with timeback.assessments.submit() instead.`);
|
|
2246
|
+
}
|
|
2145
2247
|
const data = jsonProjection(rawData);
|
|
2146
2248
|
if (!relaysToParent() && typeof data.xpAwarded !== "number") {
|
|
2147
2249
|
throw new Error("endActivity() requires xpAwarded when reporting directly. It is optional only in child mode, where the parent game decides the award.");
|
|
2148
2250
|
}
|
|
2149
|
-
const activity = currentActivity;
|
|
2150
|
-
applyOverdueInactivity();
|
|
2151
|
-
cleanupListeners();
|
|
2152
|
-
await flushHeartbeat(true);
|
|
2153
|
-
if (activity.pauseStartTime !== null) {
|
|
2154
|
-
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2155
|
-
activity.pauseStartTime = null;
|
|
2156
|
-
}
|
|
2157
|
-
const endTime = Date.now();
|
|
2158
|
-
const totalElapsed = endTime - activity.startTime;
|
|
2159
|
-
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2160
|
-
const durationSeconds = Math.floor(activeTime / 1000);
|
|
2161
|
-
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2162
|
-
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2163
2251
|
const { correctQuestions, totalQuestions } = data;
|
|
2164
2252
|
if (data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined) {
|
|
2165
2253
|
throw new Error("Cannot provide both masteredUnits and masteredUnitsAbsolute — use one or the other");
|
|
2166
2254
|
}
|
|
2255
|
+
const tracking = await finishCurrentActivity(false);
|
|
2167
2256
|
const request = {
|
|
2168
|
-
runId:
|
|
2169
|
-
resumeId:
|
|
2170
|
-
activityData:
|
|
2257
|
+
runId: tracking.runId,
|
|
2258
|
+
resumeId: tracking.resumeId,
|
|
2259
|
+
activityData: tracking.activityData,
|
|
2171
2260
|
scoreData: {
|
|
2172
2261
|
correctQuestions,
|
|
2173
2262
|
totalQuestions
|
|
2174
2263
|
},
|
|
2175
2264
|
timingData: {
|
|
2176
|
-
durationSeconds
|
|
2177
|
-
},
|
|
2178
|
-
sessionTimingData: {
|
|
2179
|
-
activeSeconds: unreportedActiveMs / 1000,
|
|
2180
|
-
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2265
|
+
durationSeconds: tracking.durationSeconds
|
|
2181
2266
|
},
|
|
2267
|
+
sessionTimingData: tracking.sessionTimingData,
|
|
2182
2268
|
xpEarned: data.xpAwarded,
|
|
2183
2269
|
masteredUnits: data.masteredUnits,
|
|
2184
2270
|
masteredUnitsAbsolute: data.masteredUnitsAbsolute,
|
|
@@ -2186,23 +2272,9 @@ function createTimebackActivityTracker(client) {
|
|
|
2186
2272
|
};
|
|
2187
2273
|
if (relaysToParent()) {
|
|
2188
2274
|
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_END" /* TIMEBACK_ACTIVITY_END */, request);
|
|
2189
|
-
|
|
2190
|
-
currentActivity = null;
|
|
2191
|
-
}
|
|
2192
|
-
return { status: "relayed", runId: activity.runId };
|
|
2193
|
-
}
|
|
2194
|
-
try {
|
|
2195
|
-
const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2196
|
-
if (currentActivity === activity) {
|
|
2197
|
-
currentActivity = null;
|
|
2198
|
-
}
|
|
2199
|
-
return response;
|
|
2200
|
-
} catch (error) {
|
|
2201
|
-
if (currentActivity === activity) {
|
|
2202
|
-
currentActivity = null;
|
|
2203
|
-
}
|
|
2204
|
-
throw error;
|
|
2275
|
+
return { status: "relayed", runId: tracking.runId };
|
|
2205
2276
|
}
|
|
2277
|
+
return client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2206
2278
|
}
|
|
2207
2279
|
};
|
|
2208
2280
|
}
|
|
@@ -2935,8 +3007,10 @@ function createTimebackEngine(client) {
|
|
|
2935
3007
|
currentRunId: activityTracker.currentRunId,
|
|
2936
3008
|
isManuallyPaused: activityTracker.isActivityManuallyPaused,
|
|
2937
3009
|
start: activityTracker.startActivity,
|
|
3010
|
+
tryStartAssessment: activityTracker.tryStartAssessmentTracking,
|
|
2938
3011
|
pause: activityTracker.pauseActivity,
|
|
2939
3012
|
resume: activityTracker.resumeActivity,
|
|
3013
|
+
finishAssessment: activityTracker.finishAssessmentTracking,
|
|
2940
3014
|
end: activityTracker.endActivity
|
|
2941
3015
|
},
|
|
2942
3016
|
course: {
|
|
@@ -2963,6 +3037,7 @@ function createTimebackEngine(client) {
|
|
|
2963
3037
|
var VALID_XP_INCLUDE_OPTIONS = ["perCourse", "today"];
|
|
2964
3038
|
var VALID_MASTERY_INCLUDE_OPTIONS = ["perCourse"];
|
|
2965
3039
|
var ASSESSMENTS_ROUTE = TIMEBACK_ROUTES.ASSESSMENTS;
|
|
3040
|
+
var ASSESSMENT_SUBMIT_RETRY_POLICY = END_ACTIVITY_RETRY_POLICY;
|
|
2966
3041
|
function validateAssessmentFilters(options) {
|
|
2967
3042
|
if (!isAssessmentPurpose(options?.purpose)) {
|
|
2968
3043
|
throw new Error("purpose must be end_of_course, diagnostic, review, or mastery");
|
|
@@ -2987,6 +3062,85 @@ function validateAssessmentFilters(options) {
|
|
|
2987
3062
|
}
|
|
2988
3063
|
function createTimebackNamespace(client) {
|
|
2989
3064
|
const engine = createTimebackEngine(client);
|
|
3065
|
+
const pendingAssessmentSubmissions = new Map;
|
|
3066
|
+
const warnedAssessmentTrackingSkips = new Map;
|
|
3067
|
+
function clearAssessmentTrackingWarnings(attemptId) {
|
|
3068
|
+
warnedAssessmentTrackingSkips.delete(attemptId);
|
|
3069
|
+
}
|
|
3070
|
+
function warnAssessmentTrackingSkipped(attemptId, reason) {
|
|
3071
|
+
const warnedReasons = warnedAssessmentTrackingSkips.get(attemptId) ?? new Set;
|
|
3072
|
+
if (warnedReasons.has(reason)) {
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
warnedReasons.add(reason);
|
|
3076
|
+
warnedAssessmentTrackingSkips.set(attemptId, warnedReasons);
|
|
3077
|
+
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.";
|
|
3078
|
+
console.warn(`[Playcademy SDK] timeback.assessments: active-time tracking was not attached to assessment ${attemptId} (${reason}). ${remedy}`);
|
|
3079
|
+
}
|
|
3080
|
+
function releasePendingSubmission(attemptId, submissionId) {
|
|
3081
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId === submissionId) {
|
|
3082
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function loadAttemptSnapshot(attemptId) {
|
|
3086
|
+
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}`, "GET");
|
|
3087
|
+
}
|
|
3088
|
+
async function syncAssessmentTracking(snapshot) {
|
|
3089
|
+
if (pendingAssessmentSubmissions.has(snapshot.attemptId)) {
|
|
3090
|
+
return snapshot;
|
|
3091
|
+
}
|
|
3092
|
+
if (snapshot.status === "completed") {
|
|
3093
|
+
await engine.activity.finishAssessment(snapshot.attemptId);
|
|
3094
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3095
|
+
return snapshot;
|
|
3096
|
+
}
|
|
3097
|
+
if (!snapshot.activityData) {
|
|
3098
|
+
return snapshot;
|
|
3099
|
+
}
|
|
3100
|
+
const attachment = engine.activity.tryStartAssessment(snapshot.activityData, snapshot.attemptId);
|
|
3101
|
+
if (attachment.status === "unavailable" || attachment.status === "untrackable") {
|
|
3102
|
+
warnAssessmentTrackingSkipped(snapshot.attemptId, attachment.reason);
|
|
3103
|
+
} else {
|
|
3104
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3105
|
+
}
|
|
3106
|
+
return snapshot;
|
|
3107
|
+
}
|
|
3108
|
+
async function resumeAfterDefinitiveFailure(attemptId, submissionId) {
|
|
3109
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId !== submissionId) {
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3113
|
+
try {
|
|
3114
|
+
await syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3115
|
+
} catch {}
|
|
3116
|
+
}
|
|
3117
|
+
async function assessmentSubmissionContext(attemptId, submissionId) {
|
|
3118
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3119
|
+
if (pending) {
|
|
3120
|
+
if (pending.submissionId !== submissionId && !pending.detached) {
|
|
3121
|
+
throw new Error(`Assessment ${attemptId} already has a pending submission. Retry with the original submissionId.`);
|
|
3122
|
+
}
|
|
3123
|
+
pending.detached = false;
|
|
3124
|
+
return {
|
|
3125
|
+
submissionId: pending.submissionId,
|
|
3126
|
+
session: await pending.session
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
const session = engine.activity.finishAssessment(attemptId).then((finished) => finished ? {
|
|
3130
|
+
runId: finished.runId,
|
|
3131
|
+
resumeId: finished.resumeId,
|
|
3132
|
+
...finished.sessionTimingData
|
|
3133
|
+
} : undefined).catch((error) => {
|
|
3134
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3135
|
+
throw error;
|
|
3136
|
+
});
|
|
3137
|
+
pendingAssessmentSubmissions.set(attemptId, {
|
|
3138
|
+
submissionId,
|
|
3139
|
+
session,
|
|
3140
|
+
detached: false
|
|
3141
|
+
});
|
|
3142
|
+
return { submissionId, session: await session };
|
|
3143
|
+
}
|
|
2990
3144
|
registerPauseProbe(client, () => engine.activity.isManuallyPaused());
|
|
2991
3145
|
return {
|
|
2992
3146
|
assessments: {
|
|
@@ -3015,7 +3169,8 @@ function createTimebackNamespace(client) {
|
|
|
3015
3169
|
throw new Error(`itemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.itemsPerStandard}`);
|
|
3016
3170
|
}
|
|
3017
3171
|
}
|
|
3018
|
-
|
|
3172
|
+
const snapshot = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/start`, "POST", input);
|
|
3173
|
+
return syncAssessmentTracking(snapshot);
|
|
3019
3174
|
},
|
|
3020
3175
|
latest: async (options) => {
|
|
3021
3176
|
assertPlatformMode(client, "timeback.assessments.latest()");
|
|
@@ -3038,7 +3193,20 @@ function createTimebackNamespace(client) {
|
|
|
3038
3193
|
if (!attemptId?.trim()) {
|
|
3039
3194
|
throw new Error("attemptId is required");
|
|
3040
3195
|
}
|
|
3041
|
-
return
|
|
3196
|
+
return syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3197
|
+
},
|
|
3198
|
+
stop: async (attemptId) => {
|
|
3199
|
+
assertPlatformMode(client, "timeback.assessments.stop()");
|
|
3200
|
+
if (!attemptId?.trim()) {
|
|
3201
|
+
throw new Error("attemptId is required");
|
|
3202
|
+
}
|
|
3203
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3204
|
+
if (pending) {
|
|
3205
|
+
pending.detached = true;
|
|
3206
|
+
}
|
|
3207
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3208
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3209
|
+
return { attemptId };
|
|
3042
3210
|
},
|
|
3043
3211
|
save: async (attemptId, input) => {
|
|
3044
3212
|
assertPlatformMode(client, "timeback.assessments.save()");
|
|
@@ -3077,7 +3245,25 @@ function createTimebackNamespace(client) {
|
|
|
3077
3245
|
if (!input.submissionId?.trim()) {
|
|
3078
3246
|
throw new Error("submissionId is required");
|
|
3079
3247
|
}
|
|
3080
|
-
|
|
3248
|
+
const { expectedResponseVersion } = input;
|
|
3249
|
+
const { submissionId, session } = await assessmentSubmissionContext(attemptId, input.submissionId);
|
|
3250
|
+
const request = {
|
|
3251
|
+
expectedResponseVersion,
|
|
3252
|
+
submissionId,
|
|
3253
|
+
...session ? { session } : {}
|
|
3254
|
+
};
|
|
3255
|
+
try {
|
|
3256
|
+
const result = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/submit`, "POST", request, undefined, { retryPolicy: ASSESSMENT_SUBMIT_RETRY_POLICY });
|
|
3257
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3258
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3259
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3260
|
+
return result;
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
if (error instanceof ApiError && !error.isRetryable()) {
|
|
3263
|
+
await resumeAfterDefinitiveFailure(attemptId, submissionId);
|
|
3264
|
+
}
|
|
3265
|
+
throw error;
|
|
3266
|
+
}
|
|
3081
3267
|
}
|
|
3082
3268
|
},
|
|
3083
3269
|
get user() {
|
|
@@ -3484,7 +3670,7 @@ async function request({
|
|
|
3484
3670
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
3485
3671
|
}
|
|
3486
3672
|
// src/version.ts
|
|
3487
|
-
var SDK_VERSION = "0.16.1-beta.
|
|
3673
|
+
var SDK_VERSION = "0.16.1-beta.15";
|
|
3488
3674
|
|
|
3489
3675
|
// src/clients/base.ts
|
|
3490
3676
|
class PlaycademyBaseClient {
|
package/dist/internal.d.ts
CHANGED
|
@@ -3489,6 +3489,19 @@ interface DashboardThemeConfig {
|
|
|
3489
3489
|
/** Secondary/accent color as a hex string, e.g. '#ffd166' */
|
|
3490
3490
|
secondary?: string;
|
|
3491
3491
|
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Child-catalog contract generation (see @playcademy/sdk/contracts).
|
|
3494
|
+
* The extractor is a repo-local script (run with the CLI's runtime) that
|
|
3495
|
+
* prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
|
|
3496
|
+
* validates, stamps the contract fields, and writes
|
|
3497
|
+
* `.playcademy/catalog.json` deterministically. The document carries no
|
|
3498
|
+
* app identity — consuming repos assign the namespace key from sync
|
|
3499
|
+
* provenance.
|
|
3500
|
+
*/
|
|
3501
|
+
interface CatalogConfig {
|
|
3502
|
+
/** Path to the extractor script, relative to the config file. */
|
|
3503
|
+
extractor: string;
|
|
3504
|
+
}
|
|
3492
3505
|
/**
|
|
3493
3506
|
* Unified Playcademy configuration
|
|
3494
3507
|
* Used for playcademy.config.{js,json}
|
|
@@ -3521,6 +3534,8 @@ interface PlaycademyConfig {
|
|
|
3521
3534
|
dashboard?: DashboardConfig | boolean;
|
|
3522
3535
|
/** Integrations (database, custom routes, external services) */
|
|
3523
3536
|
integrations?: IntegrationsConfig;
|
|
3537
|
+
/** Child-catalog contract generation (`playcademy catalog`) */
|
|
3538
|
+
catalog?: CatalogConfig;
|
|
3524
3539
|
}
|
|
3525
3540
|
|
|
3526
3541
|
/**
|
package/dist/internal.js
CHANGED
|
@@ -1730,6 +1730,29 @@ function createTimebackActivityTracker(client) {
|
|
|
1730
1730
|
const resumeRunId = client["initPayload"]?.parent?.resumeRunId;
|
|
1731
1731
|
return typeof resumeRunId === "string" && isValidUUID(resumeRunId) ? resumeRunId : undefined;
|
|
1732
1732
|
}
|
|
1733
|
+
function clockAvailability(owner) {
|
|
1734
|
+
if (owner.kind === "assessment" && currentActivity?.owner.kind === "assessment" && currentActivity.owner.attemptId === owner.attemptId) {
|
|
1735
|
+
return "held-by-this-owner";
|
|
1736
|
+
}
|
|
1737
|
+
if (hasLiveSession(client)) {
|
|
1738
|
+
return { unavailable: "embedded-child" };
|
|
1739
|
+
}
|
|
1740
|
+
if (!currentActivity) {
|
|
1741
|
+
return "available";
|
|
1742
|
+
}
|
|
1743
|
+
if (owner.kind === "activity") {
|
|
1744
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1745
|
+
unavailable: "other-assessment",
|
|
1746
|
+
runId: currentActivity.runId,
|
|
1747
|
+
attemptId: currentActivity.owner.attemptId
|
|
1748
|
+
} : "available";
|
|
1749
|
+
}
|
|
1750
|
+
return currentActivity.owner.kind === "assessment" ? {
|
|
1751
|
+
unavailable: "other-assessment",
|
|
1752
|
+
runId: currentActivity.runId,
|
|
1753
|
+
attemptId: currentActivity.owner.attemptId
|
|
1754
|
+
} : { unavailable: "ordinary-activity", runId: currentActivity.runId };
|
|
1755
|
+
}
|
|
1733
1756
|
function startHeartbeatInterval(activity) {
|
|
1734
1757
|
if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
|
|
1735
1758
|
return;
|
|
@@ -2041,6 +2064,110 @@ function createTimebackActivityTracker(client) {
|
|
|
2041
2064
|
}
|
|
2042
2065
|
stopRelayInterval();
|
|
2043
2066
|
}
|
|
2067
|
+
async function finishCurrentActivity(discardRelayedTiming) {
|
|
2068
|
+
const activity = currentActivity;
|
|
2069
|
+
if (activity.finishInFlight) {
|
|
2070
|
+
return activity.finishInFlight;
|
|
2071
|
+
}
|
|
2072
|
+
activity.finishInFlight = (async () => {
|
|
2073
|
+
applyOverdueInactivity();
|
|
2074
|
+
cleanupListeners();
|
|
2075
|
+
const relayedWithoutReconciliation = discardRelayedTiming && relaysToParent();
|
|
2076
|
+
await flushHeartbeat(true);
|
|
2077
|
+
if (activity.pauseStartTime !== null) {
|
|
2078
|
+
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2079
|
+
activity.pauseStartTime = null;
|
|
2080
|
+
}
|
|
2081
|
+
const endTime = Date.now();
|
|
2082
|
+
const totalElapsed = endTime - activity.startTime;
|
|
2083
|
+
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2084
|
+
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2085
|
+
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2086
|
+
if (currentActivity === activity) {
|
|
2087
|
+
currentActivity = null;
|
|
2088
|
+
}
|
|
2089
|
+
return {
|
|
2090
|
+
runId: activity.runId,
|
|
2091
|
+
resumeId: activity.resumeId,
|
|
2092
|
+
activityData: activity.metadata,
|
|
2093
|
+
durationSeconds: Math.floor(activeTime / 1000),
|
|
2094
|
+
sessionTimingData: relayedWithoutReconciliation ? { activeSeconds: 0 } : {
|
|
2095
|
+
activeSeconds: unreportedActiveMs / 1000,
|
|
2096
|
+
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
})();
|
|
2100
|
+
return activity.finishInFlight;
|
|
2101
|
+
}
|
|
2102
|
+
function assertValidRunId(options) {
|
|
2103
|
+
if (options?.runId !== undefined && !isValidUUID(options.runId)) {
|
|
2104
|
+
throw new Error(`startActivity: \`runId\` must be a UUID (received \`${JSON.stringify(options.runId)}\`). Use crypto.randomUUID() or persist a previously-generated UUID.`);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function beginActivity(rawMetadata, options, owner) {
|
|
2108
|
+
const metadata = jsonProjection(rawMetadata);
|
|
2109
|
+
cleanupListeners();
|
|
2110
|
+
const now = Date.now();
|
|
2111
|
+
const runId = owner.kind === "assessment" ? owner.attemptId : options?.runId ?? adoptResumedRunId() ?? crypto.randomUUID();
|
|
2112
|
+
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2113
|
+
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2114
|
+
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2115
|
+
currentActivity = {
|
|
2116
|
+
runId,
|
|
2117
|
+
resumeId: crypto.randomUUID(),
|
|
2118
|
+
owner,
|
|
2119
|
+
startTime: now,
|
|
2120
|
+
metadata,
|
|
2121
|
+
pausedTime: 0,
|
|
2122
|
+
pauseStartTime: null,
|
|
2123
|
+
pauseReasons: new Set,
|
|
2124
|
+
pausedHeartbeatTimeoutId: null,
|
|
2125
|
+
pausedHeartbeatTimedOut: false,
|
|
2126
|
+
pausedHeartbeatTimeoutMs,
|
|
2127
|
+
windowStartTime: now,
|
|
2128
|
+
windowPausedAtStart: 0,
|
|
2129
|
+
heartbeatIntervalId: null,
|
|
2130
|
+
heartbeatIntervalMs,
|
|
2131
|
+
inactivityTimeoutId: null,
|
|
2132
|
+
inactivityTimeoutMs,
|
|
2133
|
+
inactivityTimerStartedAt: null,
|
|
2134
|
+
remainingInactivityMs: inactivityTimeoutMs,
|
|
2135
|
+
flushInFlight: null,
|
|
2136
|
+
finishInFlight: null,
|
|
2137
|
+
totalPersistedActiveMs: 0,
|
|
2138
|
+
totalPersistedPausedMs: 0
|
|
2139
|
+
};
|
|
2140
|
+
if (typeof document !== "undefined") {
|
|
2141
|
+
boundVisibilityHandler = handleVisibilityChange;
|
|
2142
|
+
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2143
|
+
boundUserInteractionHandler = handleUserInteraction;
|
|
2144
|
+
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2145
|
+
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2146
|
+
}
|
|
2147
|
+
if (document.visibilityState === "hidden") {
|
|
2148
|
+
handleVisibilityChange();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
startHeartbeatInterval(currentActivity);
|
|
2152
|
+
startRelayInterval();
|
|
2153
|
+
if (typeof globalThis.window !== "undefined") {
|
|
2154
|
+
boundPageHideHandler = handlePageHide;
|
|
2155
|
+
globalThis.window.addEventListener("pagehide", boundPageHideHandler);
|
|
2156
|
+
}
|
|
2157
|
+
boundShellPauseHandler = handleShellPause;
|
|
2158
|
+
boundShellResumeHandler = handleShellResume;
|
|
2159
|
+
messaging.listen("PLAYCADEMY_PAUSE" /* PAUSE */, boundShellPauseHandler);
|
|
2160
|
+
messaging.listen("PLAYCADEMY_RESUME" /* RESUME */, boundShellResumeHandler);
|
|
2161
|
+
syncInactivityTracking();
|
|
2162
|
+
if (relaysToParent()) {
|
|
2163
|
+
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_START" /* TIMEBACK_ACTIVITY_START */, {
|
|
2164
|
+
runId,
|
|
2165
|
+
resumeId: currentActivity.resumeId,
|
|
2166
|
+
activityData: metadata
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
return { runId };
|
|
2170
|
+
}
|
|
2044
2171
|
return {
|
|
2045
2172
|
currentRunId() {
|
|
2046
2173
|
return currentActivity?.runId;
|
|
@@ -2048,74 +2175,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2048
2175
|
isActivityManuallyPaused() {
|
|
2049
2176
|
return currentActivity?.pauseReasons.has("manual") ?? false;
|
|
2050
2177
|
},
|
|
2051
|
-
startActivity(rawMetadata, options) {
|
|
2052
|
-
|
|
2178
|
+
startActivity(rawMetadata, options, owner = { kind: "activity" }) {
|
|
2179
|
+
const availability = clockAvailability(owner);
|
|
2180
|
+
if (typeof availability === "object" && availability.unavailable === "embedded-child") {
|
|
2053
2181
|
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.");
|
|
2054
2182
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2183
|
+
assertValidRunId(options);
|
|
2184
|
+
if (availability === "held-by-this-owner") {
|
|
2185
|
+
return { runId: currentActivity.runId };
|
|
2057
2186
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
const resumedRunId = adoptResumedRunId();
|
|
2062
|
-
const runId = options?.runId ?? resumedRunId ?? crypto.randomUUID();
|
|
2063
|
-
const heartbeatIntervalMs = normalizeDelayMs(options?.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS, false);
|
|
2064
|
-
const pausedHeartbeatTimeoutMs = normalizeDelayMs(options?.pausedHeartbeatTimeoutMs ?? options?.hiddenTimeoutMs, DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS, false);
|
|
2065
|
-
const inactivityTimeoutMs = normalizeDelayMs(options?.inactivityTimeoutMs, DEFAULT_INACTIVITY_TIMEOUT_MS, false);
|
|
2066
|
-
currentActivity = {
|
|
2067
|
-
runId,
|
|
2068
|
-
resumeId: crypto.randomUUID(),
|
|
2069
|
-
startTime: now,
|
|
2070
|
-
metadata,
|
|
2071
|
-
pausedTime: 0,
|
|
2072
|
-
pauseStartTime: null,
|
|
2073
|
-
pauseReasons: new Set,
|
|
2074
|
-
pausedHeartbeatTimeoutId: null,
|
|
2075
|
-
pausedHeartbeatTimedOut: false,
|
|
2076
|
-
pausedHeartbeatTimeoutMs,
|
|
2077
|
-
windowStartTime: now,
|
|
2078
|
-
windowPausedAtStart: 0,
|
|
2079
|
-
heartbeatIntervalId: null,
|
|
2080
|
-
heartbeatIntervalMs,
|
|
2081
|
-
inactivityTimeoutId: null,
|
|
2082
|
-
inactivityTimeoutMs,
|
|
2083
|
-
inactivityTimerStartedAt: null,
|
|
2084
|
-
remainingInactivityMs: inactivityTimeoutMs,
|
|
2085
|
-
flushInFlight: null,
|
|
2086
|
-
totalPersistedActiveMs: 0,
|
|
2087
|
-
totalPersistedPausedMs: 0
|
|
2088
|
-
};
|
|
2089
|
-
if (typeof document !== "undefined") {
|
|
2090
|
-
boundVisibilityHandler = handleVisibilityChange;
|
|
2091
|
-
document.addEventListener("visibilitychange", boundVisibilityHandler);
|
|
2092
|
-
boundUserInteractionHandler = handleUserInteraction;
|
|
2093
|
-
for (const eventName of USER_ACTIVITY_EVENTS) {
|
|
2094
|
-
document.addEventListener(eventName, boundUserInteractionHandler, USER_ACTIVITY_LISTENER_OPTIONS);
|
|
2095
|
-
}
|
|
2096
|
-
if (document.visibilityState === "hidden") {
|
|
2097
|
-
handleVisibilityChange();
|
|
2187
|
+
if (typeof availability === "object") {
|
|
2188
|
+
if (owner.kind === "assessment") {
|
|
2189
|
+
throw new Error(`Cannot track assessment ${owner.attemptId} while activity run ${availability.runId} is active. End the current activity first.`);
|
|
2098
2190
|
}
|
|
2191
|
+
const blockingAssessment = availability;
|
|
2192
|
+
throw new Error(`Cannot start an activity while assessment ${blockingAssessment.attemptId} is in progress. Submit the assessment first.`);
|
|
2099
2193
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2194
|
+
return beginActivity(rawMetadata, options, owner);
|
|
2195
|
+
},
|
|
2196
|
+
tryStartAssessmentTracking(metadata, attemptId) {
|
|
2197
|
+
if (!isValidUUID(attemptId)) {
|
|
2198
|
+
return { status: "untrackable", reason: "invalid-attempt-id" };
|
|
2105
2199
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
runId,
|
|
2114
|
-
resumeId: currentActivity.resumeId,
|
|
2115
|
-
activityData: metadata
|
|
2116
|
-
});
|
|
2200
|
+
const owner = { kind: "assessment", attemptId };
|
|
2201
|
+
const availability = clockAvailability(owner);
|
|
2202
|
+
if (typeof availability === "object") {
|
|
2203
|
+
return { status: "unavailable", reason: availability.unavailable };
|
|
2204
|
+
}
|
|
2205
|
+
if (availability === "held-by-this-owner") {
|
|
2206
|
+
return { status: "already-attached", runId: currentActivity.runId };
|
|
2117
2207
|
}
|
|
2118
|
-
return {
|
|
2208
|
+
return {
|
|
2209
|
+
status: "attached",
|
|
2210
|
+
runId: beginActivity(metadata, { runId: attemptId }, owner).runId
|
|
2211
|
+
};
|
|
2119
2212
|
},
|
|
2120
2213
|
pauseActivity() {
|
|
2121
2214
|
if (!currentActivity) {
|
|
@@ -2138,47 +2231,40 @@ function createTimebackActivityTracker(client) {
|
|
|
2138
2231
|
}
|
|
2139
2232
|
removePauseReason("manual");
|
|
2140
2233
|
},
|
|
2234
|
+
async finishAssessmentTracking(expectedAttemptId) {
|
|
2235
|
+
if (!currentActivity || currentActivity.owner.kind !== "assessment" || currentActivity.owner.attemptId !== expectedAttemptId) {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
return finishCurrentActivity(true);
|
|
2239
|
+
},
|
|
2141
2240
|
async endActivity(rawData) {
|
|
2142
2241
|
if (!currentActivity) {
|
|
2143
2242
|
throw new Error("No activity in progress. Call startActivity() before endActivity().");
|
|
2144
2243
|
}
|
|
2244
|
+
if (currentActivity.owner.kind === "assessment") {
|
|
2245
|
+
throw new Error(`endActivity() cannot finalize assessment ${currentActivity.owner.attemptId}. Submit it with timeback.assessments.submit() instead.`);
|
|
2246
|
+
}
|
|
2145
2247
|
const data = jsonProjection(rawData);
|
|
2146
2248
|
if (!relaysToParent() && typeof data.xpAwarded !== "number") {
|
|
2147
2249
|
throw new Error("endActivity() requires xpAwarded when reporting directly. It is optional only in child mode, where the parent game decides the award.");
|
|
2148
2250
|
}
|
|
2149
|
-
const activity = currentActivity;
|
|
2150
|
-
applyOverdueInactivity();
|
|
2151
|
-
cleanupListeners();
|
|
2152
|
-
await flushHeartbeat(true);
|
|
2153
|
-
if (activity.pauseStartTime !== null) {
|
|
2154
|
-
activity.pausedTime += Date.now() - activity.pauseStartTime;
|
|
2155
|
-
activity.pauseStartTime = null;
|
|
2156
|
-
}
|
|
2157
|
-
const endTime = Date.now();
|
|
2158
|
-
const totalElapsed = endTime - activity.startTime;
|
|
2159
|
-
const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
|
|
2160
|
-
const durationSeconds = Math.floor(activeTime / 1000);
|
|
2161
|
-
const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
|
|
2162
|
-
const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
|
|
2163
2251
|
const { correctQuestions, totalQuestions } = data;
|
|
2164
2252
|
if (data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined) {
|
|
2165
2253
|
throw new Error("Cannot provide both masteredUnits and masteredUnitsAbsolute — use one or the other");
|
|
2166
2254
|
}
|
|
2255
|
+
const tracking = await finishCurrentActivity(false);
|
|
2167
2256
|
const request = {
|
|
2168
|
-
runId:
|
|
2169
|
-
resumeId:
|
|
2170
|
-
activityData:
|
|
2257
|
+
runId: tracking.runId,
|
|
2258
|
+
resumeId: tracking.resumeId,
|
|
2259
|
+
activityData: tracking.activityData,
|
|
2171
2260
|
scoreData: {
|
|
2172
2261
|
correctQuestions,
|
|
2173
2262
|
totalQuestions
|
|
2174
2263
|
},
|
|
2175
2264
|
timingData: {
|
|
2176
|
-
durationSeconds
|
|
2177
|
-
},
|
|
2178
|
-
sessionTimingData: {
|
|
2179
|
-
activeSeconds: unreportedActiveMs / 1000,
|
|
2180
|
-
...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
|
|
2265
|
+
durationSeconds: tracking.durationSeconds
|
|
2181
2266
|
},
|
|
2267
|
+
sessionTimingData: tracking.sessionTimingData,
|
|
2182
2268
|
xpEarned: data.xpAwarded,
|
|
2183
2269
|
masteredUnits: data.masteredUnits,
|
|
2184
2270
|
masteredUnitsAbsolute: data.masteredUnitsAbsolute,
|
|
@@ -2186,23 +2272,9 @@ function createTimebackActivityTracker(client) {
|
|
|
2186
2272
|
};
|
|
2187
2273
|
if (relaysToParent()) {
|
|
2188
2274
|
messaging.send("PLAYCADEMY_TIMEBACK_ACTIVITY_END" /* TIMEBACK_ACTIVITY_END */, request);
|
|
2189
|
-
|
|
2190
|
-
currentActivity = null;
|
|
2191
|
-
}
|
|
2192
|
-
return { status: "relayed", runId: activity.runId };
|
|
2193
|
-
}
|
|
2194
|
-
try {
|
|
2195
|
-
const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2196
|
-
if (currentActivity === activity) {
|
|
2197
|
-
currentActivity = null;
|
|
2198
|
-
}
|
|
2199
|
-
return response;
|
|
2200
|
-
} catch (error) {
|
|
2201
|
-
if (currentActivity === activity) {
|
|
2202
|
-
currentActivity = null;
|
|
2203
|
-
}
|
|
2204
|
-
throw error;
|
|
2275
|
+
return { status: "relayed", runId: tracking.runId };
|
|
2205
2276
|
}
|
|
2277
|
+
return client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request, undefined, { retryPolicy: END_ACTIVITY_RETRY_POLICY });
|
|
2206
2278
|
}
|
|
2207
2279
|
};
|
|
2208
2280
|
}
|
|
@@ -2935,8 +3007,10 @@ function createTimebackEngine(client) {
|
|
|
2935
3007
|
currentRunId: activityTracker.currentRunId,
|
|
2936
3008
|
isManuallyPaused: activityTracker.isActivityManuallyPaused,
|
|
2937
3009
|
start: activityTracker.startActivity,
|
|
3010
|
+
tryStartAssessment: activityTracker.tryStartAssessmentTracking,
|
|
2938
3011
|
pause: activityTracker.pauseActivity,
|
|
2939
3012
|
resume: activityTracker.resumeActivity,
|
|
3013
|
+
finishAssessment: activityTracker.finishAssessmentTracking,
|
|
2940
3014
|
end: activityTracker.endActivity
|
|
2941
3015
|
},
|
|
2942
3016
|
course: {
|
|
@@ -2963,6 +3037,7 @@ function createTimebackEngine(client) {
|
|
|
2963
3037
|
var VALID_XP_INCLUDE_OPTIONS = ["perCourse", "today"];
|
|
2964
3038
|
var VALID_MASTERY_INCLUDE_OPTIONS = ["perCourse"];
|
|
2965
3039
|
var ASSESSMENTS_ROUTE = TIMEBACK_ROUTES.ASSESSMENTS;
|
|
3040
|
+
var ASSESSMENT_SUBMIT_RETRY_POLICY = END_ACTIVITY_RETRY_POLICY;
|
|
2966
3041
|
function validateAssessmentFilters(options) {
|
|
2967
3042
|
if (!isAssessmentPurpose(options?.purpose)) {
|
|
2968
3043
|
throw new Error("purpose must be end_of_course, diagnostic, review, or mastery");
|
|
@@ -2987,6 +3062,85 @@ function validateAssessmentFilters(options) {
|
|
|
2987
3062
|
}
|
|
2988
3063
|
function createTimebackNamespace(client) {
|
|
2989
3064
|
const engine = createTimebackEngine(client);
|
|
3065
|
+
const pendingAssessmentSubmissions = new Map;
|
|
3066
|
+
const warnedAssessmentTrackingSkips = new Map;
|
|
3067
|
+
function clearAssessmentTrackingWarnings(attemptId) {
|
|
3068
|
+
warnedAssessmentTrackingSkips.delete(attemptId);
|
|
3069
|
+
}
|
|
3070
|
+
function warnAssessmentTrackingSkipped(attemptId, reason) {
|
|
3071
|
+
const warnedReasons = warnedAssessmentTrackingSkips.get(attemptId) ?? new Set;
|
|
3072
|
+
if (warnedReasons.has(reason)) {
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
warnedReasons.add(reason);
|
|
3076
|
+
warnedAssessmentTrackingSkips.set(attemptId, warnedReasons);
|
|
3077
|
+
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.";
|
|
3078
|
+
console.warn(`[Playcademy SDK] timeback.assessments: active-time tracking was not attached to assessment ${attemptId} (${reason}). ${remedy}`);
|
|
3079
|
+
}
|
|
3080
|
+
function releasePendingSubmission(attemptId, submissionId) {
|
|
3081
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId === submissionId) {
|
|
3082
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
function loadAttemptSnapshot(attemptId) {
|
|
3086
|
+
return client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}`, "GET");
|
|
3087
|
+
}
|
|
3088
|
+
async function syncAssessmentTracking(snapshot) {
|
|
3089
|
+
if (pendingAssessmentSubmissions.has(snapshot.attemptId)) {
|
|
3090
|
+
return snapshot;
|
|
3091
|
+
}
|
|
3092
|
+
if (snapshot.status === "completed") {
|
|
3093
|
+
await engine.activity.finishAssessment(snapshot.attemptId);
|
|
3094
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3095
|
+
return snapshot;
|
|
3096
|
+
}
|
|
3097
|
+
if (!snapshot.activityData) {
|
|
3098
|
+
return snapshot;
|
|
3099
|
+
}
|
|
3100
|
+
const attachment = engine.activity.tryStartAssessment(snapshot.activityData, snapshot.attemptId);
|
|
3101
|
+
if (attachment.status === "unavailable" || attachment.status === "untrackable") {
|
|
3102
|
+
warnAssessmentTrackingSkipped(snapshot.attemptId, attachment.reason);
|
|
3103
|
+
} else {
|
|
3104
|
+
clearAssessmentTrackingWarnings(snapshot.attemptId);
|
|
3105
|
+
}
|
|
3106
|
+
return snapshot;
|
|
3107
|
+
}
|
|
3108
|
+
async function resumeAfterDefinitiveFailure(attemptId, submissionId) {
|
|
3109
|
+
if (pendingAssessmentSubmissions.get(attemptId)?.submissionId !== submissionId) {
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
pendingAssessmentSubmissions.delete(attemptId);
|
|
3113
|
+
try {
|
|
3114
|
+
await syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3115
|
+
} catch {}
|
|
3116
|
+
}
|
|
3117
|
+
async function assessmentSubmissionContext(attemptId, submissionId) {
|
|
3118
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3119
|
+
if (pending) {
|
|
3120
|
+
if (pending.submissionId !== submissionId && !pending.detached) {
|
|
3121
|
+
throw new Error(`Assessment ${attemptId} already has a pending submission. Retry with the original submissionId.`);
|
|
3122
|
+
}
|
|
3123
|
+
pending.detached = false;
|
|
3124
|
+
return {
|
|
3125
|
+
submissionId: pending.submissionId,
|
|
3126
|
+
session: await pending.session
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
const session = engine.activity.finishAssessment(attemptId).then((finished) => finished ? {
|
|
3130
|
+
runId: finished.runId,
|
|
3131
|
+
resumeId: finished.resumeId,
|
|
3132
|
+
...finished.sessionTimingData
|
|
3133
|
+
} : undefined).catch((error) => {
|
|
3134
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3135
|
+
throw error;
|
|
3136
|
+
});
|
|
3137
|
+
pendingAssessmentSubmissions.set(attemptId, {
|
|
3138
|
+
submissionId,
|
|
3139
|
+
session,
|
|
3140
|
+
detached: false
|
|
3141
|
+
});
|
|
3142
|
+
return { submissionId, session: await session };
|
|
3143
|
+
}
|
|
2990
3144
|
registerPauseProbe(client, () => engine.activity.isManuallyPaused());
|
|
2991
3145
|
return {
|
|
2992
3146
|
assessments: {
|
|
@@ -3015,7 +3169,8 @@ function createTimebackNamespace(client) {
|
|
|
3015
3169
|
throw new Error(`itemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.itemsPerStandard}`);
|
|
3016
3170
|
}
|
|
3017
3171
|
}
|
|
3018
|
-
|
|
3172
|
+
const snapshot = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/start`, "POST", input);
|
|
3173
|
+
return syncAssessmentTracking(snapshot);
|
|
3019
3174
|
},
|
|
3020
3175
|
latest: async (options) => {
|
|
3021
3176
|
assertPlatformMode(client, "timeback.assessments.latest()");
|
|
@@ -3038,7 +3193,20 @@ function createTimebackNamespace(client) {
|
|
|
3038
3193
|
if (!attemptId?.trim()) {
|
|
3039
3194
|
throw new Error("attemptId is required");
|
|
3040
3195
|
}
|
|
3041
|
-
return
|
|
3196
|
+
return syncAssessmentTracking(await loadAttemptSnapshot(attemptId));
|
|
3197
|
+
},
|
|
3198
|
+
stop: async (attemptId) => {
|
|
3199
|
+
assertPlatformMode(client, "timeback.assessments.stop()");
|
|
3200
|
+
if (!attemptId?.trim()) {
|
|
3201
|
+
throw new Error("attemptId is required");
|
|
3202
|
+
}
|
|
3203
|
+
const pending = pendingAssessmentSubmissions.get(attemptId);
|
|
3204
|
+
if (pending) {
|
|
3205
|
+
pending.detached = true;
|
|
3206
|
+
}
|
|
3207
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3208
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3209
|
+
return { attemptId };
|
|
3042
3210
|
},
|
|
3043
3211
|
save: async (attemptId, input) => {
|
|
3044
3212
|
assertPlatformMode(client, "timeback.assessments.save()");
|
|
@@ -3077,7 +3245,25 @@ function createTimebackNamespace(client) {
|
|
|
3077
3245
|
if (!input.submissionId?.trim()) {
|
|
3078
3246
|
throw new Error("submissionId is required");
|
|
3079
3247
|
}
|
|
3080
|
-
|
|
3248
|
+
const { expectedResponseVersion } = input;
|
|
3249
|
+
const { submissionId, session } = await assessmentSubmissionContext(attemptId, input.submissionId);
|
|
3250
|
+
const request = {
|
|
3251
|
+
expectedResponseVersion,
|
|
3252
|
+
submissionId,
|
|
3253
|
+
...session ? { session } : {}
|
|
3254
|
+
};
|
|
3255
|
+
try {
|
|
3256
|
+
const result = await client["requestGameBackend"](`${ASSESSMENTS_ROUTE}/${encodeURIComponent(attemptId)}/submit`, "POST", request, undefined, { retryPolicy: ASSESSMENT_SUBMIT_RETRY_POLICY });
|
|
3257
|
+
releasePendingSubmission(attemptId, submissionId);
|
|
3258
|
+
await engine.activity.finishAssessment(attemptId);
|
|
3259
|
+
clearAssessmentTrackingWarnings(attemptId);
|
|
3260
|
+
return result;
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
if (error instanceof ApiError && !error.isRetryable()) {
|
|
3263
|
+
await resumeAfterDefinitiveFailure(attemptId, submissionId);
|
|
3264
|
+
}
|
|
3265
|
+
throw error;
|
|
3266
|
+
}
|
|
3081
3267
|
}
|
|
3082
3268
|
},
|
|
3083
3269
|
get user() {
|
|
@@ -4360,7 +4546,7 @@ async function request({
|
|
|
4360
4546
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
4361
4547
|
}
|
|
4362
4548
|
// src/version.ts
|
|
4363
|
-
var SDK_VERSION = "0.16.1-beta.
|
|
4549
|
+
var SDK_VERSION = "0.16.1-beta.15";
|
|
4364
4550
|
|
|
4365
4551
|
// src/clients/base.ts
|
|
4366
4552
|
class PlaycademyBaseClient {
|
package/dist/server/edge.d.ts
CHANGED
|
@@ -142,6 +142,19 @@ interface DashboardThemeConfig {
|
|
|
142
142
|
/** Secondary/accent color as a hex string, e.g. '#ffd166' */
|
|
143
143
|
secondary?: string;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Child-catalog contract generation (see @playcademy/sdk/contracts).
|
|
147
|
+
* The extractor is a repo-local script (run with the CLI's runtime) that
|
|
148
|
+
* prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
|
|
149
|
+
* validates, stamps the contract fields, and writes
|
|
150
|
+
* `.playcademy/catalog.json` deterministically. The document carries no
|
|
151
|
+
* app identity — consuming repos assign the namespace key from sync
|
|
152
|
+
* provenance.
|
|
153
|
+
*/
|
|
154
|
+
interface CatalogConfig {
|
|
155
|
+
/** Path to the extractor script, relative to the config file. */
|
|
156
|
+
extractor: string;
|
|
157
|
+
}
|
|
145
158
|
/**
|
|
146
159
|
* Unified Playcademy configuration
|
|
147
160
|
* Used for playcademy.config.{js,json}
|
|
@@ -174,6 +187,8 @@ interface PlaycademyConfig {
|
|
|
174
187
|
dashboard?: DashboardConfig | boolean;
|
|
175
188
|
/** Integrations (database, custom routes, external services) */
|
|
176
189
|
integrations?: IntegrationsConfig;
|
|
190
|
+
/** Child-catalog contract generation (`playcademy catalog`) */
|
|
191
|
+
catalog?: CatalogConfig;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
/**
|
|
@@ -360,7 +375,7 @@ declare class PlaycademyClient {
|
|
|
360
375
|
get: (studentId: string, attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
361
376
|
save: (studentId: string, attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
|
|
362
377
|
submitItem: (studentId: string, attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
|
|
363
|
-
submit: (studentId: string, attemptId: string, input: _playcademy_types.
|
|
378
|
+
submit: (studentId: string, attemptId: string, input: _playcademy_types.SubmitAssessmentRequest, context: {
|
|
364
379
|
sensorUrl: string;
|
|
365
380
|
}) => Promise<_playcademy_types.AssessmentSubmitResult>;
|
|
366
381
|
};
|
package/dist/server/edge.js
CHANGED
package/dist/server.d.ts
CHANGED
|
@@ -142,6 +142,19 @@ interface DashboardThemeConfig {
|
|
|
142
142
|
/** Secondary/accent color as a hex string, e.g. '#ffd166' */
|
|
143
143
|
secondary?: string;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Child-catalog contract generation (see @playcademy/sdk/contracts).
|
|
147
|
+
* The extractor is a repo-local script (run with the CLI's runtime) that
|
|
148
|
+
* prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
|
|
149
|
+
* validates, stamps the contract fields, and writes
|
|
150
|
+
* `.playcademy/catalog.json` deterministically. The document carries no
|
|
151
|
+
* app identity — consuming repos assign the namespace key from sync
|
|
152
|
+
* provenance.
|
|
153
|
+
*/
|
|
154
|
+
interface CatalogConfig {
|
|
155
|
+
/** Path to the extractor script, relative to the config file. */
|
|
156
|
+
extractor: string;
|
|
157
|
+
}
|
|
145
158
|
/**
|
|
146
159
|
* Unified Playcademy configuration
|
|
147
160
|
* Used for playcademy.config.{js,json}
|
|
@@ -174,6 +187,8 @@ interface PlaycademyConfig {
|
|
|
174
187
|
dashboard?: DashboardConfig | boolean;
|
|
175
188
|
/** Integrations (database, custom routes, external services) */
|
|
176
189
|
integrations?: IntegrationsConfig;
|
|
190
|
+
/** Child-catalog contract generation (`playcademy catalog`) */
|
|
191
|
+
catalog?: CatalogConfig;
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
/**
|
|
@@ -360,7 +375,7 @@ declare class PlaycademyClient$1 {
|
|
|
360
375
|
get: (studentId: string, attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
361
376
|
save: (studentId: string, attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
|
|
362
377
|
submitItem: (studentId: string, attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
|
|
363
|
-
submit: (studentId: string, attemptId: string, input: _playcademy_types.
|
|
378
|
+
submit: (studentId: string, attemptId: string, input: _playcademy_types.SubmitAssessmentRequest, context: {
|
|
364
379
|
sensorUrl: string;
|
|
365
380
|
}) => Promise<_playcademy_types.AssessmentSubmitResult>;
|
|
366
381
|
};
|
package/dist/server.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -363,6 +363,19 @@ interface DashboardThemeConfig {
|
|
|
363
363
|
/** Secondary/accent color as a hex string, e.g. '#ffd166' */
|
|
364
364
|
secondary?: string;
|
|
365
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Child-catalog contract generation (see @playcademy/sdk/contracts).
|
|
368
|
+
* The extractor is a repo-local script (run with the CLI's runtime) that
|
|
369
|
+
* prints `{ generatedFrom, deliveries }` as JSON on stdout; the CLI
|
|
370
|
+
* validates, stamps the contract fields, and writes
|
|
371
|
+
* `.playcademy/catalog.json` deterministically. The document carries no
|
|
372
|
+
* app identity — consuming repos assign the namespace key from sync
|
|
373
|
+
* provenance.
|
|
374
|
+
*/
|
|
375
|
+
interface CatalogConfig {
|
|
376
|
+
/** Path to the extractor script, relative to the config file. */
|
|
377
|
+
extractor: string;
|
|
378
|
+
}
|
|
366
379
|
/**
|
|
367
380
|
* Unified Playcademy configuration
|
|
368
381
|
* Used for playcademy.config.{js,json}
|
|
@@ -395,6 +408,8 @@ interface PlaycademyConfig {
|
|
|
395
408
|
dashboard?: DashboardConfig | boolean;
|
|
396
409
|
/** Integrations (database, custom routes, external services) */
|
|
397
410
|
integrations?: IntegrationsConfig;
|
|
411
|
+
/** Child-catalog contract generation (`playcademy catalog`) */
|
|
412
|
+
catalog?: CatalogConfig;
|
|
398
413
|
}
|
|
399
414
|
|
|
400
415
|
/**
|
|
@@ -1999,6 +2014,9 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
|
|
|
1999
2014
|
start: (input: _playcademy_types.StartAssessmentInput) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
2000
2015
|
latest: (options: _playcademy_types.GetLatestAssessmentOptions) => Promise<_playcademy_types.LatestAssessmentResult | null>;
|
|
2001
2016
|
get: (attemptId: string) => Promise<_playcademy_types.AssessmentAttemptSnapshot>;
|
|
2017
|
+
stop: (attemptId: string) => Promise<{
|
|
2018
|
+
attemptId: string;
|
|
2019
|
+
}>;
|
|
2002
2020
|
save: (attemptId: string, input: _playcademy_types.SaveAssessmentInput) => Promise<_playcademy_types.AssessmentSaveResult>;
|
|
2003
2021
|
submitItem: (attemptId: string, input: _playcademy_types.SubmitAssessmentItemInput) => Promise<_playcademy_types.SubmitAssessmentItemResult>;
|
|
2004
2022
|
submit: (attemptId: string, input: _playcademy_types.SubmitAssessmentInput) => Promise<_playcademy_types.AssessmentSubmitResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/sdk",
|
|
3
|
-
"version": "0.16.1-beta.
|
|
3
|
+
"version": "0.16.1-beta.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
"import": "./dist/types.js",
|
|
22
22
|
"require": "./dist/types.js"
|
|
23
23
|
},
|
|
24
|
+
"./contracts": {
|
|
25
|
+
"source": "./src/contracts.ts",
|
|
26
|
+
"types": "./dist/contracts.d.ts",
|
|
27
|
+
"import": "./dist/contracts.js",
|
|
28
|
+
"require": "./dist/contracts.js"
|
|
29
|
+
},
|
|
24
30
|
"./internal": {
|
|
25
31
|
"source": "./src/internal.ts",
|
|
26
32
|
"types": "./dist/internal.d.ts",
|