@playcademy/sdk 0.4.1-beta.6 → 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 +13 -0
- package/dist/index.js +22 -10
- package/dist/internal.d.ts +8 -1
- package/dist/internal.js +22 -10
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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/internal.d.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
/**
|