@playcademy/sdk 0.4.1-beta.3 → 0.4.1-beta.5

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
@@ -381,74 +381,6 @@ declare function parseOAuthState(state: string): {
381
381
  /** Permitted HTTP verbs */
382
382
  type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
383
383
 
384
- /**
385
- * User Types
386
- *
387
- * Enums, DTOs and API response types. Database row types are in @playcademy/data/types.
388
- *
389
- * @module types/user
390
- */
391
- type UserRoleEnumType = 'admin' | 'player' | 'developer';
392
- type DeveloperStatusEnumType = 'none' | 'pending' | 'approved';
393
- type TimebackUserRole = 'administrator' | 'aide' | 'guardian' | 'parent' | 'proctor' | 'relative' | 'student' | 'teacher';
394
- type TimebackOrgType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
395
- interface UserEnrollment {
396
- gameId?: string;
397
- courseId: string;
398
- grade: number;
399
- subject: string;
400
- orgId?: string;
401
- }
402
- interface UserOrganization {
403
- id: string;
404
- name: string | null;
405
- type: TimebackOrgType | string;
406
- isPrimary: boolean;
407
- }
408
- interface TimebackStudentProfile {
409
- role: TimebackUserRole;
410
- organizations: UserOrganization[];
411
- }
412
- interface UserTimebackData extends TimebackStudentProfile {
413
- id: string;
414
- enrollments: UserEnrollment[];
415
- }
416
- /**
417
- * OpenID Connect UserInfo claims (NOT a database row).
418
- */
419
- interface UserInfo {
420
- sub: string;
421
- email: string;
422
- name: string | null;
423
- email_verified?: boolean;
424
- given_name?: string;
425
- family_name?: string;
426
- issuer?: string;
427
- lti_roles?: unknown;
428
- lti_context?: unknown;
429
- lti_resource_link?: unknown;
430
- timeback_id?: string;
431
- }
432
- /**
433
- * Authenticated user for API responses.
434
- * Differs from UserRow: omits timebackId, adds hasTimebackAccount and timeback.
435
- */
436
- interface AuthenticatedUser {
437
- id: string;
438
- email: string;
439
- emailVerified: boolean;
440
- name: string | null;
441
- image: string | null;
442
- username: string | null;
443
- role: UserRoleEnumType;
444
- developerStatus: DeveloperStatusEnumType;
445
- characterCreated: boolean;
446
- createdAt: Date;
447
- updatedAt: Date;
448
- hasTimebackAccount: boolean;
449
- timeback?: UserTimebackData;
450
- }
451
-
452
384
  /**
453
385
  * TimeBack Enums & Literal Types
454
386
  *
@@ -551,6 +483,74 @@ interface EndActivityResponse {
551
483
  inProgress?: string;
552
484
  }
553
485
 
486
+ /**
487
+ * User Types
488
+ *
489
+ * Enums, DTOs and API response types. Database row types are in @playcademy/data/types.
490
+ *
491
+ * @module types/user
492
+ */
493
+ type UserRoleEnumType = 'admin' | 'player' | 'developer' | 'teacher';
494
+ type DeveloperStatusEnumType = 'none' | 'pending' | 'approved';
495
+ type TimebackUserRole = 'administrator' | 'aide' | 'guardian' | 'parent' | 'proctor' | 'relative' | 'student' | 'teacher';
496
+ type TimebackOrgType = 'department' | 'school' | 'district' | 'local' | 'state' | 'national';
497
+ interface UserEnrollment {
498
+ gameId?: string;
499
+ courseId: string;
500
+ grade: number;
501
+ subject: string;
502
+ orgId?: string;
503
+ }
504
+ interface UserOrganization {
505
+ id: string;
506
+ name: string | null;
507
+ type: TimebackOrgType | string;
508
+ isPrimary: boolean;
509
+ }
510
+ interface TimebackStudentProfile {
511
+ role: TimebackUserRole;
512
+ organizations: UserOrganization[];
513
+ }
514
+ interface UserTimebackData extends TimebackStudentProfile {
515
+ id: string;
516
+ enrollments: UserEnrollment[];
517
+ }
518
+ /**
519
+ * OpenID Connect UserInfo claims (NOT a database row).
520
+ */
521
+ interface UserInfo {
522
+ sub: string;
523
+ email: string;
524
+ name: string | null;
525
+ email_verified?: boolean;
526
+ given_name?: string;
527
+ family_name?: string;
528
+ issuer?: string;
529
+ lti_roles?: unknown;
530
+ lti_context?: unknown;
531
+ lti_resource_link?: unknown;
532
+ timeback_id?: string;
533
+ }
534
+ /**
535
+ * Authenticated user for API responses.
536
+ * Differs from UserRow: omits timebackId, adds hasTimebackAccount and timeback.
537
+ */
538
+ interface AuthenticatedUser {
539
+ id: string;
540
+ email: string;
541
+ emailVerified: boolean;
542
+ name: string | null;
543
+ image: string | null;
544
+ username: string | null;
545
+ role: UserRoleEnumType;
546
+ developerStatus: DeveloperStatusEnumType;
547
+ characterCreated: boolean;
548
+ createdAt: Date;
549
+ updatedAt: Date;
550
+ hasTimebackAccount: boolean;
551
+ timeback?: UserTimebackData;
552
+ }
553
+
554
554
  declare const items: drizzle_orm_pg_core.PgTableWithColumns<{
555
555
  name: "items";
556
556
  schema: undefined;
@@ -982,6 +982,23 @@ declare abstract class PlaycademyBaseClient {
982
982
  };
983
983
  }
984
984
 
985
+ /**
986
+ * Options for configuring activity tracking behavior.
987
+ */
988
+ interface StartActivityOptions {
989
+ /**
990
+ * How long the tab can stay hidden before the timing window resets on return.
991
+ * Defaults to 10 minutes. Set to `Infinity` to disable.
992
+ */
993
+ hiddenTimeoutMs?: number;
994
+ /**
995
+ * How often to flush periodic heartbeats with accumulated time data.
996
+ * Defaults to 15 seconds. Set to `Infinity` to disable the interval;
997
+ * final unload/endActivity flushes still run.
998
+ */
999
+ heartbeatIntervalMs?: number;
1000
+ }
1001
+
985
1002
  /**
986
1003
  * Auto-initializes a PlaycademyClient with context from the environment.
987
1004
  * Works in both iframe mode (production/development) and standalone mode (local dev).
@@ -1119,7 +1136,7 @@ declare class PlaycademyClient extends PlaycademyBaseClient {
1119
1136
  */
1120
1137
  timeback: {
1121
1138
  readonly user: TimebackUser;
1122
- startActivity: (metadata: ActivityData) => void;
1139
+ startActivity: (metadata: ActivityData, options?: StartActivityOptions | undefined) => void;
1123
1140
  pauseActivity: () => void;
1124
1141
  resumeActivity: () => void;
1125
1142
  endActivity: (data: EndActivityScoreData) => Promise<EndActivityResponse>;
package/dist/index.js CHANGED
@@ -1274,7 +1274,8 @@ var BADGES = {
1274
1274
  // ../constants/src/timeback.ts
1275
1275
  var TIMEBACK_ROUTES = {
1276
1276
  END_ACTIVITY: "/integrations/timeback/end-activity",
1277
- GET_XP: "/integrations/timeback/xp"
1277
+ GET_XP: "/integrations/timeback/xp",
1278
+ HEARTBEAT: "/integrations/timeback/heartbeat"
1278
1279
  };
1279
1280
  // src/core/cache/singleton-cache.ts
1280
1281
  function createSingletonCache() {
@@ -1378,6 +1379,325 @@ function createRealtimeNamespace(client) {
1378
1379
  }
1379
1380
  };
1380
1381
  }
1382
+ // src/core/activity-tracker.ts
1383
+ var DEFAULT_HIDDEN_TIMEOUT_MS = 10 * 60 * 1000;
1384
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
1385
+ function getCurrentPausedTotal(activity, now = Date.now()) {
1386
+ if (activity.pauseStartTime === null) {
1387
+ return activity.pausedTime;
1388
+ }
1389
+ return activity.pausedTime + (now - activity.pauseStartTime);
1390
+ }
1391
+ function computeWindowAndReset(activity) {
1392
+ const now = Date.now();
1393
+ const totalPaused = getCurrentPausedTotal(activity, now);
1394
+ if (activity.pauseStartTime !== null) {
1395
+ activity.pausedTime = totalPaused;
1396
+ activity.pauseStartTime = now;
1397
+ }
1398
+ const windowPaused = totalPaused - activity.windowPausedAtStart;
1399
+ const windowElapsed = now - activity.windowStartTime;
1400
+ const activeMs = Math.max(0, windowElapsed - windowPaused);
1401
+ activity.windowStartTime = now;
1402
+ activity.windowPausedAtStart = totalPaused;
1403
+ activity.windowSequence++;
1404
+ return { activeMs, pausedMs: windowPaused };
1405
+ }
1406
+ function markPersistedTiming(activity, timing) {
1407
+ activity.totalPersistedActiveMs += timing.activeMs;
1408
+ activity.totalPersistedPausedMs += timing.pausedMs;
1409
+ }
1410
+ function queueHeartbeatFlush(activity, timing, flush) {
1411
+ markPersistedTiming(activity, timing);
1412
+ async function doFlush() {
1413
+ await flush();
1414
+ }
1415
+ if (activity.flushInFlight) {
1416
+ const previousFlush = activity.flushInFlight.catch(() => {
1417
+ return;
1418
+ });
1419
+ activity.flushInFlight = previousFlush.then(doFlush);
1420
+ } else {
1421
+ activity.flushInFlight = doFlush();
1422
+ }
1423
+ return activity.flushInFlight;
1424
+ }
1425
+ function stopHeartbeatInterval(activity) {
1426
+ if (activity.heartbeatIntervalId === null) {
1427
+ return;
1428
+ }
1429
+ clearInterval(activity.heartbeatIntervalId);
1430
+ activity.heartbeatIntervalId = null;
1431
+ }
1432
+ function createTimebackActivityTracker(client) {
1433
+ let currentActivity = null;
1434
+ let boundVisibilityHandler = null;
1435
+ let boundShellPauseHandler = null;
1436
+ let boundShellResumeHandler = null;
1437
+ let boundPageHideHandler = null;
1438
+ function startHeartbeatInterval(activity) {
1439
+ if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
1440
+ return;
1441
+ }
1442
+ activity.heartbeatIntervalId = setInterval(() => {
1443
+ flushHeartbeat();
1444
+ }, activity.heartbeatIntervalMs);
1445
+ }
1446
+ function addPauseReason(reason) {
1447
+ if (!currentActivity) {
1448
+ return;
1449
+ }
1450
+ const wasPaused = currentActivity.pauseReasons.size > 0;
1451
+ currentActivity.pauseReasons.add(reason);
1452
+ if (!wasPaused && currentActivity.pauseReasons.size > 0) {
1453
+ currentActivity.pauseStartTime = Date.now();
1454
+ }
1455
+ }
1456
+ function removePauseReason(reason) {
1457
+ if (!currentActivity) {
1458
+ return;
1459
+ }
1460
+ currentActivity.pauseReasons.delete(reason);
1461
+ if (currentActivity.pauseReasons.size === 0 && currentActivity.pauseStartTime !== null) {
1462
+ currentActivity.pausedTime += Date.now() - currentActivity.pauseStartTime;
1463
+ currentActivity.pauseStartTime = null;
1464
+ }
1465
+ }
1466
+ function handleVisibilityChange() {
1467
+ if (!currentActivity) {
1468
+ return;
1469
+ }
1470
+ if (document.visibilityState === "hidden") {
1471
+ addPauseReason("hidden");
1472
+ if (currentActivity.hiddenTimeoutMs !== Infinity) {
1473
+ if (currentActivity.hiddenTimeoutId !== null) {
1474
+ clearTimeout(currentActivity.hiddenTimeoutId);
1475
+ }
1476
+ const activity = currentActivity;
1477
+ currentActivity.hiddenTimeoutId = setTimeout(() => {
1478
+ if (currentActivity === activity) {
1479
+ activity.hiddenTimeoutId = null;
1480
+ activity.hiddenTimedOut = true;
1481
+ stopHeartbeatInterval(activity);
1482
+ }
1483
+ }, activity.hiddenTimeoutMs);
1484
+ }
1485
+ } else {
1486
+ const shouldResetWindow = currentActivity.hiddenTimedOut;
1487
+ if (currentActivity.hiddenTimeoutId !== null) {
1488
+ clearTimeout(currentActivity.hiddenTimeoutId);
1489
+ currentActivity.hiddenTimeoutId = null;
1490
+ }
1491
+ removePauseReason("hidden");
1492
+ if (shouldResetWindow) {
1493
+ const now = Date.now();
1494
+ const pausedAtReset = getCurrentPausedTotal(currentActivity, now);
1495
+ currentActivity.hiddenTimedOut = false;
1496
+ currentActivity.windowStartTime = now;
1497
+ currentActivity.windowPausedAtStart = pausedAtReset;
1498
+ startHeartbeatInterval(currentActivity);
1499
+ }
1500
+ }
1501
+ }
1502
+ function handleShellPause() {
1503
+ addPauseReason("shell");
1504
+ }
1505
+ function handleShellResume() {
1506
+ removePauseReason("shell");
1507
+ }
1508
+ function buildHeartbeatBody(activity, timing, sequence, isFinal) {
1509
+ return {
1510
+ runId: activity.runId,
1511
+ activityData: activity.metadata,
1512
+ timingData: { activeMs: timing.activeMs, pausedMs: timing.pausedMs },
1513
+ windowSequence: sequence,
1514
+ isFinal
1515
+ };
1516
+ }
1517
+ async function flushHeartbeat(isFinal) {
1518
+ const activity = currentActivity;
1519
+ if (!activity) {
1520
+ return;
1521
+ }
1522
+ const trackedActivity = activity;
1523
+ const sequence = trackedActivity.windowSequence;
1524
+ const timing = computeWindowAndReset(trackedActivity);
1525
+ if (timing.activeMs === 0 && timing.pausedMs === 0) {
1526
+ return;
1527
+ }
1528
+ const body = buildHeartbeatBody(trackedActivity, timing, sequence, isFinal);
1529
+ await queueHeartbeatFlush(trackedActivity, timing, async () => {
1530
+ try {
1531
+ await client["requestGameBackend"](TIMEBACK_ROUTES.HEARTBEAT, "POST", body);
1532
+ } catch {}
1533
+ });
1534
+ }
1535
+ function handlePageHide() {
1536
+ const activity = currentActivity;
1537
+ if (!activity) {
1538
+ return;
1539
+ }
1540
+ const sequence = activity.windowSequence;
1541
+ const timing = computeWindowAndReset(activity);
1542
+ if (timing.activeMs === 0 && timing.pausedMs === 0) {
1543
+ return;
1544
+ }
1545
+ const body = buildHeartbeatBody(activity, timing, sequence, true);
1546
+ queueHeartbeatFlush(activity, timing, async () => {
1547
+ try {
1548
+ const baseUrl = client["getGameBackendUrl"]();
1549
+ const url = `${baseUrl}${TIMEBACK_ROUTES.HEARTBEAT}`;
1550
+ const headers = {
1551
+ "Content-Type": "application/json",
1552
+ ...client["authStrategy"].getHeaders()
1553
+ };
1554
+ const response = await fetch(url, {
1555
+ method: "POST",
1556
+ headers,
1557
+ body: JSON.stringify(body),
1558
+ keepalive: true
1559
+ });
1560
+ if (response.ok) {
1561
+ return;
1562
+ }
1563
+ } catch {}
1564
+ });
1565
+ }
1566
+ function cleanupListeners() {
1567
+ if (boundVisibilityHandler && typeof document !== "undefined") {
1568
+ document.removeEventListener("visibilitychange", boundVisibilityHandler);
1569
+ boundVisibilityHandler = null;
1570
+ }
1571
+ if (boundShellPauseHandler) {
1572
+ messaging.unlisten("PLAYCADEMY_PAUSE" /* PAUSE */, boundShellPauseHandler);
1573
+ boundShellPauseHandler = null;
1574
+ }
1575
+ if (boundShellResumeHandler) {
1576
+ messaging.unlisten("PLAYCADEMY_RESUME" /* RESUME */, boundShellResumeHandler);
1577
+ boundShellResumeHandler = null;
1578
+ }
1579
+ if (boundPageHideHandler && typeof globalThis.window !== "undefined") {
1580
+ globalThis.window.removeEventListener("pagehide", boundPageHideHandler);
1581
+ boundPageHideHandler = null;
1582
+ }
1583
+ if (currentActivity?.hiddenTimeoutId != null) {
1584
+ clearTimeout(currentActivity.hiddenTimeoutId);
1585
+ }
1586
+ if (currentActivity?.heartbeatIntervalId != null) {
1587
+ stopHeartbeatInterval(currentActivity);
1588
+ }
1589
+ }
1590
+ return {
1591
+ startActivity(metadata, options) {
1592
+ cleanupListeners();
1593
+ const now = Date.now();
1594
+ const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1595
+ currentActivity = {
1596
+ runId: crypto.randomUUID(),
1597
+ startTime: now,
1598
+ metadata,
1599
+ pausedTime: 0,
1600
+ pauseStartTime: null,
1601
+ pauseReasons: new Set,
1602
+ hiddenTimeoutId: null,
1603
+ hiddenTimedOut: false,
1604
+ hiddenTimeoutMs: options?.hiddenTimeoutMs ?? DEFAULT_HIDDEN_TIMEOUT_MS,
1605
+ windowStartTime: now,
1606
+ windowPausedAtStart: 0,
1607
+ windowSequence: 0,
1608
+ heartbeatIntervalId: null,
1609
+ heartbeatIntervalMs,
1610
+ flushInFlight: null,
1611
+ totalPersistedActiveMs: 0,
1612
+ totalPersistedPausedMs: 0
1613
+ };
1614
+ if (typeof document !== "undefined") {
1615
+ boundVisibilityHandler = handleVisibilityChange;
1616
+ document.addEventListener("visibilitychange", boundVisibilityHandler);
1617
+ if (document.visibilityState === "hidden") {
1618
+ handleVisibilityChange();
1619
+ }
1620
+ }
1621
+ startHeartbeatInterval(currentActivity);
1622
+ if (typeof globalThis.window !== "undefined") {
1623
+ boundPageHideHandler = handlePageHide;
1624
+ globalThis.window.addEventListener("pagehide", boundPageHideHandler);
1625
+ }
1626
+ boundShellPauseHandler = handleShellPause;
1627
+ boundShellResumeHandler = handleShellResume;
1628
+ messaging.listen("PLAYCADEMY_PAUSE" /* PAUSE */, boundShellPauseHandler);
1629
+ messaging.listen("PLAYCADEMY_RESUME" /* RESUME */, boundShellResumeHandler);
1630
+ },
1631
+ pauseActivity() {
1632
+ if (!currentActivity) {
1633
+ throw new Error("No activity in progress. Call startActivity() before pauseActivity().");
1634
+ }
1635
+ if (currentActivity.pauseReasons.has("manual")) {
1636
+ throw new Error("Activity is already paused.");
1637
+ }
1638
+ addPauseReason("manual");
1639
+ },
1640
+ resumeActivity() {
1641
+ if (!currentActivity) {
1642
+ throw new Error("No activity in progress. Call startActivity() before resumeActivity().");
1643
+ }
1644
+ if (!currentActivity.pauseReasons.has("manual")) {
1645
+ throw new Error("Activity is not paused.");
1646
+ }
1647
+ removePauseReason("manual");
1648
+ },
1649
+ async endActivity(data) {
1650
+ if (!currentActivity) {
1651
+ throw new Error("No activity in progress. Call startActivity() before endActivity().");
1652
+ }
1653
+ const activity = currentActivity;
1654
+ cleanupListeners();
1655
+ await flushHeartbeat(true);
1656
+ if (activity.pauseStartTime !== null) {
1657
+ activity.pausedTime += Date.now() - activity.pauseStartTime;
1658
+ activity.pauseStartTime = null;
1659
+ }
1660
+ const endTime = Date.now();
1661
+ const totalElapsed = endTime - activity.startTime;
1662
+ const activeTime = Math.max(0, totalElapsed - activity.pausedTime);
1663
+ const durationSeconds = Math.floor(activeTime / 1000);
1664
+ const unreportedActiveMs = Math.max(0, activeTime - activity.totalPersistedActiveMs);
1665
+ const unreportedPausedMs = Math.max(0, activity.pausedTime - activity.totalPersistedPausedMs);
1666
+ const { correctQuestions, totalQuestions } = data;
1667
+ const request = {
1668
+ runId: activity.runId,
1669
+ activityData: activity.metadata,
1670
+ scoreData: {
1671
+ correctQuestions,
1672
+ totalQuestions
1673
+ },
1674
+ timingData: {
1675
+ durationSeconds
1676
+ },
1677
+ sessionTimingData: {
1678
+ activeSeconds: unreportedActiveMs / 1000,
1679
+ ...unreportedPausedMs > 0 ? { inactiveSeconds: unreportedPausedMs / 1000 } : {}
1680
+ },
1681
+ xpEarned: data.xpAwarded,
1682
+ masteredUnits: data.masteredUnits,
1683
+ extensions: data.extensions
1684
+ };
1685
+ try {
1686
+ const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request);
1687
+ if (currentActivity === activity) {
1688
+ currentActivity = null;
1689
+ }
1690
+ return response;
1691
+ } catch (error) {
1692
+ if (currentActivity === activity) {
1693
+ currentActivity = null;
1694
+ }
1695
+ throw error;
1696
+ }
1697
+ }
1698
+ };
1699
+ }
1700
+
1381
1701
  // src/core/cache/ttl-cache.ts
1382
1702
  function createTTLCache(options) {
1383
1703
  const cache = new Map;
@@ -1473,7 +1793,7 @@ function isValidSubject(value) {
1473
1793
 
1474
1794
  // src/namespaces/game/timeback.ts
1475
1795
  function createTimebackNamespace(client) {
1476
- let currentActivity = null;
1796
+ const activityTracker = createTimebackActivityTracker(client);
1477
1797
  const userCache = createTTLCache({
1478
1798
  ttl: 5 * 60 * 1000,
1479
1799
  keyPrefix: "game.timeback.user"
@@ -1558,70 +1878,16 @@ function createTimebackNamespace(client) {
1558
1878
  }
1559
1879
  };
1560
1880
  },
1561
- startActivity: (metadata) => {
1562
- currentActivity = {
1563
- startTime: Date.now(),
1564
- metadata,
1565
- pausedTime: 0,
1566
- pauseStartTime: null
1567
- };
1881
+ startActivity: (metadata, options) => {
1882
+ activityTracker.startActivity(metadata, options);
1568
1883
  },
1569
1884
  pauseActivity: () => {
1570
- if (!currentActivity) {
1571
- throw new Error("No activity in progress. Call startActivity() before pauseActivity().");
1572
- }
1573
- if (currentActivity.pauseStartTime !== null) {
1574
- throw new Error("Activity is already paused.");
1575
- }
1576
- currentActivity.pauseStartTime = Date.now();
1885
+ activityTracker.pauseActivity();
1577
1886
  },
1578
1887
  resumeActivity: () => {
1579
- if (!currentActivity) {
1580
- throw new Error("No activity in progress. Call startActivity() before resumeActivity().");
1581
- }
1582
- if (currentActivity.pauseStartTime === null) {
1583
- throw new Error("Activity is not paused.");
1584
- }
1585
- const pauseDuration = Date.now() - currentActivity.pauseStartTime;
1586
- currentActivity.pausedTime += pauseDuration;
1587
- currentActivity.pauseStartTime = null;
1888
+ activityTracker.resumeActivity();
1588
1889
  },
1589
- endActivity: async (data) => {
1590
- if (!currentActivity) {
1591
- throw new Error("No activity in progress. Call startActivity() before endActivity().");
1592
- }
1593
- if (currentActivity.pauseStartTime !== null) {
1594
- const pauseDuration = Date.now() - currentActivity.pauseStartTime;
1595
- currentActivity.pausedTime += pauseDuration;
1596
- currentActivity.pauseStartTime = null;
1597
- }
1598
- const endTime = Date.now();
1599
- const totalElapsed = endTime - currentActivity.startTime;
1600
- const activeTime = totalElapsed - currentActivity.pausedTime;
1601
- const durationSeconds = Math.floor(activeTime / 1000);
1602
- const { correctQuestions, totalQuestions } = data;
1603
- const request = {
1604
- activityData: currentActivity.metadata,
1605
- scoreData: {
1606
- correctQuestions,
1607
- totalQuestions
1608
- },
1609
- timingData: {
1610
- durationSeconds
1611
- },
1612
- xpEarned: data.xpAwarded,
1613
- masteredUnits: data.masteredUnits,
1614
- extensions: data.extensions
1615
- };
1616
- try {
1617
- const response = await client["requestGameBackend"](TIMEBACK_ROUTES.END_ACTIVITY, "POST", request);
1618
- currentActivity = null;
1619
- return response;
1620
- } catch (error) {
1621
- currentActivity = null;
1622
- throw error;
1623
- }
1624
- }
1890
+ endActivity: async (data) => activityTracker.endActivity(data)
1625
1891
  };
1626
1892
  }
1627
1893
  // src/core/auth/strategies.ts