@playcademy/sdk 0.12.1-beta.1 → 0.12.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 +14 -1
- package/dist/index.js +82 -22
- package/dist/internal.d.ts +15 -2
- package/dist/internal.js +82 -22
- package/dist/server/edge.js +1 -1
- package/dist/server.js +1 -1
- package/dist/types.d.ts +13 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _playcademy_types from '@playcademy/types';
|
|
2
|
-
import { TimebackGrade, TimebackSubject } from '@playcademy/types/timeback';
|
|
2
|
+
import { TimebackGrade, TimebackSubject, HeartbeatRequest } from '@playcademy/types/timeback';
|
|
3
3
|
import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
|
|
4
4
|
import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
|
|
5
5
|
|
|
@@ -511,6 +511,12 @@ declare enum MessageEvents {
|
|
|
511
511
|
* - `metadata?`: `Record<string, unknown>`
|
|
512
512
|
*/
|
|
513
513
|
DEMO_END = "PLAYCADEMY_DEMO_END",
|
|
514
|
+
/**
|
|
515
|
+
* Game shares its latest TimeBack heartbeat window with the parent shell.
|
|
516
|
+
* The shell can relay this payload during top-level page teardown, which is
|
|
517
|
+
* more reliable than relying only on cross-origin iframe unload events.
|
|
518
|
+
*/
|
|
519
|
+
TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
|
|
514
520
|
/**
|
|
515
521
|
* Notifies about authentication state changes.
|
|
516
522
|
* Can be sent in both directions depending on auth flow.
|
|
@@ -578,6 +584,8 @@ interface MessageEventMap {
|
|
|
578
584
|
[MessageEvents.KEY_EVENT]: KeyEventPayload;
|
|
579
585
|
/** Demo end signal from game */
|
|
580
586
|
[MessageEvents.DEMO_END]: DemoEndPayload;
|
|
587
|
+
/** Latest TimeBack heartbeat window for parent-shell unload relay */
|
|
588
|
+
[MessageEvents.TIMEBACK_HEARTBEAT_RELAY]: TimebackHeartbeatRelayRequest;
|
|
581
589
|
/** Authentication state change notification */
|
|
582
590
|
[MessageEvents.AUTH_STATE_CHANGE]: AuthStateChangePayload;
|
|
583
591
|
/** OAuth callback data from popup/new-tab windows */
|
|
@@ -1442,6 +1450,8 @@ interface InitPayload {
|
|
|
1442
1450
|
mode?: PlaycademyMode;
|
|
1443
1451
|
/** Launch session correlation ID (UUID, set by platform on game launch) */
|
|
1444
1452
|
launchId?: string;
|
|
1453
|
+
/** When `true`, the parent shell provides a heartbeat relay via postMessage, so the SDK can skip its own `fetch({ keepalive })` beacon on pagehide. Defaults to `false`. */
|
|
1454
|
+
hasHeartbeatRelay?: boolean;
|
|
1445
1455
|
}
|
|
1446
1456
|
interface GameContextPayload {
|
|
1447
1457
|
token: string;
|
|
@@ -1547,6 +1557,9 @@ interface DemoEndOptions {
|
|
|
1547
1557
|
interface DemoEndPayload extends DemoEndOptions {
|
|
1548
1558
|
score: number;
|
|
1549
1559
|
}
|
|
1560
|
+
type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
|
|
1561
|
+
windowStartedAtMs: number;
|
|
1562
|
+
};
|
|
1550
1563
|
|
|
1551
1564
|
/**
|
|
1552
1565
|
* SDK-specific API response types
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ var MessageEvents;
|
|
|
25
25
|
MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
|
|
26
26
|
MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
|
|
27
27
|
MessageEvents2["DEMO_END"] = "PLAYCADEMY_DEMO_END";
|
|
28
|
+
MessageEvents2["TIMEBACK_HEARTBEAT_RELAY"] = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY";
|
|
28
29
|
MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
|
|
29
30
|
MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
|
|
30
31
|
})(MessageEvents ||= {});
|
|
@@ -85,7 +86,8 @@ class PlaycademyMessaging {
|
|
|
85
86
|
"PLAYCADEMY_EXIT" /* EXIT */,
|
|
86
87
|
"PLAYCADEMY_TELEMETRY" /* TELEMETRY */,
|
|
87
88
|
"PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */,
|
|
88
|
-
"PLAYCADEMY_DEMO_END" /* DEMO_END
|
|
89
|
+
"PLAYCADEMY_DEMO_END" /* DEMO_END */,
|
|
90
|
+
"PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */
|
|
89
91
|
];
|
|
90
92
|
const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
|
|
91
93
|
return {
|
|
@@ -1242,6 +1244,7 @@ function isValidUUID(value) {
|
|
|
1242
1244
|
var DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1243
1245
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
|
|
1244
1246
|
var DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1247
|
+
var RELAY_INTERVAL_MS = 1000;
|
|
1245
1248
|
var HEARTBEAT_RETRY_POLICY = { retryableMethods: ["POST"] };
|
|
1246
1249
|
var USER_ACTIVITY_EVENTS = ["keydown", "pointerdown", "pointermove", "wheel"];
|
|
1247
1250
|
var USER_ACTIVITY_LISTENER_OPTIONS = { capture: true };
|
|
@@ -1275,6 +1278,14 @@ function computeWindowAndReset(activity) {
|
|
|
1275
1278
|
activity.windowPausedAtStart = totalPaused;
|
|
1276
1279
|
return { activeMs, pausedMs: windowPaused, windowStartedAtMs };
|
|
1277
1280
|
}
|
|
1281
|
+
function computeWindowSnapshot(activity) {
|
|
1282
|
+
const now = Date.now();
|
|
1283
|
+
const totalPaused = getCurrentPausedTotal(activity, now);
|
|
1284
|
+
const windowPaused = totalPaused - activity.windowPausedAtStart;
|
|
1285
|
+
const windowElapsed = now - activity.windowStartTime;
|
|
1286
|
+
const activeMs = Math.max(0, windowElapsed - windowPaused);
|
|
1287
|
+
return { activeMs, pausedMs: windowPaused, windowStartedAtMs: activity.windowStartTime };
|
|
1288
|
+
}
|
|
1278
1289
|
function markPersistedTiming(activity, timing) {
|
|
1279
1290
|
activity.totalPersistedActiveMs += timing.activeMs;
|
|
1280
1291
|
activity.totalPersistedPausedMs += timing.pausedMs;
|
|
@@ -1340,6 +1351,10 @@ function createTimebackActivityTracker(client) {
|
|
|
1340
1351
|
let boundShellPauseHandler = null;
|
|
1341
1352
|
let boundShellResumeHandler = null;
|
|
1342
1353
|
let boundPageHideHandler = null;
|
|
1354
|
+
let relayIntervalId = null;
|
|
1355
|
+
let lastRelayedActiveMs = -1;
|
|
1356
|
+
let lastRelayedPausedMs = -1;
|
|
1357
|
+
let lastRelayedWindowStart = -1;
|
|
1343
1358
|
function startHeartbeatInterval(activity) {
|
|
1344
1359
|
if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
|
|
1345
1360
|
return;
|
|
@@ -1355,10 +1370,12 @@ function createTimebackActivityTracker(client) {
|
|
|
1355
1370
|
activity.windowStartTime = now;
|
|
1356
1371
|
activity.windowPausedAtStart = pausedAtReset;
|
|
1357
1372
|
startHeartbeatInterval(activity);
|
|
1373
|
+
startRelayInterval();
|
|
1358
1374
|
}
|
|
1359
1375
|
function markPausedHeartbeatTimedOut(activity) {
|
|
1360
1376
|
activity.pausedHeartbeatTimeoutId = null;
|
|
1361
1377
|
activity.pausedHeartbeatTimedOut = true;
|
|
1378
|
+
stopRelayInterval();
|
|
1362
1379
|
stopHeartbeatInterval(activity);
|
|
1363
1380
|
}
|
|
1364
1381
|
function armPausedHeartbeatTimeout(activity, startedAt = Date.now()) {
|
|
@@ -1506,14 +1523,47 @@ function createTimebackActivityTracker(client) {
|
|
|
1506
1523
|
}
|
|
1507
1524
|
syncInactivityTracking();
|
|
1508
1525
|
}
|
|
1526
|
+
function startRelayInterval() {
|
|
1527
|
+
if (relayIntervalId !== null) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
relayIntervalId = setInterval(() => {
|
|
1531
|
+
const activity = currentActivity;
|
|
1532
|
+
if (!activity) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const timing = computeWindowSnapshot(activity);
|
|
1536
|
+
if (timing.activeMs === lastRelayedActiveMs && timing.pausedMs === lastRelayedPausedMs && timing.windowStartedAtMs === lastRelayedWindowStart) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
lastRelayedActiveMs = timing.activeMs;
|
|
1540
|
+
lastRelayedPausedMs = timing.pausedMs;
|
|
1541
|
+
lastRelayedWindowStart = timing.windowStartedAtMs;
|
|
1542
|
+
const body = buildHeartbeatBody(activity, timing);
|
|
1543
|
+
try {
|
|
1544
|
+
messaging.send("PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */, body);
|
|
1545
|
+
} catch {}
|
|
1546
|
+
}, RELAY_INTERVAL_MS);
|
|
1547
|
+
}
|
|
1548
|
+
function stopRelayInterval() {
|
|
1549
|
+
if (relayIntervalId !== null) {
|
|
1550
|
+
clearInterval(relayIntervalId);
|
|
1551
|
+
relayIntervalId = null;
|
|
1552
|
+
lastRelayedActiveMs = -1;
|
|
1553
|
+
lastRelayedPausedMs = -1;
|
|
1554
|
+
lastRelayedWindowStart = -1;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1509
1557
|
function handleVisibilityChange() {
|
|
1510
1558
|
if (!currentActivity) {
|
|
1511
1559
|
return;
|
|
1512
1560
|
}
|
|
1513
1561
|
if (document.visibilityState === "hidden") {
|
|
1514
1562
|
addPauseReason("hidden");
|
|
1563
|
+
flushFinalHeartbeatBeacon();
|
|
1515
1564
|
} else {
|
|
1516
1565
|
removePauseReason("hidden");
|
|
1566
|
+
startRelayInterval();
|
|
1517
1567
|
}
|
|
1518
1568
|
}
|
|
1519
1569
|
function handleShellPause() {
|
|
@@ -1553,7 +1603,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1553
1603
|
}
|
|
1554
1604
|
});
|
|
1555
1605
|
}
|
|
1556
|
-
function
|
|
1606
|
+
function flushFinalHeartbeatBeacon() {
|
|
1557
1607
|
applyOverdueInactivity();
|
|
1558
1608
|
const activity = currentActivity;
|
|
1559
1609
|
if (!activity) {
|
|
@@ -1564,26 +1614,34 @@ function createTimebackActivityTracker(client) {
|
|
|
1564
1614
|
return;
|
|
1565
1615
|
}
|
|
1566
1616
|
const body = buildHeartbeatBody(activity, timing, true);
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1617
|
+
stopRelayInterval();
|
|
1618
|
+
try {
|
|
1619
|
+
messaging.send("PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */, body);
|
|
1620
|
+
} catch {}
|
|
1621
|
+
if (!client["initPayload"]?.hasHeartbeatRelay) {
|
|
1622
|
+
queueHeartbeatFlush(activity, timing, async () => {
|
|
1623
|
+
try {
|
|
1624
|
+
const baseUrl = client["getGameBackendUrl"]();
|
|
1625
|
+
const url = `${baseUrl}${TIMEBACK_ROUTES.HEARTBEAT}`;
|
|
1626
|
+
const headers = {
|
|
1627
|
+
"Content-Type": "application/json",
|
|
1628
|
+
...client["authStrategy"].getHeaders()
|
|
1629
|
+
};
|
|
1630
|
+
const response = await fetch(url, {
|
|
1631
|
+
method: "POST",
|
|
1632
|
+
headers,
|
|
1633
|
+
body: JSON.stringify(body),
|
|
1634
|
+
keepalive: true
|
|
1635
|
+
});
|
|
1636
|
+
return response.ok;
|
|
1637
|
+
} catch {
|
|
1638
|
+
return false;
|
|
1583
1639
|
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function handlePageHide() {
|
|
1644
|
+
flushFinalHeartbeatBeacon();
|
|
1587
1645
|
}
|
|
1588
1646
|
function cleanupListeners() {
|
|
1589
1647
|
if (boundVisibilityHandler && typeof document !== "undefined") {
|
|
@@ -1615,6 +1673,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1615
1673
|
if (currentActivity?.heartbeatIntervalId != null) {
|
|
1616
1674
|
stopHeartbeatInterval(currentActivity);
|
|
1617
1675
|
}
|
|
1676
|
+
stopRelayInterval();
|
|
1618
1677
|
}
|
|
1619
1678
|
return {
|
|
1620
1679
|
currentRunId() {
|
|
@@ -1665,6 +1724,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1665
1724
|
}
|
|
1666
1725
|
}
|
|
1667
1726
|
startHeartbeatInterval(currentActivity);
|
|
1727
|
+
startRelayInterval();
|
|
1668
1728
|
if (typeof globalThis.window !== "undefined") {
|
|
1669
1729
|
boundPageHideHandler = handlePageHide;
|
|
1670
1730
|
globalThis.window.addEventListener("pagehide", boundPageHideHandler);
|
|
@@ -2318,7 +2378,7 @@ async function request({
|
|
|
2318
2378
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
2319
2379
|
}
|
|
2320
2380
|
// src/version.ts
|
|
2321
|
-
var SDK_VERSION = "0.12.1-beta.
|
|
2381
|
+
var SDK_VERSION = "0.12.1-beta.2";
|
|
2322
2382
|
|
|
2323
2383
|
// src/clients/base.ts
|
|
2324
2384
|
class PlaycademyBaseClient {
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaInfo } from '@playcademy/cloudflare';
|
|
2
|
-
import { TimebackGrade, TimebackSubject, TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/types/timeback';
|
|
2
|
+
import { TimebackGrade, TimebackSubject, HeartbeatRequest, TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig } from '@playcademy/types/timeback';
|
|
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';
|
|
@@ -291,6 +291,12 @@ declare enum MessageEvents {
|
|
|
291
291
|
* - `metadata?`: `Record<string, unknown>`
|
|
292
292
|
*/
|
|
293
293
|
DEMO_END = "PLAYCADEMY_DEMO_END",
|
|
294
|
+
/**
|
|
295
|
+
* Game shares its latest TimeBack heartbeat window with the parent shell.
|
|
296
|
+
* The shell can relay this payload during top-level page teardown, which is
|
|
297
|
+
* more reliable than relying only on cross-origin iframe unload events.
|
|
298
|
+
*/
|
|
299
|
+
TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
|
|
294
300
|
/**
|
|
295
301
|
* Notifies about authentication state changes.
|
|
296
302
|
* Can be sent in both directions depending on auth flow.
|
|
@@ -358,6 +364,8 @@ interface MessageEventMap {
|
|
|
358
364
|
[MessageEvents.KEY_EVENT]: KeyEventPayload;
|
|
359
365
|
/** Demo end signal from game */
|
|
360
366
|
[MessageEvents.DEMO_END]: DemoEndPayload;
|
|
367
|
+
/** Latest TimeBack heartbeat window for parent-shell unload relay */
|
|
368
|
+
[MessageEvents.TIMEBACK_HEARTBEAT_RELAY]: TimebackHeartbeatRelayRequest;
|
|
361
369
|
/** Authentication state change notification */
|
|
362
370
|
[MessageEvents.AUTH_STATE_CHANGE]: AuthStateChangePayload;
|
|
363
371
|
/** OAuth callback data from popup/new-tab windows */
|
|
@@ -1293,6 +1301,8 @@ interface InitPayload {
|
|
|
1293
1301
|
mode?: PlaycademyMode;
|
|
1294
1302
|
/** Launch session correlation ID (UUID, set by platform on game launch) */
|
|
1295
1303
|
launchId?: string;
|
|
1304
|
+
/** When `true`, the parent shell provides a heartbeat relay via postMessage, so the SDK can skip its own `fetch({ keepalive })` beacon on pagehide. Defaults to `false`. */
|
|
1305
|
+
hasHeartbeatRelay?: boolean;
|
|
1296
1306
|
}
|
|
1297
1307
|
/**
|
|
1298
1308
|
* Simplified user data passed to games via InitPayload
|
|
@@ -1437,6 +1447,9 @@ interface DemoEndOptions {
|
|
|
1437
1447
|
interface DemoEndPayload extends DemoEndOptions {
|
|
1438
1448
|
score: number;
|
|
1439
1449
|
}
|
|
1450
|
+
type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
|
|
1451
|
+
windowStartedAtMs: number;
|
|
1452
|
+
};
|
|
1440
1453
|
|
|
1441
1454
|
/**
|
|
1442
1455
|
* SDK-specific API response types
|
|
@@ -3152,4 +3165,4 @@ declare class PlaycademyInternalClient extends PlaycademyBaseClient {
|
|
|
3152
3165
|
}
|
|
3153
3166
|
|
|
3154
3167
|
export { ApiError, MessageEvents, PlaycademyInternalClient as PlaycademyClient, PlaycademyError, PlaycademyInternalClient, extractApiErrorInfo, messaging };
|
|
3155
|
-
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, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, 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, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
|
|
3168
|
+
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, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, MessageEventMap, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackHeartbeatRelayRequest, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
|
package/dist/internal.js
CHANGED
|
@@ -25,6 +25,7 @@ var MessageEvents;
|
|
|
25
25
|
MessageEvents2["TELEMETRY"] = "PLAYCADEMY_TELEMETRY";
|
|
26
26
|
MessageEvents2["KEY_EVENT"] = "PLAYCADEMY_KEY_EVENT";
|
|
27
27
|
MessageEvents2["DEMO_END"] = "PLAYCADEMY_DEMO_END";
|
|
28
|
+
MessageEvents2["TIMEBACK_HEARTBEAT_RELAY"] = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY";
|
|
28
29
|
MessageEvents2["AUTH_STATE_CHANGE"] = "PLAYCADEMY_AUTH_STATE_CHANGE";
|
|
29
30
|
MessageEvents2["AUTH_CALLBACK"] = "PLAYCADEMY_AUTH_CALLBACK";
|
|
30
31
|
})(MessageEvents ||= {});
|
|
@@ -85,7 +86,8 @@ class PlaycademyMessaging {
|
|
|
85
86
|
"PLAYCADEMY_EXIT" /* EXIT */,
|
|
86
87
|
"PLAYCADEMY_TELEMETRY" /* TELEMETRY */,
|
|
87
88
|
"PLAYCADEMY_KEY_EVENT" /* KEY_EVENT */,
|
|
88
|
-
"PLAYCADEMY_DEMO_END" /* DEMO_END
|
|
89
|
+
"PLAYCADEMY_DEMO_END" /* DEMO_END */,
|
|
90
|
+
"PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */
|
|
89
91
|
];
|
|
90
92
|
const shouldUsePostMessage = isIframe && iframeToParentEvents.includes(eventType);
|
|
91
93
|
return {
|
|
@@ -1242,6 +1244,7 @@ function isValidUUID(value) {
|
|
|
1242
1244
|
var DEFAULT_PAUSED_HEARTBEAT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1243
1245
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15000;
|
|
1244
1246
|
var DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1247
|
+
var RELAY_INTERVAL_MS = 1000;
|
|
1245
1248
|
var HEARTBEAT_RETRY_POLICY = { retryableMethods: ["POST"] };
|
|
1246
1249
|
var USER_ACTIVITY_EVENTS = ["keydown", "pointerdown", "pointermove", "wheel"];
|
|
1247
1250
|
var USER_ACTIVITY_LISTENER_OPTIONS = { capture: true };
|
|
@@ -1275,6 +1278,14 @@ function computeWindowAndReset(activity) {
|
|
|
1275
1278
|
activity.windowPausedAtStart = totalPaused;
|
|
1276
1279
|
return { activeMs, pausedMs: windowPaused, windowStartedAtMs };
|
|
1277
1280
|
}
|
|
1281
|
+
function computeWindowSnapshot(activity) {
|
|
1282
|
+
const now = Date.now();
|
|
1283
|
+
const totalPaused = getCurrentPausedTotal(activity, now);
|
|
1284
|
+
const windowPaused = totalPaused - activity.windowPausedAtStart;
|
|
1285
|
+
const windowElapsed = now - activity.windowStartTime;
|
|
1286
|
+
const activeMs = Math.max(0, windowElapsed - windowPaused);
|
|
1287
|
+
return { activeMs, pausedMs: windowPaused, windowStartedAtMs: activity.windowStartTime };
|
|
1288
|
+
}
|
|
1278
1289
|
function markPersistedTiming(activity, timing) {
|
|
1279
1290
|
activity.totalPersistedActiveMs += timing.activeMs;
|
|
1280
1291
|
activity.totalPersistedPausedMs += timing.pausedMs;
|
|
@@ -1340,6 +1351,10 @@ function createTimebackActivityTracker(client) {
|
|
|
1340
1351
|
let boundShellPauseHandler = null;
|
|
1341
1352
|
let boundShellResumeHandler = null;
|
|
1342
1353
|
let boundPageHideHandler = null;
|
|
1354
|
+
let relayIntervalId = null;
|
|
1355
|
+
let lastRelayedActiveMs = -1;
|
|
1356
|
+
let lastRelayedPausedMs = -1;
|
|
1357
|
+
let lastRelayedWindowStart = -1;
|
|
1343
1358
|
function startHeartbeatInterval(activity) {
|
|
1344
1359
|
if (activity.heartbeatIntervalMs === Infinity || activity.heartbeatIntervalId !== null) {
|
|
1345
1360
|
return;
|
|
@@ -1355,10 +1370,12 @@ function createTimebackActivityTracker(client) {
|
|
|
1355
1370
|
activity.windowStartTime = now;
|
|
1356
1371
|
activity.windowPausedAtStart = pausedAtReset;
|
|
1357
1372
|
startHeartbeatInterval(activity);
|
|
1373
|
+
startRelayInterval();
|
|
1358
1374
|
}
|
|
1359
1375
|
function markPausedHeartbeatTimedOut(activity) {
|
|
1360
1376
|
activity.pausedHeartbeatTimeoutId = null;
|
|
1361
1377
|
activity.pausedHeartbeatTimedOut = true;
|
|
1378
|
+
stopRelayInterval();
|
|
1362
1379
|
stopHeartbeatInterval(activity);
|
|
1363
1380
|
}
|
|
1364
1381
|
function armPausedHeartbeatTimeout(activity, startedAt = Date.now()) {
|
|
@@ -1506,14 +1523,47 @@ function createTimebackActivityTracker(client) {
|
|
|
1506
1523
|
}
|
|
1507
1524
|
syncInactivityTracking();
|
|
1508
1525
|
}
|
|
1526
|
+
function startRelayInterval() {
|
|
1527
|
+
if (relayIntervalId !== null) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
relayIntervalId = setInterval(() => {
|
|
1531
|
+
const activity = currentActivity;
|
|
1532
|
+
if (!activity) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const timing = computeWindowSnapshot(activity);
|
|
1536
|
+
if (timing.activeMs === lastRelayedActiveMs && timing.pausedMs === lastRelayedPausedMs && timing.windowStartedAtMs === lastRelayedWindowStart) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
lastRelayedActiveMs = timing.activeMs;
|
|
1540
|
+
lastRelayedPausedMs = timing.pausedMs;
|
|
1541
|
+
lastRelayedWindowStart = timing.windowStartedAtMs;
|
|
1542
|
+
const body = buildHeartbeatBody(activity, timing);
|
|
1543
|
+
try {
|
|
1544
|
+
messaging.send("PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */, body);
|
|
1545
|
+
} catch {}
|
|
1546
|
+
}, RELAY_INTERVAL_MS);
|
|
1547
|
+
}
|
|
1548
|
+
function stopRelayInterval() {
|
|
1549
|
+
if (relayIntervalId !== null) {
|
|
1550
|
+
clearInterval(relayIntervalId);
|
|
1551
|
+
relayIntervalId = null;
|
|
1552
|
+
lastRelayedActiveMs = -1;
|
|
1553
|
+
lastRelayedPausedMs = -1;
|
|
1554
|
+
lastRelayedWindowStart = -1;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1509
1557
|
function handleVisibilityChange() {
|
|
1510
1558
|
if (!currentActivity) {
|
|
1511
1559
|
return;
|
|
1512
1560
|
}
|
|
1513
1561
|
if (document.visibilityState === "hidden") {
|
|
1514
1562
|
addPauseReason("hidden");
|
|
1563
|
+
flushFinalHeartbeatBeacon();
|
|
1515
1564
|
} else {
|
|
1516
1565
|
removePauseReason("hidden");
|
|
1566
|
+
startRelayInterval();
|
|
1517
1567
|
}
|
|
1518
1568
|
}
|
|
1519
1569
|
function handleShellPause() {
|
|
@@ -1553,7 +1603,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1553
1603
|
}
|
|
1554
1604
|
});
|
|
1555
1605
|
}
|
|
1556
|
-
function
|
|
1606
|
+
function flushFinalHeartbeatBeacon() {
|
|
1557
1607
|
applyOverdueInactivity();
|
|
1558
1608
|
const activity = currentActivity;
|
|
1559
1609
|
if (!activity) {
|
|
@@ -1564,26 +1614,34 @@ function createTimebackActivityTracker(client) {
|
|
|
1564
1614
|
return;
|
|
1565
1615
|
}
|
|
1566
1616
|
const body = buildHeartbeatBody(activity, timing, true);
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1617
|
+
stopRelayInterval();
|
|
1618
|
+
try {
|
|
1619
|
+
messaging.send("PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY" /* TIMEBACK_HEARTBEAT_RELAY */, body);
|
|
1620
|
+
} catch {}
|
|
1621
|
+
if (!client["initPayload"]?.hasHeartbeatRelay) {
|
|
1622
|
+
queueHeartbeatFlush(activity, timing, async () => {
|
|
1623
|
+
try {
|
|
1624
|
+
const baseUrl = client["getGameBackendUrl"]();
|
|
1625
|
+
const url = `${baseUrl}${TIMEBACK_ROUTES.HEARTBEAT}`;
|
|
1626
|
+
const headers = {
|
|
1627
|
+
"Content-Type": "application/json",
|
|
1628
|
+
...client["authStrategy"].getHeaders()
|
|
1629
|
+
};
|
|
1630
|
+
const response = await fetch(url, {
|
|
1631
|
+
method: "POST",
|
|
1632
|
+
headers,
|
|
1633
|
+
body: JSON.stringify(body),
|
|
1634
|
+
keepalive: true
|
|
1635
|
+
});
|
|
1636
|
+
return response.ok;
|
|
1637
|
+
} catch {
|
|
1638
|
+
return false;
|
|
1583
1639
|
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function handlePageHide() {
|
|
1644
|
+
flushFinalHeartbeatBeacon();
|
|
1587
1645
|
}
|
|
1588
1646
|
function cleanupListeners() {
|
|
1589
1647
|
if (boundVisibilityHandler && typeof document !== "undefined") {
|
|
@@ -1615,6 +1673,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1615
1673
|
if (currentActivity?.heartbeatIntervalId != null) {
|
|
1616
1674
|
stopHeartbeatInterval(currentActivity);
|
|
1617
1675
|
}
|
|
1676
|
+
stopRelayInterval();
|
|
1618
1677
|
}
|
|
1619
1678
|
return {
|
|
1620
1679
|
currentRunId() {
|
|
@@ -1665,6 +1724,7 @@ function createTimebackActivityTracker(client) {
|
|
|
1665
1724
|
}
|
|
1666
1725
|
}
|
|
1667
1726
|
startHeartbeatInterval(currentActivity);
|
|
1727
|
+
startRelayInterval();
|
|
1668
1728
|
if (typeof globalThis.window !== "undefined") {
|
|
1669
1729
|
boundPageHideHandler = handlePageHide;
|
|
1670
1730
|
globalThis.window.addEventListener("pagehide", boundPageHideHandler);
|
|
@@ -3009,7 +3069,7 @@ async function request({
|
|
|
3009
3069
|
return rawText && rawText.length > 0 ? rawText : undefined;
|
|
3010
3070
|
}
|
|
3011
3071
|
// src/version.ts
|
|
3012
|
-
var SDK_VERSION = "0.12.1-beta.
|
|
3072
|
+
var SDK_VERSION = "0.12.1-beta.2";
|
|
3013
3073
|
|
|
3014
3074
|
// src/clients/base.ts
|
|
3015
3075
|
class PlaycademyBaseClient {
|
package/dist/server/edge.js
CHANGED
package/dist/server.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as _playcademy_types from '@playcademy/types';
|
|
2
2
|
import { GameManifest } from '@playcademy/types';
|
|
3
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
|
-
import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig, TimebackGrade, TimebackSubject } from '@playcademy/types/timeback';
|
|
4
|
+
import { TimebackCourseConfig, CourseConfig, OrganizationConfig, ComponentConfig, ResourceConfig, ComponentResourceConfig, TimebackGrade, TimebackSubject, HeartbeatRequest } from '@playcademy/types/timeback';
|
|
5
5
|
export { QtiTestQuestionRef, QtiTestQuestionsResponse } from '@playcademy/types/timeback';
|
|
6
6
|
import { TimebackUserRole, UserEnrollment, UserOrganization, UserInfo } from '@playcademy/types/user';
|
|
7
7
|
import { AUTH_PROVIDER_IDS } from '@playcademy/constants';
|
|
@@ -157,6 +157,12 @@ declare enum MessageEvents {
|
|
|
157
157
|
* - `metadata?`: `Record<string, unknown>`
|
|
158
158
|
*/
|
|
159
159
|
DEMO_END = "PLAYCADEMY_DEMO_END",
|
|
160
|
+
/**
|
|
161
|
+
* Game shares its latest TimeBack heartbeat window with the parent shell.
|
|
162
|
+
* The shell can relay this payload during top-level page teardown, which is
|
|
163
|
+
* more reliable than relying only on cross-origin iframe unload events.
|
|
164
|
+
*/
|
|
165
|
+
TIMEBACK_HEARTBEAT_RELAY = "PLAYCADEMY_TIMEBACK_HEARTBEAT_RELAY",
|
|
160
166
|
/**
|
|
161
167
|
* Notifies about authentication state changes.
|
|
162
168
|
* Can be sent in both directions depending on auth flow.
|
|
@@ -1799,6 +1805,8 @@ interface InitPayload {
|
|
|
1799
1805
|
mode?: PlaycademyMode;
|
|
1800
1806
|
/** Launch session correlation ID (UUID, set by platform on game launch) */
|
|
1801
1807
|
launchId?: string;
|
|
1808
|
+
/** When `true`, the parent shell provides a heartbeat relay via postMessage, so the SDK can skip its own `fetch({ keepalive })` beacon on pagehide. Defaults to `false`. */
|
|
1809
|
+
hasHeartbeatRelay?: boolean;
|
|
1802
1810
|
}
|
|
1803
1811
|
/**
|
|
1804
1812
|
* Simplified user data passed to games via InitPayload
|
|
@@ -1943,6 +1951,9 @@ interface DemoEndOptions {
|
|
|
1943
1951
|
interface DemoEndPayload extends DemoEndOptions {
|
|
1944
1952
|
score: number;
|
|
1945
1953
|
}
|
|
1954
|
+
type TimebackHeartbeatRelayRequest = Omit<HeartbeatRequest, 'gameId' | 'studentId' | 'windowStartedAtMs' | 'windowSequence'> & {
|
|
1955
|
+
windowStartedAtMs: number;
|
|
1956
|
+
};
|
|
1946
1957
|
|
|
1947
1958
|
/**
|
|
1948
1959
|
* SDK-specific API response types
|
|
@@ -2182,4 +2193,4 @@ interface AssessmentBankStatus {
|
|
|
2182
2193
|
}
|
|
2183
2194
|
|
|
2184
2195
|
export { PlaycademyClient };
|
|
2185
|
-
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, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
|
|
2196
|
+
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, GetHighestGradeMasteredOptions, GetMasteryOptions, GetXpOptions, HighestGradeMasteredResponse, HostedGame, InitErrorPayload, InitPayload, KVKeyEntry, KVKeyMetadata, KVSeedEntry, KVStatsResponse, KeyEventPayload, LoginResponse, MasteryResponse, PlatformTimebackUser, PlatformTimebackUserContext, PlaycademyMode, PlaycademyServerClientConfig, PlaycademyServerClientState, ScoreSubmission, StartActivityOptions, StartActivityResult, TelemetryPayload, TimebackEnrollment, TimebackHeartbeatRelayRequest, TimebackInitContext, TimebackOrganization, TimebackUser, TimebackUserContext, TimebackUserHighestGradeMastered, TimebackUserMastery, TimebackUserRefreshField, TimebackUserRefreshOptions, TimebackUserXp, TokenRefreshPayload, TokenType, UpsertGameMetadataInput, UserRow as User, XpResponse };
|