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

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
@@ -997,6 +997,19 @@ interface StartActivityOptions {
997
997
  * final unload/endActivity flushes still run.
998
998
  */
999
999
  heartbeatIntervalMs?: number;
1000
+ /**
1001
+ * Stable identifier for this activity run. When provided, it is used on
1002
+ * every heartbeat and on endActivity instead of a freshly-generated UUID.
1003
+ *
1004
+ * Pass the same `runId` across multiple `startActivity()` calls (for
1005
+ * example, after the player closes and reopens a resumable activity) so
1006
+ * downstream systems can correlate related sessions into a single run.
1007
+ *
1008
+ * Must be a UUID (the backend validates it as such) and unique per
1009
+ * logical run. If omitted, the SDK generates a new UUID on each call,
1010
+ * which means every session is treated as its own run.
1011
+ */
1012
+ runId?: string;
1000
1013
  }
1001
1014
 
1002
1015
  /**
package/dist/index.js CHANGED
@@ -1379,6 +1379,15 @@ function createRealtimeNamespace(client) {
1379
1379
  }
1380
1380
  };
1381
1381
  }
1382
+ // ../utils/src/uuid.ts
1383
+ var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
1384
+ function isValidUUID(value) {
1385
+ if (!value || typeof value !== "string") {
1386
+ return false;
1387
+ }
1388
+ return UUID_REGEX.test(value);
1389
+ }
1390
+
1382
1391
  // src/core/activity-tracker.ts
1383
1392
  var DEFAULT_HIDDEN_TIMEOUT_MS = 10 * 60 * 1000;
1384
1393
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
@@ -1389,6 +1398,7 @@ function getCurrentPausedTotal(activity, now = Date.now()) {
1389
1398
  return activity.pausedTime + (now - activity.pauseStartTime);
1390
1399
  }
1391
1400
  function computeWindowAndReset(activity) {
1401
+ const windowStartedAtMs = activity.windowStartTime;
1392
1402
  const now = Date.now();
1393
1403
  const totalPaused = getCurrentPausedTotal(activity, now);
1394
1404
  if (activity.pauseStartTime !== null) {
@@ -1400,8 +1410,7 @@ function computeWindowAndReset(activity) {
1400
1410
  const activeMs = Math.max(0, windowElapsed - windowPaused);
1401
1411
  activity.windowStartTime = now;
1402
1412
  activity.windowPausedAtStart = totalPaused;
1403
- activity.windowSequence++;
1404
- return { activeMs, pausedMs: windowPaused };
1413
+ return { activeMs, pausedMs: windowPaused, windowStartedAtMs };
1405
1414
  }
1406
1415
  function markPersistedTiming(activity, timing) {
1407
1416
  activity.totalPersistedActiveMs += timing.activeMs;
@@ -1505,12 +1514,13 @@ function createTimebackActivityTracker(client) {
1505
1514
  function handleShellResume() {
1506
1515
  removePauseReason("shell");
1507
1516
  }
1508
- function buildHeartbeatBody(activity, timing, sequence, isFinal) {
1517
+ function buildHeartbeatBody(activity, timing, isFinal) {
1509
1518
  return {
1510
1519
  runId: activity.runId,
1520
+ resumeId: activity.resumeId,
1511
1521
  activityData: activity.metadata,
1512
1522
  timingData: { activeMs: timing.activeMs, pausedMs: timing.pausedMs },
1513
- windowSequence: sequence,
1523
+ windowStartedAtMs: timing.windowStartedAtMs,
1514
1524
  isFinal
1515
1525
  };
1516
1526
  }
@@ -1520,12 +1530,11 @@ function createTimebackActivityTracker(client) {
1520
1530
  return;
1521
1531
  }
1522
1532
  const trackedActivity = activity;
1523
- const sequence = trackedActivity.windowSequence;
1524
1533
  const timing = computeWindowAndReset(trackedActivity);
1525
1534
  if (timing.activeMs === 0 && timing.pausedMs === 0) {
1526
1535
  return;
1527
1536
  }
1528
- const body = buildHeartbeatBody(trackedActivity, timing, sequence, isFinal);
1537
+ const body = buildHeartbeatBody(trackedActivity, timing, isFinal);
1529
1538
  await queueHeartbeatFlush(trackedActivity, timing, async () => {
1530
1539
  try {
1531
1540
  await client["requestGameBackend"](TIMEBACK_ROUTES.HEARTBEAT, "POST", body);
@@ -1537,12 +1546,11 @@ function createTimebackActivityTracker(client) {
1537
1546
  if (!activity) {
1538
1547
  return;
1539
1548
  }
1540
- const sequence = activity.windowSequence;
1541
1549
  const timing = computeWindowAndReset(activity);
1542
1550
  if (timing.activeMs === 0 && timing.pausedMs === 0) {
1543
1551
  return;
1544
1552
  }
1545
- const body = buildHeartbeatBody(activity, timing, sequence, true);
1553
+ const body = buildHeartbeatBody(activity, timing, true);
1546
1554
  queueHeartbeatFlush(activity, timing, async () => {
1547
1555
  try {
1548
1556
  const baseUrl = client["getGameBackendUrl"]();
@@ -1589,11 +1597,15 @@ function createTimebackActivityTracker(client) {
1589
1597
  }
1590
1598
  return {
1591
1599
  startActivity(metadata, options) {
1600
+ if (options?.runId !== undefined && !isValidUUID(options.runId)) {
1601
+ throw new Error(`startActivity: \`runId\` must be a UUID (received \`${JSON.stringify(options.runId)}\`). Use crypto.randomUUID() or persist a previously-generated UUID.`);
1602
+ }
1592
1603
  cleanupListeners();
1593
1604
  const now = Date.now();
1594
1605
  const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1595
1606
  currentActivity = {
1596
- runId: crypto.randomUUID(),
1607
+ runId: options?.runId ?? crypto.randomUUID(),
1608
+ resumeId: crypto.randomUUID(),
1597
1609
  startTime: now,
1598
1610
  metadata,
1599
1611
  pausedTime: 0,
@@ -1604,7 +1616,6 @@ function createTimebackActivityTracker(client) {
1604
1616
  hiddenTimeoutMs: options?.hiddenTimeoutMs ?? DEFAULT_HIDDEN_TIMEOUT_MS,
1605
1617
  windowStartTime: now,
1606
1618
  windowPausedAtStart: 0,
1607
- windowSequence: 0,
1608
1619
  heartbeatIntervalId: null,
1609
1620
  heartbeatIntervalMs,
1610
1621
  flushInFlight: null,
@@ -1666,6 +1677,7 @@ function createTimebackActivityTracker(client) {
1666
1677
  const { correctQuestions, totalQuestions } = data;
1667
1678
  const request = {
1668
1679
  runId: activity.runId,
1680
+ resumeId: activity.resumeId,
1669
1681
  activityData: activity.metadata,
1670
1682
  scoreData: {
1671
1683
  correctQuestions,
@@ -457,7 +457,7 @@ interface TimebackStudentCourseOverview {
457
457
  completionStatus: CourseCompletionStatus;
458
458
  history: TimebackStudentHistoryPoint[];
459
459
  }
460
- type TimebackRecentActivityKind = 'activity' | 'time-spent' | 'remediation-xp' | 'remediation-time' | 'remediation-mastery' | 'course-completed' | 'course-resumed';
460
+ type TimebackRecentActivityKind = 'activity' | 'activity-in-progress' | 'time-spent' | 'remediation-xp' | 'remediation-time' | 'remediation-mastery' | 'course-completed' | 'course-resumed';
461
461
  interface TimebackRecentActivity {
462
462
  id: string;
463
463
  kind: TimebackRecentActivityKind;
@@ -471,6 +471,8 @@ interface TimebackRecentActivity {
471
471
  xpDelta?: number;
472
472
  timeDeltaSeconds?: number;
473
473
  masteredUnitsDelta?: number;
474
+ runId?: string;
475
+ sessionCount?: number;
474
476
  }
475
477
  interface TimebackStudentOverviewResponse {
476
478
  student: {
@@ -491,6 +493,7 @@ interface GrantTimebackXpRequest {
491
493
  xp: number;
492
494
  reason: string;
493
495
  date?: string;
496
+ useCurrentTime?: boolean;
494
497
  }
495
498
  interface AdjustTimebackTimeRequest {
496
499
  gameId: string;
@@ -499,6 +502,8 @@ interface AdjustTimebackTimeRequest {
499
502
  seconds: number;
500
503
  reason: string;
501
504
  date?: string;
505
+ /** See {@link GrantTimebackXpRequest.useCurrentTime}. */
506
+ useCurrentTime?: boolean;
502
507
  }
503
508
  interface AdjustTimebackMasteryRequest {
504
509
  gameId: string;
@@ -507,6 +512,8 @@ interface AdjustTimebackMasteryRequest {
507
512
  units: number;
508
513
  reason: string;
509
514
  date?: string;
515
+ /** See {@link GrantTimebackXpRequest.useCurrentTime}. */
516
+ useCurrentTime?: boolean;
510
517
  }
511
518
  interface ToggleCourseCompletionRequest {
512
519
  gameId: string;
package/dist/internal.js CHANGED
@@ -1379,6 +1379,15 @@ function createRealtimeNamespace(client) {
1379
1379
  }
1380
1380
  };
1381
1381
  }
1382
+ // ../utils/src/uuid.ts
1383
+ var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
1384
+ function isValidUUID(value) {
1385
+ if (!value || typeof value !== "string") {
1386
+ return false;
1387
+ }
1388
+ return UUID_REGEX.test(value);
1389
+ }
1390
+
1382
1391
  // src/core/activity-tracker.ts
1383
1392
  var DEFAULT_HIDDEN_TIMEOUT_MS = 10 * 60 * 1000;
1384
1393
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
@@ -1389,6 +1398,7 @@ function getCurrentPausedTotal(activity, now = Date.now()) {
1389
1398
  return activity.pausedTime + (now - activity.pauseStartTime);
1390
1399
  }
1391
1400
  function computeWindowAndReset(activity) {
1401
+ const windowStartedAtMs = activity.windowStartTime;
1392
1402
  const now = Date.now();
1393
1403
  const totalPaused = getCurrentPausedTotal(activity, now);
1394
1404
  if (activity.pauseStartTime !== null) {
@@ -1400,8 +1410,7 @@ function computeWindowAndReset(activity) {
1400
1410
  const activeMs = Math.max(0, windowElapsed - windowPaused);
1401
1411
  activity.windowStartTime = now;
1402
1412
  activity.windowPausedAtStart = totalPaused;
1403
- activity.windowSequence++;
1404
- return { activeMs, pausedMs: windowPaused };
1413
+ return { activeMs, pausedMs: windowPaused, windowStartedAtMs };
1405
1414
  }
1406
1415
  function markPersistedTiming(activity, timing) {
1407
1416
  activity.totalPersistedActiveMs += timing.activeMs;
@@ -1505,12 +1514,13 @@ function createTimebackActivityTracker(client) {
1505
1514
  function handleShellResume() {
1506
1515
  removePauseReason("shell");
1507
1516
  }
1508
- function buildHeartbeatBody(activity, timing, sequence, isFinal) {
1517
+ function buildHeartbeatBody(activity, timing, isFinal) {
1509
1518
  return {
1510
1519
  runId: activity.runId,
1520
+ resumeId: activity.resumeId,
1511
1521
  activityData: activity.metadata,
1512
1522
  timingData: { activeMs: timing.activeMs, pausedMs: timing.pausedMs },
1513
- windowSequence: sequence,
1523
+ windowStartedAtMs: timing.windowStartedAtMs,
1514
1524
  isFinal
1515
1525
  };
1516
1526
  }
@@ -1520,12 +1530,11 @@ function createTimebackActivityTracker(client) {
1520
1530
  return;
1521
1531
  }
1522
1532
  const trackedActivity = activity;
1523
- const sequence = trackedActivity.windowSequence;
1524
1533
  const timing = computeWindowAndReset(trackedActivity);
1525
1534
  if (timing.activeMs === 0 && timing.pausedMs === 0) {
1526
1535
  return;
1527
1536
  }
1528
- const body = buildHeartbeatBody(trackedActivity, timing, sequence, isFinal);
1537
+ const body = buildHeartbeatBody(trackedActivity, timing, isFinal);
1529
1538
  await queueHeartbeatFlush(trackedActivity, timing, async () => {
1530
1539
  try {
1531
1540
  await client["requestGameBackend"](TIMEBACK_ROUTES.HEARTBEAT, "POST", body);
@@ -1537,12 +1546,11 @@ function createTimebackActivityTracker(client) {
1537
1546
  if (!activity) {
1538
1547
  return;
1539
1548
  }
1540
- const sequence = activity.windowSequence;
1541
1549
  const timing = computeWindowAndReset(activity);
1542
1550
  if (timing.activeMs === 0 && timing.pausedMs === 0) {
1543
1551
  return;
1544
1552
  }
1545
- const body = buildHeartbeatBody(activity, timing, sequence, true);
1553
+ const body = buildHeartbeatBody(activity, timing, true);
1546
1554
  queueHeartbeatFlush(activity, timing, async () => {
1547
1555
  try {
1548
1556
  const baseUrl = client["getGameBackendUrl"]();
@@ -1589,11 +1597,15 @@ function createTimebackActivityTracker(client) {
1589
1597
  }
1590
1598
  return {
1591
1599
  startActivity(metadata, options) {
1600
+ if (options?.runId !== undefined && !isValidUUID(options.runId)) {
1601
+ throw new Error(`startActivity: \`runId\` must be a UUID (received \`${JSON.stringify(options.runId)}\`). Use crypto.randomUUID() or persist a previously-generated UUID.`);
1602
+ }
1592
1603
  cleanupListeners();
1593
1604
  const now = Date.now();
1594
1605
  const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1595
1606
  currentActivity = {
1596
- runId: crypto.randomUUID(),
1607
+ runId: options?.runId ?? crypto.randomUUID(),
1608
+ resumeId: crypto.randomUUID(),
1597
1609
  startTime: now,
1598
1610
  metadata,
1599
1611
  pausedTime: 0,
@@ -1604,7 +1616,6 @@ function createTimebackActivityTracker(client) {
1604
1616
  hiddenTimeoutMs: options?.hiddenTimeoutMs ?? DEFAULT_HIDDEN_TIMEOUT_MS,
1605
1617
  windowStartTime: now,
1606
1618
  windowPausedAtStart: 0,
1607
- windowSequence: 0,
1608
1619
  heartbeatIntervalId: null,
1609
1620
  heartbeatIntervalMs,
1610
1621
  flushInFlight: null,
@@ -1666,6 +1677,7 @@ function createTimebackActivityTracker(client) {
1666
1677
  const { correctQuestions, totalQuestions } = data;
1667
1678
  const request = {
1668
1679
  runId: activity.runId,
1680
+ resumeId: activity.resumeId,
1669
1681
  activityData: activity.metadata,
1670
1682
  scoreData: {
1671
1683
  correctQuestions,
package/dist/types.d.ts CHANGED
@@ -4731,6 +4731,19 @@ interface StartActivityOptions {
4731
4731
  * final unload/endActivity flushes still run.
4732
4732
  */
4733
4733
  heartbeatIntervalMs?: number;
4734
+ /**
4735
+ * Stable identifier for this activity run. When provided, it is used on
4736
+ * every heartbeat and on endActivity instead of a freshly-generated UUID.
4737
+ *
4738
+ * Pass the same `runId` across multiple `startActivity()` calls (for
4739
+ * example, after the player closes and reopens a resumable activity) so
4740
+ * downstream systems can correlate related sessions into a single run.
4741
+ *
4742
+ * Must be a UUID (the backend validates it as such) and unique per
4743
+ * logical run. If omitted, the SDK generates a new UUID on each call,
4744
+ * which means every session is treated as its own run.
4745
+ */
4746
+ runId?: string;
4734
4747
  }
4735
4748
 
4736
4749
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sdk",
3
- "version": "0.4.1-beta.5",
3
+ "version": "0.4.1-beta.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {