@playcademy/sdk 0.10.0 → 0.10.1-beta.2

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/index.d.ts CHANGED
@@ -1145,9 +1145,7 @@ interface StartActivityResult {
1145
1145
 
1146
1146
  /**
1147
1147
  * A TimeBack enrollment for the current game session.
1148
- * Alias for UserEnrollment without the optional gameId. Active enrollment IDs
1149
- * are available at `enrollment.enrollmentIds?.active` when supplied by the
1150
- * platform.
1148
+ * Alias for UserEnrollment without the optional gameId.
1151
1149
  */
1152
1150
  type TimebackEnrollment = Omit<UserEnrollment, 'gameId'>;
1153
1151
  /**
@@ -1253,6 +1251,50 @@ interface TimebackUser extends TimebackUserContext {
1253
1251
  * Call `xp.fetch()` to get XP from the server.
1254
1252
  */
1255
1253
  xp: TimebackUserXp;
1254
+ /**
1255
+ * Mastery data for the current user.
1256
+ * Call `mastery.fetch()` to get mastery progress from the server.
1257
+ */
1258
+ mastery: TimebackUserMastery;
1259
+ }
1260
+ /**
1261
+ * Mastery data access for the current user.
1262
+ * Results are cached for 5 seconds to avoid redundant network requests.
1263
+ */
1264
+ interface TimebackUserMastery {
1265
+ /**
1266
+ * Fetch mastery data from the server.
1267
+ * Returns mastery for all courses in this game, or filter by grade/subject.
1268
+ * Results are cached for 5 seconds (use `force: true` to bypass).
1269
+ *
1270
+ * @param options - Query options
1271
+ * @param options.grade - Grade level to filter (must be used with subject)
1272
+ * @param options.subject - Subject to filter (must be used with grade)
1273
+ * @param options.include - Additional data to include: 'perCourse'
1274
+ * @param options.force - Bypass cache and fetch fresh data (default: false)
1275
+ * @returns Promise resolving to mastery data
1276
+ *
1277
+ * @example
1278
+ * ```typescript
1279
+ * // Get total mastery for all game courses
1280
+ * const mastery = await client.timeback.user.mastery.fetch()
1281
+ *
1282
+ * // Get mastery for a specific grade/subject
1283
+ * const mastery = await client.timeback.user.mastery.fetch({
1284
+ * grade: 3,
1285
+ * subject: 'Math'
1286
+ * })
1287
+ *
1288
+ * // Get mastery with per-course breakdown
1289
+ * const mastery = await client.timeback.user.mastery.fetch({
1290
+ * include: ['perCourse']
1291
+ * })
1292
+ *
1293
+ * // Force fresh data
1294
+ * const mastery = await client.timeback.user.mastery.fetch({ force: true })
1295
+ * ```
1296
+ */
1297
+ fetch(options?: GetMasteryOptions): Promise<MasteryResponse>;
1256
1298
  }
1257
1299
  /**
1258
1300
  * Options for querying student XP.
@@ -1267,6 +1309,19 @@ interface GetXpOptions {
1267
1309
  /** Bypass cache and fetch fresh data (default: false) */
1268
1310
  force?: boolean;
1269
1311
  }
1312
+ /**
1313
+ * Options for querying student mastery.
1314
+ */
1315
+ interface GetMasteryOptions {
1316
+ /** Grade level to filter (must be used with subject) */
1317
+ grade?: TimebackGrade;
1318
+ /** Subject to filter (must be used with grade) */
1319
+ subject?: TimebackSubject;
1320
+ /** Additional data to include: 'perCourse' */
1321
+ include?: 'perCourse'[];
1322
+ /** Bypass cache and fetch fresh data (default: false) */
1323
+ force?: boolean;
1324
+ }
1270
1325
  /**
1271
1326
  * XP data for a single course.
1272
1327
  */
@@ -1285,6 +1340,26 @@ interface XpResponse {
1285
1340
  todayXp?: number;
1286
1341
  courses?: CourseXp[];
1287
1342
  }
1343
+ /**
1344
+ * Mastery data for a single course.
1345
+ */
1346
+ interface CourseMastery {
1347
+ grade: TimebackGrade;
1348
+ subject: TimebackSubject;
1349
+ title: string;
1350
+ masteredUnits: number;
1351
+ masterableUnits: number;
1352
+ pctComplete: number;
1353
+ isComplete: boolean;
1354
+ }
1355
+ /**
1356
+ * Response from mastery query.
1357
+ */
1358
+ interface MasteryResponse {
1359
+ totalMasteredUnits: number;
1360
+ totalMasterableUnits: number;
1361
+ courses?: CourseMastery[];
1362
+ }
1288
1363
 
1289
1364
  /**
1290
1365
  * Core client configuration and lifecycle types
package/dist/index.js CHANGED
@@ -1145,9 +1145,21 @@ function isValidSubject(value) {
1145
1145
  var TIMEBACK_ROUTES = {
1146
1146
  END_ACTIVITY: "/integrations/timeback/end-activity",
1147
1147
  GET_XP: "/integrations/timeback/xp",
1148
+ GET_MASTERY: "/integrations/timeback/mastery",
1148
1149
  HEARTBEAT: "/integrations/timeback/heartbeat",
1149
1150
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
1150
1151
  };
1152
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
1153
+ xp: 1,
1154
+ mastery: 0,
1155
+ score: 2
1156
+ };
1157
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
1158
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
1159
+ mastery: 0,
1160
+ time: 60,
1161
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
1162
+ };
1151
1163
  // src/core/cache/ttl-cache.ts
1152
1164
  function createTTLCache(options) {
1153
1165
  const cache = new Map;
@@ -1705,6 +1717,9 @@ function createTimebackActivityTracker(client) {
1705
1717
  const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
1706
1718
  const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
1707
1719
  const { correctQuestions, totalQuestions } = data;
1720
+ if (data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined) {
1721
+ throw new Error("Cannot provide both masteredUnits and masteredUnitsAbsolute — use one or the other");
1722
+ }
1708
1723
  const request = {
1709
1724
  runId: activity.runId,
1710
1725
  resumeId: activity.resumeId,
@@ -1722,6 +1737,7 @@ function createTimebackActivityTracker(client) {
1722
1737
  },
1723
1738
  xpEarned: data.xpAwarded,
1724
1739
  masteredUnits: data.masteredUnits,
1740
+ masteredUnitsAbsolute: data.masteredUnitsAbsolute,
1725
1741
  extensions: data.extensions
1726
1742
  };
1727
1743
  try {
@@ -1780,6 +1796,10 @@ function createTimebackEngine(client) {
1780
1796
  ttl: 5000,
1781
1797
  keyPrefix: "game.timeback.xp"
1782
1798
  });
1799
+ const masteryCache = createTTLCache({
1800
+ ttl: 5000,
1801
+ keyPrefix: "game.timeback.mastery"
1802
+ });
1783
1803
  const enrollmentsCache = createTTLCache({
1784
1804
  ttl: 5 * 60 * 1000,
1785
1805
  keyPrefix: "game.timeback.enrollments"
@@ -1847,6 +1867,30 @@ function createTimebackEngine(client) {
1847
1867
  return client["requestGameBackend"](endpoint, "GET");
1848
1868
  }, { force: options.force });
1849
1869
  }
1870
+ },
1871
+ mastery: {
1872
+ fetch: (options) => {
1873
+ const cacheKey = [
1874
+ options.grade ?? "",
1875
+ options.subject ?? "",
1876
+ options.include?.toSorted().join(",") ?? ""
1877
+ ].join(":");
1878
+ return masteryCache.get(cacheKey, async () => {
1879
+ const params = new URLSearchParams;
1880
+ if (options.grade !== undefined) {
1881
+ params.set("grade", String(options.grade));
1882
+ }
1883
+ if (options.subject !== undefined) {
1884
+ params.set("subject", options.subject);
1885
+ }
1886
+ if (options.include?.length) {
1887
+ params.set("include", options.include.join(","));
1888
+ }
1889
+ const queryString = params.toString();
1890
+ const endpoint = `${TIMEBACK_ROUTES.GET_MASTERY}${queryString ? `?${queryString}` : ""}`;
1891
+ return client["requestGameBackend"](endpoint, "GET");
1892
+ }, { force: options.force });
1893
+ }
1850
1894
  }
1851
1895
  },
1852
1896
  activity: {
@@ -1865,7 +1909,8 @@ function createTimebackEngine(client) {
1865
1909
  }
1866
1910
 
1867
1911
  // src/namespaces/game/timeback.ts
1868
- var VALID_INCLUDE_OPTIONS = ["perCourse", "today"];
1912
+ var VALID_XP_INCLUDE_OPTIONS = ["perCourse", "today"];
1913
+ var VALID_MASTERY_INCLUDE_OPTIONS = ["perCourse"];
1869
1914
  function createTimebackNamespace(client) {
1870
1915
  const engine = createTimebackEngine(client);
1871
1916
  return {
@@ -1901,13 +1946,36 @@ function createTimebackNamespace(client) {
1901
1946
  }
1902
1947
  if (options.include?.length) {
1903
1948
  for (const opt of options.include) {
1904
- if (!VALID_INCLUDE_OPTIONS.includes(opt)) {
1905
- throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_INCLUDE_OPTIONS.join(", ")}`);
1949
+ if (!VALID_XP_INCLUDE_OPTIONS.includes(opt)) {
1950
+ throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_XP_INCLUDE_OPTIONS.join(", ")}`);
1906
1951
  }
1907
1952
  }
1908
1953
  }
1909
1954
  return engine.user.xp.fetch(options);
1910
1955
  }
1956
+ },
1957
+ mastery: {
1958
+ fetch: async (options = {}) => {
1959
+ const hasGrade = options.grade !== undefined;
1960
+ const hasSubject = options.subject !== undefined;
1961
+ if (hasGrade !== hasSubject) {
1962
+ throw new Error("Both grade and subject must be provided together");
1963
+ }
1964
+ if (hasGrade && !isValidGrade(options.grade)) {
1965
+ throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
1966
+ }
1967
+ if (hasSubject && !isValidSubject(options.subject)) {
1968
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
1969
+ }
1970
+ if (options.include?.length) {
1971
+ for (const opt of options.include) {
1972
+ if (!VALID_MASTERY_INCLUDE_OPTIONS.includes(opt)) {
1973
+ throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_MASTERY_INCLUDE_OPTIONS.join(", ")}`);
1974
+ }
1975
+ }
1976
+ }
1977
+ return engine.user.mastery.fetch(options);
1978
+ }
1911
1979
  }
1912
1980
  };
1913
1981
  },
@@ -3,7 +3,7 @@ import { TimebackGrade, TimebackSubject, TimebackCourseConfig, CourseConfig, Org
3
3
  export { QtiTestQuestionRef, QtiTestQuestionsResponse } from '@playcademy/types/timeback';
4
4
  import * as _playcademy_types from '@playcademy/types';
5
5
  import { GameManifest } from '@playcademy/types';
6
- export { AuthenticatedUser, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, GameActivityMetrics, GameCourseMetrics, GameLeaderboardEntry, GameManifest, GameMetricsProxyResponse, GameMetricsResponse, GameMetricsUnsupportedReason, GamePlatform, GameTimebackIntegration, GameType, GameUser, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, ManifestV1, ManifestV2, ManifestVersions, PopulateStudentResponse, UserEnrollment, UserInfo, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData } from '@playcademy/types';
6
+ export { AuthenticatedUser, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, GameCourseMetrics, GameLeaderboardEntry, GameManifest, GameMetricComparisonKind, GameMetricComparisonMetric, GameMetricComparisonRow, GameMetricComparisonRowStatus, GameMetricsProxyResponse, GameMetricsResponse, GameMetricsUnsupportedReason, GamePlatform, GameRunMetrics, GameRunMetricsComparison, GameRunMetricsComparisonStatus, GameRunMetricsComparisonSummary, GameTimebackIntegration, GameType, GameUser, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, ManifestV1, ManifestV2, ManifestVersions, PopulateStudentResponse, UserEnrollment, UserInfo, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData } from '@playcademy/types';
7
7
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
8
8
  import { z } from 'zod';
9
9
  import { DomainValidationRecords } from '@playcademy/types/game';
@@ -999,9 +999,7 @@ interface StartActivityResult {
999
999
 
1000
1000
  /**
1001
1001
  * A TimeBack enrollment for the current game session.
1002
- * Alias for UserEnrollment without the optional gameId. Active enrollment IDs
1003
- * are available at `enrollment.enrollmentIds?.active` when supplied by the
1004
- * platform.
1002
+ * Alias for UserEnrollment without the optional gameId.
1005
1003
  */
1006
1004
  type TimebackEnrollment = Omit<UserEnrollment, 'gameId'>;
1007
1005
  /**
@@ -1107,6 +1105,50 @@ interface TimebackUser extends TimebackUserContext {
1107
1105
  * Call `xp.fetch()` to get XP from the server.
1108
1106
  */
1109
1107
  xp: TimebackUserXp;
1108
+ /**
1109
+ * Mastery data for the current user.
1110
+ * Call `mastery.fetch()` to get mastery progress from the server.
1111
+ */
1112
+ mastery: TimebackUserMastery;
1113
+ }
1114
+ /**
1115
+ * Mastery data access for the current user.
1116
+ * Results are cached for 5 seconds to avoid redundant network requests.
1117
+ */
1118
+ interface TimebackUserMastery {
1119
+ /**
1120
+ * Fetch mastery data from the server.
1121
+ * Returns mastery for all courses in this game, or filter by grade/subject.
1122
+ * Results are cached for 5 seconds (use `force: true` to bypass).
1123
+ *
1124
+ * @param options - Query options
1125
+ * @param options.grade - Grade level to filter (must be used with subject)
1126
+ * @param options.subject - Subject to filter (must be used with grade)
1127
+ * @param options.include - Additional data to include: 'perCourse'
1128
+ * @param options.force - Bypass cache and fetch fresh data (default: false)
1129
+ * @returns Promise resolving to mastery data
1130
+ *
1131
+ * @example
1132
+ * ```typescript
1133
+ * // Get total mastery for all game courses
1134
+ * const mastery = await client.timeback.user.mastery.fetch()
1135
+ *
1136
+ * // Get mastery for a specific grade/subject
1137
+ * const mastery = await client.timeback.user.mastery.fetch({
1138
+ * grade: 3,
1139
+ * subject: 'Math'
1140
+ * })
1141
+ *
1142
+ * // Get mastery with per-course breakdown
1143
+ * const mastery = await client.timeback.user.mastery.fetch({
1144
+ * include: ['perCourse']
1145
+ * })
1146
+ *
1147
+ * // Force fresh data
1148
+ * const mastery = await client.timeback.user.mastery.fetch({ force: true })
1149
+ * ```
1150
+ */
1151
+ fetch(options?: GetMasteryOptions): Promise<MasteryResponse>;
1110
1152
  }
1111
1153
  /**
1112
1154
  * Options for querying student XP.
@@ -1121,6 +1163,19 @@ interface GetXpOptions {
1121
1163
  /** Bypass cache and fetch fresh data (default: false) */
1122
1164
  force?: boolean;
1123
1165
  }
1166
+ /**
1167
+ * Options for querying student mastery.
1168
+ */
1169
+ interface GetMasteryOptions {
1170
+ /** Grade level to filter (must be used with subject) */
1171
+ grade?: TimebackGrade;
1172
+ /** Subject to filter (must be used with grade) */
1173
+ subject?: TimebackSubject;
1174
+ /** Additional data to include: 'perCourse' */
1175
+ include?: 'perCourse'[];
1176
+ /** Bypass cache and fetch fresh data (default: false) */
1177
+ force?: boolean;
1178
+ }
1124
1179
  /**
1125
1180
  * XP data for a single course.
1126
1181
  */
@@ -1139,6 +1194,26 @@ interface XpResponse {
1139
1194
  todayXp?: number;
1140
1195
  courses?: CourseXp[];
1141
1196
  }
1197
+ /**
1198
+ * Mastery data for a single course.
1199
+ */
1200
+ interface CourseMastery {
1201
+ grade: TimebackGrade;
1202
+ subject: TimebackSubject;
1203
+ title: string;
1204
+ masteredUnits: number;
1205
+ masterableUnits: number;
1206
+ pctComplete: number;
1207
+ isComplete: boolean;
1208
+ }
1209
+ /**
1210
+ * Response from mastery query.
1211
+ */
1212
+ interface MasteryResponse {
1213
+ totalMasteredUnits: number;
1214
+ totalMasterableUnits: number;
1215
+ courses?: CourseMastery[];
1216
+ }
1142
1217
 
1143
1218
  /**
1144
1219
  * Core client configuration and lifecycle types
@@ -2960,6 +3035,10 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
2960
3035
  gameId: string;
2961
3036
  courseId?: string;
2962
3037
  }) => Promise<_playcademy_types.TimebackStudentOverviewResponse>;
3038
+ getGameMetrics: (timebackId: string, options: {
3039
+ gameId: string;
3040
+ runIds?: string[];
3041
+ }) => Promise<_playcademy_types.GameMetricsProxyResponse>;
2963
3042
  getStudentActivity: (timebackId: string, courseId: string, options: {
2964
3043
  gameId: string;
2965
3044
  limit?: number;
@@ -3025,4 +3104,4 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
3025
3104
  }
3026
3105
 
3027
3106
  export { ApiError, MessageEvents, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging };
3028
- export type { ApiErrorCode, ApiErrorInfo, AssessmentBankStatus, AssessmentRow, AssessmentSummary, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetXpOptions, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MessageEventMap, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
3107
+ export type { ApiErrorCode, ApiErrorInfo, AssessmentBankStatus, AssessmentRow, AssessmentSummary, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseMastery, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, ErrorResponseBody, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetMasteryOptions, GetXpOptions, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, MessageEventMap, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
package/dist/internal.js CHANGED
@@ -1145,9 +1145,21 @@ function isValidSubject(value) {
1145
1145
  var TIMEBACK_ROUTES = {
1146
1146
  END_ACTIVITY: "/integrations/timeback/end-activity",
1147
1147
  GET_XP: "/integrations/timeback/xp",
1148
+ GET_MASTERY: "/integrations/timeback/mastery",
1148
1149
  HEARTBEAT: "/integrations/timeback/heartbeat",
1149
1150
  ADVANCE_COURSE: "/integrations/timeback/advance-course"
1150
1151
  };
1152
+ var TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
1153
+ xp: 1,
1154
+ mastery: 0,
1155
+ score: 2
1156
+ };
1157
+ var TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
1158
+ xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
1159
+ mastery: 0,
1160
+ time: 60,
1161
+ score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
1162
+ };
1151
1163
  // src/core/cache/ttl-cache.ts
1152
1164
  function createTTLCache(options) {
1153
1165
  const cache = new Map;
@@ -1705,6 +1717,9 @@ function createTimebackActivityTracker(client) {
1705
1717
  const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
1706
1718
  const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
1707
1719
  const { correctQuestions, totalQuestions } = data;
1720
+ if (data.masteredUnits !== undefined && data.masteredUnitsAbsolute !== undefined) {
1721
+ throw new Error("Cannot provide both masteredUnits and masteredUnitsAbsolute — use one or the other");
1722
+ }
1708
1723
  const request = {
1709
1724
  runId: activity.runId,
1710
1725
  resumeId: activity.resumeId,
@@ -1722,6 +1737,7 @@ function createTimebackActivityTracker(client) {
1722
1737
  },
1723
1738
  xpEarned: data.xpAwarded,
1724
1739
  masteredUnits: data.masteredUnits,
1740
+ masteredUnitsAbsolute: data.masteredUnitsAbsolute,
1725
1741
  extensions: data.extensions
1726
1742
  };
1727
1743
  try {
@@ -1780,6 +1796,10 @@ function createTimebackEngine(client) {
1780
1796
  ttl: 5000,
1781
1797
  keyPrefix: "game.timeback.xp"
1782
1798
  });
1799
+ const masteryCache = createTTLCache({
1800
+ ttl: 5000,
1801
+ keyPrefix: "game.timeback.mastery"
1802
+ });
1783
1803
  const enrollmentsCache = createTTLCache({
1784
1804
  ttl: 5 * 60 * 1000,
1785
1805
  keyPrefix: "game.timeback.enrollments"
@@ -1847,6 +1867,30 @@ function createTimebackEngine(client) {
1847
1867
  return client["requestGameBackend"](endpoint, "GET");
1848
1868
  }, { force: options.force });
1849
1869
  }
1870
+ },
1871
+ mastery: {
1872
+ fetch: (options) => {
1873
+ const cacheKey = [
1874
+ options.grade ?? "",
1875
+ options.subject ?? "",
1876
+ options.include?.toSorted().join(",") ?? ""
1877
+ ].join(":");
1878
+ return masteryCache.get(cacheKey, async () => {
1879
+ const params = new URLSearchParams;
1880
+ if (options.grade !== undefined) {
1881
+ params.set("grade", String(options.grade));
1882
+ }
1883
+ if (options.subject !== undefined) {
1884
+ params.set("subject", options.subject);
1885
+ }
1886
+ if (options.include?.length) {
1887
+ params.set("include", options.include.join(","));
1888
+ }
1889
+ const queryString = params.toString();
1890
+ const endpoint = `${TIMEBACK_ROUTES.GET_MASTERY}${queryString ? `?${queryString}` : ""}`;
1891
+ return client["requestGameBackend"](endpoint, "GET");
1892
+ }, { force: options.force });
1893
+ }
1850
1894
  }
1851
1895
  },
1852
1896
  activity: {
@@ -1865,7 +1909,8 @@ function createTimebackEngine(client) {
1865
1909
  }
1866
1910
 
1867
1911
  // src/namespaces/game/timeback.ts
1868
- var VALID_INCLUDE_OPTIONS = ["perCourse", "today"];
1912
+ var VALID_XP_INCLUDE_OPTIONS = ["perCourse", "today"];
1913
+ var VALID_MASTERY_INCLUDE_OPTIONS = ["perCourse"];
1869
1914
  function createTimebackNamespace(client) {
1870
1915
  const engine = createTimebackEngine(client);
1871
1916
  return {
@@ -1901,13 +1946,36 @@ function createTimebackNamespace(client) {
1901
1946
  }
1902
1947
  if (options.include?.length) {
1903
1948
  for (const opt of options.include) {
1904
- if (!VALID_INCLUDE_OPTIONS.includes(opt)) {
1905
- throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_INCLUDE_OPTIONS.join(", ")}`);
1949
+ if (!VALID_XP_INCLUDE_OPTIONS.includes(opt)) {
1950
+ throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_XP_INCLUDE_OPTIONS.join(", ")}`);
1906
1951
  }
1907
1952
  }
1908
1953
  }
1909
1954
  return engine.user.xp.fetch(options);
1910
1955
  }
1956
+ },
1957
+ mastery: {
1958
+ fetch: async (options = {}) => {
1959
+ const hasGrade = options.grade !== undefined;
1960
+ const hasSubject = options.subject !== undefined;
1961
+ if (hasGrade !== hasSubject) {
1962
+ throw new Error("Both grade and subject must be provided together");
1963
+ }
1964
+ if (hasGrade && !isValidGrade(options.grade)) {
1965
+ throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
1966
+ }
1967
+ if (hasSubject && !isValidSubject(options.subject)) {
1968
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
1969
+ }
1970
+ if (options.include?.length) {
1971
+ for (const opt of options.include) {
1972
+ if (!VALID_MASTERY_INCLUDE_OPTIONS.includes(opt)) {
1973
+ throw new Error(`Invalid include option: ${opt}. Valid options: ${VALID_MASTERY_INCLUDE_OPTIONS.join(", ")}`);
1974
+ }
1975
+ }
1976
+ }
1977
+ return engine.user.mastery.fetch(options);
1978
+ }
1911
1979
  }
1912
1980
  };
1913
1981
  },
@@ -2577,6 +2645,14 @@ function createTimebackNamespace2(client) {
2577
2645
  }
2578
2646
  return client["request"](`/timeback/student-overview/${timebackId}?${params}`, "GET");
2579
2647
  },
2648
+ getGameMetrics: (timebackId, options) => {
2649
+ const params = new URLSearchParams;
2650
+ for (const runId of options.runIds ?? []) {
2651
+ params.append("runId", runId);
2652
+ }
2653
+ const query = params.toString();
2654
+ return client["request"](`/timeback/game-metrics/${options.gameId}/${timebackId}${query ? `?${query}` : ""}`, "GET");
2655
+ },
2580
2656
  getStudentActivity: (timebackId, courseId, options) => {
2581
2657
  const params = new URLSearchParams({ gameId: options.gameId });
2582
2658
  if (options.limit !== undefined) {
@@ -298,6 +298,11 @@ declare class PlaycademyClient {
298
298
  subject?: string;
299
299
  include?: ('perCourse' | 'today')[];
300
300
  }) => Promise<_playcademy_types.StudentXpResponse>;
301
+ getStudentMastery: (studentId: string, options?: {
302
+ grade?: number;
303
+ subject?: string;
304
+ include?: 'perCourse'[];
305
+ }) => Promise<_playcademy_types.StudentMasteryResponse>;
301
306
  };
302
307
  }
303
308
 
@@ -83,6 +83,33 @@ function createTimebackNamespace(client) {
83
83
  const queryString = params.toString();
84
84
  const endpoint = `/api/timeback/student-xp/${studentId}?${queryString}`;
85
85
  return client["request"](endpoint, "GET");
86
+ },
87
+ getStudentMastery: async (studentId, options) => {
88
+ const hasGrade = options?.grade !== undefined;
89
+ const hasSubject = options?.subject !== undefined;
90
+ if (hasGrade !== hasSubject) {
91
+ throw new Error("Both grade and subject must be provided together");
92
+ }
93
+ if (hasGrade && !isValidGrade(options.grade)) {
94
+ throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
95
+ }
96
+ if (hasSubject && !isValidSubject(options.subject)) {
97
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
98
+ }
99
+ const params = new URLSearchParams;
100
+ params.set("gameId", client.gameId);
101
+ if (options?.grade !== undefined) {
102
+ params.set("grade", String(options.grade));
103
+ }
104
+ if (options?.subject) {
105
+ params.set("subject", options.subject);
106
+ }
107
+ if (options?.include?.length) {
108
+ params.set("include", options.include.join(","));
109
+ }
110
+ const queryString = params.toString();
111
+ const endpoint = `/api/timeback/student-mastery/${studentId}?${queryString}`;
112
+ return client["request"](endpoint, "GET");
86
113
  }
87
114
  };
88
115
  }
package/dist/server.d.ts CHANGED
@@ -298,6 +298,11 @@ declare class PlaycademyClient$1 {
298
298
  subject?: string;
299
299
  include?: ('perCourse' | 'today')[];
300
300
  }) => Promise<_playcademy_types.StudentXpResponse>;
301
+ getStudentMastery: (studentId: string, options?: {
302
+ grade?: number;
303
+ subject?: string;
304
+ include?: 'perCourse'[];
305
+ }) => Promise<_playcademy_types.StudentMasteryResponse>;
301
306
  };
302
307
  }
303
308
 
package/dist/server.js CHANGED
@@ -272,6 +272,33 @@ function createTimebackNamespace(client) {
272
272
  const queryString = params.toString();
273
273
  const endpoint = `/api/timeback/student-xp/${studentId}?${queryString}`;
274
274
  return client["request"](endpoint, "GET");
275
+ },
276
+ getStudentMastery: async (studentId, options) => {
277
+ const hasGrade = options?.grade !== undefined;
278
+ const hasSubject = options?.subject !== undefined;
279
+ if (hasGrade !== hasSubject) {
280
+ throw new Error("Both grade and subject must be provided together");
281
+ }
282
+ if (hasGrade && !isValidGrade(options.grade)) {
283
+ throw new Error(`Invalid grade: ${options.grade}. Valid grades: ${VALID_GRADES.join(", ")}`);
284
+ }
285
+ if (hasSubject && !isValidSubject(options.subject)) {
286
+ throw new Error(`Invalid subject: ${options.subject}. Valid subjects: ${VALID_SUBJECTS.join(", ")}`);
287
+ }
288
+ const params = new URLSearchParams;
289
+ params.set("gameId", client.gameId);
290
+ if (options?.grade !== undefined) {
291
+ params.set("grade", String(options.grade));
292
+ }
293
+ if (options?.subject) {
294
+ params.set("subject", options.subject);
295
+ }
296
+ if (options?.include?.length) {
297
+ params.set("include", options.include.join(","));
298
+ }
299
+ const queryString = params.toString();
300
+ const endpoint = `/api/timeback/student-mastery/${studentId}?${queryString}`;
301
+ return client["request"](endpoint, "GET");
275
302
  }
276
303
  };
277
304
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _playcademy_types from '@playcademy/types';
2
2
  import { GameManifest } from '@playcademy/types';
3
- export { AuthenticatedUser, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, GameActivityMetrics, GameCourseMetrics, GameLeaderboardEntry, GameManifest, GameMetricsProxyResponse, GameMetricsResponse, GameMetricsUnsupportedReason, GamePlatform, GameTimebackIntegration, GameType, GameUser, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, ManifestV1, ManifestV2, ManifestVersions, PopulateStudentResponse, UserEnrollment, UserInfo, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData } from '@playcademy/types';
3
+ export { AuthenticatedUser, DeveloperStatusEnumType, DeveloperStatusResponse, DeveloperStatusValue, GameCourseMetrics, GameLeaderboardEntry, GameManifest, GameMetricComparisonKind, GameMetricComparisonMetric, GameMetricComparisonRow, GameMetricComparisonRowStatus, GameMetricsProxyResponse, GameMetricsResponse, GameMetricsUnsupportedReason, GamePlatform, GameRunMetrics, GameRunMetricsComparison, GameRunMetricsComparisonStatus, GameRunMetricsComparisonSummary, GameTimebackIntegration, GameType, GameUser, LeaderboardEntry, LeaderboardOptions, LeaderboardTimeframe, ManifestV1, ManifestV2, ManifestVersions, PopulateStudentResponse, UserEnrollment, UserInfo, UserOrganization, UserRank, UserRankResponse, UserRoleEnumType, UserScore, UserTimebackData } from '@playcademy/types';
4
4
  import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig, TimebackGrade, TimebackSubject } from '@playcademy/types/timeback';
5
5
  export { QtiTestQuestionRef, QtiTestQuestionsResponse } from '@playcademy/types/timeback';
6
6
  import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
@@ -1502,9 +1502,7 @@ interface StartActivityResult {
1502
1502
 
1503
1503
  /**
1504
1504
  * A TimeBack enrollment for the current game session.
1505
- * Alias for UserEnrollment without the optional gameId. Active enrollment IDs
1506
- * are available at `enrollment.enrollmentIds?.active` when supplied by the
1507
- * platform.
1505
+ * Alias for UserEnrollment without the optional gameId.
1508
1506
  */
1509
1507
  type TimebackEnrollment = Omit<UserEnrollment, 'gameId'>;
1510
1508
  /**
@@ -1610,6 +1608,50 @@ interface TimebackUser extends TimebackUserContext {
1610
1608
  * Call `xp.fetch()` to get XP from the server.
1611
1609
  */
1612
1610
  xp: TimebackUserXp;
1611
+ /**
1612
+ * Mastery data for the current user.
1613
+ * Call `mastery.fetch()` to get mastery progress from the server.
1614
+ */
1615
+ mastery: TimebackUserMastery;
1616
+ }
1617
+ /**
1618
+ * Mastery data access for the current user.
1619
+ * Results are cached for 5 seconds to avoid redundant network requests.
1620
+ */
1621
+ interface TimebackUserMastery {
1622
+ /**
1623
+ * Fetch mastery data from the server.
1624
+ * Returns mastery for all courses in this game, or filter by grade/subject.
1625
+ * Results are cached for 5 seconds (use `force: true` to bypass).
1626
+ *
1627
+ * @param options - Query options
1628
+ * @param options.grade - Grade level to filter (must be used with subject)
1629
+ * @param options.subject - Subject to filter (must be used with grade)
1630
+ * @param options.include - Additional data to include: 'perCourse'
1631
+ * @param options.force - Bypass cache and fetch fresh data (default: false)
1632
+ * @returns Promise resolving to mastery data
1633
+ *
1634
+ * @example
1635
+ * ```typescript
1636
+ * // Get total mastery for all game courses
1637
+ * const mastery = await client.timeback.user.mastery.fetch()
1638
+ *
1639
+ * // Get mastery for a specific grade/subject
1640
+ * const mastery = await client.timeback.user.mastery.fetch({
1641
+ * grade: 3,
1642
+ * subject: 'Math'
1643
+ * })
1644
+ *
1645
+ * // Get mastery with per-course breakdown
1646
+ * const mastery = await client.timeback.user.mastery.fetch({
1647
+ * include: ['perCourse']
1648
+ * })
1649
+ *
1650
+ * // Force fresh data
1651
+ * const mastery = await client.timeback.user.mastery.fetch({ force: true })
1652
+ * ```
1653
+ */
1654
+ fetch(options?: GetMasteryOptions): Promise<MasteryResponse>;
1613
1655
  }
1614
1656
  /**
1615
1657
  * Options for querying student XP.
@@ -1624,6 +1666,19 @@ interface GetXpOptions {
1624
1666
  /** Bypass cache and fetch fresh data (default: false) */
1625
1667
  force?: boolean;
1626
1668
  }
1669
+ /**
1670
+ * Options for querying student mastery.
1671
+ */
1672
+ interface GetMasteryOptions {
1673
+ /** Grade level to filter (must be used with subject) */
1674
+ grade?: TimebackGrade;
1675
+ /** Subject to filter (must be used with grade) */
1676
+ subject?: TimebackSubject;
1677
+ /** Additional data to include: 'perCourse' */
1678
+ include?: 'perCourse'[];
1679
+ /** Bypass cache and fetch fresh data (default: false) */
1680
+ force?: boolean;
1681
+ }
1627
1682
  /**
1628
1683
  * XP data for a single course.
1629
1684
  */
@@ -1642,6 +1697,26 @@ interface XpResponse {
1642
1697
  todayXp?: number;
1643
1698
  courses?: CourseXp[];
1644
1699
  }
1700
+ /**
1701
+ * Mastery data for a single course.
1702
+ */
1703
+ interface CourseMastery {
1704
+ grade: TimebackGrade;
1705
+ subject: TimebackSubject;
1706
+ title: string;
1707
+ masteredUnits: number;
1708
+ masterableUnits: number;
1709
+ pctComplete: number;
1710
+ isComplete: boolean;
1711
+ }
1712
+ /**
1713
+ * Response from mastery query.
1714
+ */
1715
+ interface MasteryResponse {
1716
+ totalMasteredUnits: number;
1717
+ totalMasterableUnits: number;
1718
+ courses?: CourseMastery[];
1719
+ }
1645
1720
 
1646
1721
  /**
1647
1722
  * Core client configuration and lifecycle types
@@ -2067,4 +2142,4 @@ interface AssessmentBankStatus {
2067
2142
  }
2068
2143
 
2069
2144
  export { PlaycademyClient };
2070
- export type { AssessmentBankStatus, AssessmentRow, AssessmentSummary, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetXpOptions, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
2145
+ export type { AssessmentBankStatus, AssessmentRow, AssessmentSummary, AuthCallbackPayload, AuthOptions, AuthProviderType, AuthResult, AuthServerMessage, AuthStateChangePayload, AuthStateUpdate, BetterAuthApiKey, BetterAuthApiKeyResponse, BetterAuthSignInResponse, BucketFile, ClientConfig, ClientEvents, CourseMastery, CourseXp, DemoEndOptions, DemoEndPayload, DevUploadEvent, DevUploadHooks, EventListeners, ExternalGame, FetchedGame, Game, GameContextPayload, GameCustomHostname, GameInitUser, GameRow as GameRecord, GameTokenResponse, GetMasteryOptions, GetXpOptions, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-beta.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {