@playcademy/vite-plugin 1.1.3-beta.1 → 1.1.3-beta.3
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.js +345 -51
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -23746,7 +23746,7 @@ import path from "node:path";
|
|
|
23746
23746
|
// package.json
|
|
23747
23747
|
var package_default = {
|
|
23748
23748
|
name: "@playcademy/vite-plugin",
|
|
23749
|
-
version: "1.1.3-beta.
|
|
23749
|
+
version: "1.1.3-beta.3",
|
|
23750
23750
|
type: "module",
|
|
23751
23751
|
exports: {
|
|
23752
23752
|
".": {
|
|
@@ -23796,9 +23796,8 @@ function formatNumberWithCommas(numStr) {
|
|
|
23796
23796
|
parts2[0] = parts2[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
23797
23797
|
return parts2.join(".");
|
|
23798
23798
|
}
|
|
23799
|
-
async function
|
|
23799
|
+
async function readPackageVersion(pkgJsonPath) {
|
|
23800
23800
|
try {
|
|
23801
|
-
const pkgJsonPath = path.resolve(projectRoot, "node_modules", packageName, "package.json");
|
|
23802
23801
|
const raw = await fs2.readFile(pkgJsonPath, "utf8");
|
|
23803
23802
|
const pkg = JSON.parse(raw);
|
|
23804
23803
|
return pkg.version;
|
|
@@ -23806,6 +23805,12 @@ async function resolvePackageVersion(projectRoot, packageName) {
|
|
|
23806
23805
|
return;
|
|
23807
23806
|
}
|
|
23808
23807
|
}
|
|
23808
|
+
async function resolvePackageVersion(projectRoot, packageName) {
|
|
23809
|
+
return readPackageVersion(path.resolve(projectRoot, "node_modules", packageName, "package.json"));
|
|
23810
|
+
}
|
|
23811
|
+
async function resolveGameVersion(projectRoot) {
|
|
23812
|
+
return readPackageVersion(path.resolve(projectRoot, "package.json"));
|
|
23813
|
+
}
|
|
23809
23814
|
async function resolveCliVersion() {
|
|
23810
23815
|
const bin = path.join(homedir(), ".playcademy", "bin", "playcademy");
|
|
23811
23816
|
return new Promise((resolve) => {
|
|
@@ -23835,11 +23840,15 @@ async function resolveVersions(projectRoot) {
|
|
|
23835
23840
|
return versions;
|
|
23836
23841
|
}
|
|
23837
23842
|
async function generatePlaycademyManifest(config, outDir, buildOutputs) {
|
|
23838
|
-
const versions = await
|
|
23843
|
+
const [versions, gameVersion] = await Promise.all([
|
|
23844
|
+
resolveVersions(config.root),
|
|
23845
|
+
resolveGameVersion(config.root)
|
|
23846
|
+
]);
|
|
23839
23847
|
const manifestData = {
|
|
23840
23848
|
version: "2",
|
|
23841
23849
|
platform: "web",
|
|
23842
23850
|
createdAt: new Date().toISOString(),
|
|
23851
|
+
...gameVersion ? { gameVersion } : {},
|
|
23843
23852
|
versions
|
|
23844
23853
|
};
|
|
23845
23854
|
const manifestPath = path.resolve(outDir, "playcademy.manifest.json");
|
|
@@ -24245,6 +24254,7 @@ import { Buffer as Buffer2 } from "node:buffer";
|
|
|
24245
24254
|
import { gzipSync } from "node:zlib";
|
|
24246
24255
|
import { stdout } from "process";
|
|
24247
24256
|
import { createHash } from "node:crypto";
|
|
24257
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
24248
24258
|
import crypto3 from "node:crypto";
|
|
24249
24259
|
import * as s3 from "fs";
|
|
24250
24260
|
import * as o3 from "path";
|
|
@@ -25345,7 +25355,7 @@ var package_default2;
|
|
|
25345
25355
|
var init_package = __esm(() => {
|
|
25346
25356
|
package_default2 = {
|
|
25347
25357
|
name: "@playcademy/sandbox",
|
|
25348
|
-
version: "0.6.
|
|
25358
|
+
version: "0.6.1-beta.4",
|
|
25349
25359
|
description: "Local development server for Playcademy game development",
|
|
25350
25360
|
type: "module",
|
|
25351
25361
|
exports: {
|
|
@@ -35620,6 +35630,7 @@ var gameTimebackIntegrationStatusEnum;
|
|
|
35620
35630
|
var gameTimebackIntegrations;
|
|
35621
35631
|
var gameTimebackAssessmentTests;
|
|
35622
35632
|
var gameTimebackMetricDiscrepancyVerifications;
|
|
35633
|
+
var gameTimebackActivityCompletions;
|
|
35623
35634
|
var init_table7 = __esm(() => {
|
|
35624
35635
|
init_drizzle_orm();
|
|
35625
35636
|
init_pg_core();
|
|
@@ -35671,6 +35682,22 @@ var init_table7 = __esm(() => {
|
|
|
35671
35682
|
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
35672
35683
|
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
35673
35684
|
]);
|
|
35685
|
+
gameTimebackActivityCompletions = pgTable("game_timeback_activity_completions", {
|
|
35686
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
35687
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
35688
|
+
courseId: text("course_id").notNull(),
|
|
35689
|
+
studentId: text("student_id").notNull(),
|
|
35690
|
+
runId: uuid("run_id").notNull(),
|
|
35691
|
+
activityId: text("activity_id").notNull(),
|
|
35692
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
35693
|
+
reservedAt: timestamp("reserved_at", { withTimezone: true, mode: "string" }).notNull().defaultNow(),
|
|
35694
|
+
xpAwarded: doublePrecision("xp_awarded"),
|
|
35695
|
+
masteredUnitsApplied: integer("mastered_units_applied"),
|
|
35696
|
+
pctCompleteApp: doublePrecision("pct_complete_app"),
|
|
35697
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
35698
|
+
}, (table3) => [
|
|
35699
|
+
uniqueIndex("game_timeback_activity_completions_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId, table3.activityId)
|
|
35700
|
+
]);
|
|
35674
35701
|
});
|
|
35675
35702
|
var exports_tables_index = {};
|
|
35676
35703
|
__export(exports_tables_index, {
|
|
@@ -35686,6 +35713,7 @@ __export(exports_tables_index, {
|
|
|
35686
35713
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
35687
35714
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
35688
35715
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
35716
|
+
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
35689
35717
|
gameScoresRelations: () => gameScoresRelations,
|
|
35690
35718
|
gameScores: () => gameScores,
|
|
35691
35719
|
gamePlatformEnum: () => gamePlatformEnum,
|
|
@@ -51123,11 +51151,18 @@ var METRICS_BASE;
|
|
|
51123
51151
|
var init_tunnel = __esm(() => {
|
|
51124
51152
|
METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
|
|
51125
51153
|
});
|
|
51154
|
+
function isPreviewStage(stage) {
|
|
51155
|
+
return PREVIEW_STAGE_PATTERN.test(stage);
|
|
51156
|
+
}
|
|
51157
|
+
var PREVIEW_STAGE_PATTERN;
|
|
51158
|
+
var init_stages = __esm(() => {
|
|
51159
|
+
PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
|
|
51160
|
+
});
|
|
51126
51161
|
function getDeploymentId(gameSlug, sstStage) {
|
|
51127
51162
|
if (sstStage === "production") {
|
|
51128
51163
|
return gameSlug;
|
|
51129
51164
|
}
|
|
51130
|
-
if (sstStage === "dev" || sstStage
|
|
51165
|
+
if (sstStage === "dev" || isPreviewStage(sstStage)) {
|
|
51131
51166
|
return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
|
|
51132
51167
|
}
|
|
51133
51168
|
return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
|
|
@@ -51147,6 +51182,7 @@ async function generateDeploymentHash(code) {
|
|
|
51147
51182
|
}
|
|
51148
51183
|
var init_deployment_util = __esm(() => {
|
|
51149
51184
|
init_src();
|
|
51185
|
+
init_stages();
|
|
51150
51186
|
});
|
|
51151
51187
|
|
|
51152
51188
|
class DeployService {
|
|
@@ -100027,7 +100063,8 @@ class ActivityRecord {
|
|
|
100027
100063
|
courseName,
|
|
100028
100064
|
subject: progressData.subject,
|
|
100029
100065
|
appName: progressData.appName,
|
|
100030
|
-
sensorUrl: progressData.sensorUrl
|
|
100066
|
+
sensorUrl: progressData.sensorUrl,
|
|
100067
|
+
eventId: progressData.completionHistoryEventId
|
|
100031
100068
|
});
|
|
100032
100069
|
}
|
|
100033
100070
|
if (masteryProgress?.masteryRevoked) {
|
|
@@ -100052,6 +100089,7 @@ class ActivityRecord {
|
|
|
100052
100089
|
subject: progressData.subject,
|
|
100053
100090
|
appName: progressData.appName,
|
|
100054
100091
|
sensorUrl: progressData.sensorUrl,
|
|
100092
|
+
eventId: progressData.eventId,
|
|
100055
100093
|
extensions: extensions || progressData.extensions,
|
|
100056
100094
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
100057
100095
|
}).catch((error88) => {
|
|
@@ -100075,6 +100113,7 @@ class ActivityRecord {
|
|
|
100075
100113
|
}
|
|
100076
100114
|
async emitCourseCompletionHistoryEvent(data) {
|
|
100077
100115
|
await this.events.emitActivityEvent({
|
|
100116
|
+
eventId: data.eventId,
|
|
100078
100117
|
studentId: data.studentId,
|
|
100079
100118
|
studentEmail: data.studentEmail,
|
|
100080
100119
|
gameId: data.gameId,
|
|
@@ -100190,6 +100229,7 @@ class ActivitySession {
|
|
|
100190
100229
|
subject: sessionData.subject,
|
|
100191
100230
|
appName: sessionData.appName,
|
|
100192
100231
|
sensorUrl: sessionData.sensorUrl,
|
|
100232
|
+
eventId: sessionData.eventId,
|
|
100193
100233
|
...runId ? { runId } : {},
|
|
100194
100234
|
...extensions ? { extensions } : {}
|
|
100195
100235
|
});
|
|
@@ -100456,6 +100496,9 @@ class ActivityEvents {
|
|
|
100456
100496
|
} : {},
|
|
100457
100497
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100458
100498
|
});
|
|
100499
|
+
if (data.eventId) {
|
|
100500
|
+
event.id = data.eventId;
|
|
100501
|
+
}
|
|
100459
100502
|
await this.core.api.caliper.events.send(data.sensorUrl, [event]);
|
|
100460
100503
|
}
|
|
100461
100504
|
async emitTimeSpentEvent(data) {
|
|
@@ -100487,6 +100530,9 @@ class ActivityEvents {
|
|
|
100487
100530
|
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
100488
100531
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100489
100532
|
});
|
|
100533
|
+
if (data.eventId) {
|
|
100534
|
+
event.id = data.eventId;
|
|
100535
|
+
}
|
|
100490
100536
|
const wireEvent = data.extensions ? { ...event, generated: { ...event.generated, extensions: data.extensions } } : event;
|
|
100491
100537
|
await this.core.api.caliper.events.send(data.sensorUrl, [wireEvent]);
|
|
100492
100538
|
}
|
|
@@ -105544,6 +105590,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
105544
105590
|
static HEARTBEAT_DEDUPE_TTL_MS = 300000;
|
|
105545
105591
|
static processedHeartbeatWindows = new Map;
|
|
105546
105592
|
static inFlightHeartbeatWindows = new Map;
|
|
105593
|
+
static COMPLETION_RESERVATION_STALE_SECONDS = 300;
|
|
105594
|
+
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
105595
|
+
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
105547
105596
|
deps;
|
|
105548
105597
|
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
105549
105598
|
for (const [key, timestamp6] of this.processedHeartbeatWindows) {
|
|
@@ -105568,6 +105617,136 @@ var init_timeback_service = __esm(async () => {
|
|
|
105568
105617
|
static clearInFlightHeartbeatWindow(key) {
|
|
105569
105618
|
this.inFlightHeartbeatWindows.delete(key);
|
|
105570
105619
|
}
|
|
105620
|
+
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
105621
|
+
const hash2 = createHash2("sha256").update([
|
|
105622
|
+
"playcademy",
|
|
105623
|
+
"timeback",
|
|
105624
|
+
kind,
|
|
105625
|
+
key.gameId,
|
|
105626
|
+
key.courseId,
|
|
105627
|
+
key.studentId,
|
|
105628
|
+
key.runId,
|
|
105629
|
+
key.activityId,
|
|
105630
|
+
discriminator
|
|
105631
|
+
].join("\x00")).digest("hex");
|
|
105632
|
+
const variant = (Number.parseInt(hash2.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
105633
|
+
const uuid9 = [
|
|
105634
|
+
hash2.slice(0, 8),
|
|
105635
|
+
hash2.slice(8, 12),
|
|
105636
|
+
`5${hash2.slice(13, 16)}`,
|
|
105637
|
+
`${variant}${hash2.slice(18, 20)}`,
|
|
105638
|
+
hash2.slice(20, 32)
|
|
105639
|
+
].join("-");
|
|
105640
|
+
return `urn:uuid:${uuid9}`;
|
|
105641
|
+
}
|
|
105642
|
+
static async reserveCompletion(db2, key) {
|
|
105643
|
+
const [reserved] = await db2.insert(gameTimebackActivityCompletions).values({
|
|
105644
|
+
gameId: key.gameId,
|
|
105645
|
+
courseId: key.courseId,
|
|
105646
|
+
studentId: key.studentId,
|
|
105647
|
+
runId: key.runId,
|
|
105648
|
+
activityId: key.activityId
|
|
105649
|
+
}).onConflictDoUpdate({
|
|
105650
|
+
target: [
|
|
105651
|
+
gameTimebackActivityCompletions.gameId,
|
|
105652
|
+
gameTimebackActivityCompletions.courseId,
|
|
105653
|
+
gameTimebackActivityCompletions.studentId,
|
|
105654
|
+
gameTimebackActivityCompletions.runId,
|
|
105655
|
+
gameTimebackActivityCompletions.activityId
|
|
105656
|
+
],
|
|
105657
|
+
set: { reservedAt: sql`now()` },
|
|
105658
|
+
setWhere: sql`${gameTimebackActivityCompletions.completedAt} IS NULL AND ${gameTimebackActivityCompletions.reservedAt} < now() - make_interval(secs => ${TimebackService2.COMPLETION_RESERVATION_STALE_SECONDS})`
|
|
105659
|
+
}).returning({
|
|
105660
|
+
id: gameTimebackActivityCompletions.id,
|
|
105661
|
+
reservedAt: gameTimebackActivityCompletions.reservedAt,
|
|
105662
|
+
takenOver: sql`${gameTimebackActivityCompletions.reservedAt} <> ${gameTimebackActivityCompletions.createdAt}`
|
|
105663
|
+
});
|
|
105664
|
+
if (reserved) {
|
|
105665
|
+
return {
|
|
105666
|
+
status: "reserved",
|
|
105667
|
+
token: { id: reserved.id, reservedAt: reserved.reservedAt },
|
|
105668
|
+
takenOver: reserved.takenOver
|
|
105669
|
+
};
|
|
105670
|
+
}
|
|
105671
|
+
const existing = await db2.query.gameTimebackActivityCompletions.findFirst({
|
|
105672
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, key.gameId), eq(gameTimebackActivityCompletions.courseId, key.courseId), eq(gameTimebackActivityCompletions.studentId, key.studentId), eq(gameTimebackActivityCompletions.runId, key.runId), eq(gameTimebackActivityCompletions.activityId, key.activityId)),
|
|
105673
|
+
columns: {
|
|
105674
|
+
completedAt: true,
|
|
105675
|
+
xpAwarded: true,
|
|
105676
|
+
masteredUnitsApplied: true,
|
|
105677
|
+
pctCompleteApp: true
|
|
105678
|
+
}
|
|
105679
|
+
});
|
|
105680
|
+
if (!existing?.completedAt) {
|
|
105681
|
+
return { status: "in_flight" };
|
|
105682
|
+
}
|
|
105683
|
+
return {
|
|
105684
|
+
status: "duplicate",
|
|
105685
|
+
award: existing.xpAwarded !== null ? {
|
|
105686
|
+
xpAwarded: existing.xpAwarded,
|
|
105687
|
+
masteredUnitsApplied: existing.masteredUnitsApplied ?? 0,
|
|
105688
|
+
pctCompleteApp: existing.pctCompleteApp
|
|
105689
|
+
} : null
|
|
105690
|
+
};
|
|
105691
|
+
}
|
|
105692
|
+
static async acquireCompletionReservation(db2, key) {
|
|
105693
|
+
let reserveOutcome = await this.reserveCompletion(db2, key);
|
|
105694
|
+
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
105695
|
+
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
105696
|
+
reserveOutcome = await this.reserveCompletion(db2, key);
|
|
105697
|
+
}
|
|
105698
|
+
if (reserveOutcome.status === "duplicate") {
|
|
105699
|
+
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
105700
|
+
return reserveOutcome;
|
|
105701
|
+
}
|
|
105702
|
+
if (reserveOutcome.status === "in_flight") {
|
|
105703
|
+
setAttribute("app.timeback.end_activity_status", "completion_reservation_in_flight");
|
|
105704
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
105705
|
+
}
|
|
105706
|
+
if (reserveOutcome.takenOver) {
|
|
105707
|
+
setAttribute("app.timeback.completion_reservation_takeover", true);
|
|
105708
|
+
addEvent("timeback.completion_reservation_takeover", {
|
|
105709
|
+
"app.timeback.run_id": key.runId,
|
|
105710
|
+
"app.timeback.activity_id": key.activityId
|
|
105711
|
+
});
|
|
105712
|
+
}
|
|
105713
|
+
return { status: "reserved", token: reserveOutcome.token };
|
|
105714
|
+
}
|
|
105715
|
+
static async runWithCompletionReservation(db2, key, emit, awardFromResult) {
|
|
105716
|
+
const acquired = await this.acquireCompletionReservation(db2, key);
|
|
105717
|
+
if (acquired.status === "duplicate") {
|
|
105718
|
+
return acquired;
|
|
105719
|
+
}
|
|
105720
|
+
const logContext = { run_id: key.runId, activity_id: key.activityId };
|
|
105721
|
+
let result;
|
|
105722
|
+
try {
|
|
105723
|
+
result = await emit();
|
|
105724
|
+
} catch (error88) {
|
|
105725
|
+
try {
|
|
105726
|
+
await this.rollbackCompletion(db2, acquired.token);
|
|
105727
|
+
} catch (rollbackError) {
|
|
105728
|
+
logTimebackError("rollback completion reservation", rollbackError, logContext);
|
|
105729
|
+
}
|
|
105730
|
+
throw error88;
|
|
105731
|
+
}
|
|
105732
|
+
try {
|
|
105733
|
+
await this.confirmCompletion(db2, acquired.token, awardFromResult(result));
|
|
105734
|
+
} catch (confirmError) {
|
|
105735
|
+
logTimebackError("confirm completion reservation", confirmError, logContext);
|
|
105736
|
+
}
|
|
105737
|
+
return { status: "emitted", result };
|
|
105738
|
+
}
|
|
105739
|
+
static async confirmCompletion(db2, token, award) {
|
|
105740
|
+
await db2.update(gameTimebackActivityCompletions).set({
|
|
105741
|
+
completedAt: sql`now()`,
|
|
105742
|
+
xpAwarded: award.xpAwarded,
|
|
105743
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
105744
|
+
pctCompleteApp: award.pctCompleteApp
|
|
105745
|
+
}).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
105746
|
+
}
|
|
105747
|
+
static async rollbackCompletion(db2, token) {
|
|
105748
|
+
await db2.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
105749
|
+
}
|
|
105571
105750
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
105572
105751
|
const base = extensions ?? {};
|
|
105573
105752
|
const existingPlaycademy = base.playcademy;
|
|
@@ -106469,16 +106648,25 @@ var init_timeback_service = __esm(async () => {
|
|
|
106469
106648
|
attributes: courseAttributes
|
|
106470
106649
|
} = TimebackService2.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
106471
106650
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
106472
|
-
const
|
|
106651
|
+
const dedupeKey = runId !== undefined ? {
|
|
106652
|
+
gameId,
|
|
106653
|
+
courseId: runtimeCourseId,
|
|
106654
|
+
studentId,
|
|
106655
|
+
runId,
|
|
106656
|
+
activityId: activityData.activityId
|
|
106657
|
+
} : undefined;
|
|
106658
|
+
setAttributes({
|
|
106659
|
+
...courseAttributes,
|
|
106660
|
+
"app.timeback.grade": activityData.grade,
|
|
106661
|
+
"app.timeback.subject": activityData.subject,
|
|
106662
|
+
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
106663
|
+
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
106664
|
+
"app.timeback.score_pct": scorePercentage,
|
|
106665
|
+
"app.timeback.xp_requested": xpEarned
|
|
106666
|
+
});
|
|
106667
|
+
const courseTotalXp = integration.totalXp;
|
|
106668
|
+
const activityEventContext = {
|
|
106473
106669
|
gameId,
|
|
106474
|
-
score: scorePercentage,
|
|
106475
|
-
totalQuestions: scoreData.totalQuestions,
|
|
106476
|
-
correctQuestions: scoreData.correctQuestions,
|
|
106477
|
-
durationSeconds: timingData.durationSeconds,
|
|
106478
|
-
xpEarned,
|
|
106479
|
-
masteredUnits,
|
|
106480
|
-
masteredUnitsAbsolute,
|
|
106481
|
-
extensions: extensionsWithResumeId,
|
|
106482
106670
|
activityId: activityData.activityId,
|
|
106483
106671
|
activityName: activityData.activityName,
|
|
106484
106672
|
subject: activityData.subject,
|
|
@@ -106487,45 +106675,92 @@ var init_timeback_service = __esm(async () => {
|
|
|
106487
106675
|
courseId: runtimeCourseId,
|
|
106488
106676
|
courseName: runtimeCourseName,
|
|
106489
106677
|
studentEmail: activityData.studentEmail,
|
|
106490
|
-
|
|
106678
|
+
extensions: extensionsWithResumeId,
|
|
106491
106679
|
...runId ? { runId } : {}
|
|
106492
|
-
}
|
|
106680
|
+
};
|
|
106681
|
+
const completionEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
106682
|
+
const sessionEndEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
106683
|
+
const completionHistoryEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
106684
|
+
function emitRecord() {
|
|
106685
|
+
return client.activity.record(runtimeCourseId, studentId, {
|
|
106686
|
+
...activityEventContext,
|
|
106687
|
+
eventId: completionEventId,
|
|
106688
|
+
completionHistoryEventId,
|
|
106689
|
+
score: scorePercentage,
|
|
106690
|
+
totalQuestions: scoreData.totalQuestions,
|
|
106691
|
+
correctQuestions: scoreData.correctQuestions,
|
|
106692
|
+
durationSeconds: timingData.durationSeconds,
|
|
106693
|
+
xpEarned,
|
|
106694
|
+
masteredUnits,
|
|
106695
|
+
masteredUnitsAbsolute,
|
|
106696
|
+
courseTotalXp
|
|
106697
|
+
});
|
|
106698
|
+
}
|
|
106699
|
+
let result;
|
|
106700
|
+
let blockedByDedupeGuard = false;
|
|
106701
|
+
let duplicateAward = null;
|
|
106702
|
+
if (dedupeKey) {
|
|
106703
|
+
const outcome = await TimebackService2.runWithCompletionReservation(db2, dedupeKey, emitRecord, (emitted) => ({
|
|
106704
|
+
xpAwarded: emitted.xpAwarded,
|
|
106705
|
+
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
106706
|
+
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
106707
|
+
}));
|
|
106708
|
+
if (outcome.status === "duplicate") {
|
|
106709
|
+
blockedByDedupeGuard = true;
|
|
106710
|
+
duplicateAward = outcome.award;
|
|
106711
|
+
} else {
|
|
106712
|
+
result = outcome.result;
|
|
106713
|
+
}
|
|
106714
|
+
} else {
|
|
106715
|
+
result = await emitRecord();
|
|
106716
|
+
}
|
|
106493
106717
|
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
106494
106718
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
106495
|
-
const
|
|
106496
|
-
|
|
106497
|
-
|
|
106498
|
-
|
|
106499
|
-
|
|
106500
|
-
|
|
106501
|
-
|
|
106502
|
-
|
|
106503
|
-
|
|
106504
|
-
|
|
106505
|
-
|
|
106506
|
-
|
|
106507
|
-
|
|
106508
|
-
|
|
106509
|
-
|
|
106510
|
-
|
|
106511
|
-
}
|
|
106719
|
+
const sessionEndAttempted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
106720
|
+
let sessionEndEmitted = false;
|
|
106721
|
+
if (sessionEndAttempted) {
|
|
106722
|
+
try {
|
|
106723
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
106724
|
+
...activityEventContext,
|
|
106725
|
+
eventId: sessionEndEventId,
|
|
106726
|
+
activeTimeSeconds: sessionEndActiveSeconds,
|
|
106727
|
+
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {}
|
|
106728
|
+
});
|
|
106729
|
+
sessionEndEmitted = true;
|
|
106730
|
+
} catch (sessionError) {
|
|
106731
|
+
logTimebackError("emit session end after completion", sessionError, {
|
|
106732
|
+
run_id: runId,
|
|
106733
|
+
activity_id: activityData.activityId
|
|
106734
|
+
});
|
|
106735
|
+
}
|
|
106512
106736
|
}
|
|
106513
106737
|
setAttributes({
|
|
106514
|
-
|
|
106515
|
-
"app.timeback.grade": activityData.grade,
|
|
106516
|
-
"app.timeback.subject": activityData.subject,
|
|
106517
|
-
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
106518
|
-
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
106519
|
-
"app.timeback.score_pct": scorePercentage,
|
|
106738
|
+
"app.timeback.session_end_attempted": sessionEndAttempted,
|
|
106520
106739
|
"app.timeback.session_end_emitted": sessionEndEmitted,
|
|
106521
106740
|
"app.timeback.active_time_seconds": sessionEndActiveSeconds,
|
|
106522
106741
|
"app.timeback.inactive_time_seconds": sessionEndInactiveSeconds ?? 0,
|
|
106523
|
-
"app.timeback.
|
|
106524
|
-
"app.timeback.xp_awarded": result.xpAwarded,
|
|
106742
|
+
"app.timeback.xp_awarded": result?.xpAwarded ?? 0,
|
|
106525
106743
|
"app.timeback.mastered_units_requested": masteredUnits,
|
|
106526
106744
|
"app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
|
|
106527
|
-
"app.timeback.mastered_units_applied": result
|
|
106745
|
+
"app.timeback.mastered_units_applied": result?.masteredUnitsApplied ?? 0,
|
|
106746
|
+
...blockedByDedupeGuard ? {
|
|
106747
|
+
"app.timeback.xp_already_awarded": duplicateAward?.xpAwarded ?? 0,
|
|
106748
|
+
"app.timeback.mastered_units_already_applied": duplicateAward?.masteredUnitsApplied ?? 0
|
|
106749
|
+
} : {}
|
|
106528
106750
|
});
|
|
106751
|
+
if (blockedByDedupeGuard) {
|
|
106752
|
+
return {
|
|
106753
|
+
status: "ok",
|
|
106754
|
+
courseId: runtimeCourseId,
|
|
106755
|
+
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
106756
|
+
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
106757
|
+
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
106758
|
+
blockedByDedupeGuard: true
|
|
106759
|
+
};
|
|
106760
|
+
}
|
|
106761
|
+
if (!result) {
|
|
106762
|
+
throw new InternalError("Completion emit did not produce a result");
|
|
106763
|
+
}
|
|
106529
106764
|
return {
|
|
106530
106765
|
status: "ok",
|
|
106531
106766
|
courseId: runtimeCourseId,
|
|
@@ -165560,17 +165795,75 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
165560
165795
|
init_sqliteSchema();
|
|
165561
165796
|
init_sqliteSerializer();
|
|
165562
165797
|
});
|
|
165563
|
-
|
|
165798
|
+
function isDuplicateObjectError(error89) {
|
|
165799
|
+
const code = error89.code;
|
|
165800
|
+
if (code && DUPLICATE_OBJECT_CODES.has(code)) {
|
|
165801
|
+
return true;
|
|
165802
|
+
}
|
|
165803
|
+
return /already exists/i.test(errorMessage(error89));
|
|
165804
|
+
}
|
|
165805
|
+
async function reconcileMissingColumns(db2, snapshot) {
|
|
165806
|
+
const tables = snapshot.tables ?? {};
|
|
165807
|
+
for (const table9 of Object.values(tables)) {
|
|
165808
|
+
for (const column6 of Object.values(table9.columns ?? {})) {
|
|
165809
|
+
const clauses = [
|
|
165810
|
+
`ALTER TABLE "${table9.name}" ADD COLUMN IF NOT EXISTS "${column6.name}" ${column6.type}`
|
|
165811
|
+
];
|
|
165812
|
+
if (column6.default !== undefined) {
|
|
165813
|
+
clauses.push(`DEFAULT ${column6.default}`);
|
|
165814
|
+
}
|
|
165815
|
+
if (column6.notNull && !column6.primaryKey) {
|
|
165816
|
+
clauses.push("NOT NULL");
|
|
165817
|
+
}
|
|
165818
|
+
try {
|
|
165819
|
+
await db2.execute(clauses.join(" "));
|
|
165820
|
+
} catch (error89) {
|
|
165821
|
+
console.warn(`[Sandbox] Could not add column ${table9.name}.${column6.name} to the existing database file (${errorMessage(error89)}). Delete the file to rebuild it with the current schema.`);
|
|
165822
|
+
}
|
|
165823
|
+
}
|
|
165824
|
+
}
|
|
165825
|
+
}
|
|
165826
|
+
async function ensureSchemaMetaTable(client) {
|
|
165827
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${SCHEMA_META_TABLE} (
|
|
165828
|
+
id integer PRIMARY KEY CHECK (id = 1),
|
|
165829
|
+
snapshot jsonb NOT NULL,
|
|
165830
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
165831
|
+
)`);
|
|
165832
|
+
}
|
|
165833
|
+
async function readStoredSnapshot(client) {
|
|
165834
|
+
const result = await client.query(`SELECT snapshot FROM ${SCHEMA_META_TABLE} WHERE id = 1`);
|
|
165835
|
+
return result.rows[0]?.snapshot ?? null;
|
|
165836
|
+
}
|
|
165837
|
+
async function writeStoredSnapshot(client, snapshot) {
|
|
165838
|
+
await client.query(`INSERT INTO ${SCHEMA_META_TABLE} (id, snapshot, updated_at)
|
|
165839
|
+
VALUES (1, $1, now())
|
|
165840
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $1, updated_at = now()`, [JSON.stringify(snapshot)]);
|
|
165841
|
+
}
|
|
165842
|
+
async function syncDatabaseSchema(db2, client, isExistingDb) {
|
|
165564
165843
|
try {
|
|
165565
165844
|
const { generateDrizzleJson: generateDrizzleJson2, generateMigration: generateMigration2 } = await Promise.resolve().then(() => (init_api4(), exports_api));
|
|
165566
|
-
|
|
165845
|
+
await ensureSchemaMetaTable(client);
|
|
165846
|
+
const stored = isExistingDb ? await readStoredSnapshot(client) : null;
|
|
165847
|
+
const emptyJson = generateDrizzleJson2({});
|
|
165848
|
+
const prevJson = stored ?? emptyJson;
|
|
165567
165849
|
const curJson = generateDrizzleJson2(exports_tables_index, prevJson.id, undefined, "snake_case");
|
|
165568
165850
|
const statements = await generateMigration2(prevJson, curJson);
|
|
165851
|
+
const tolerateExisting = isExistingDb && stored === null;
|
|
165569
165852
|
for (const statement of statements) {
|
|
165570
|
-
|
|
165853
|
+
try {
|
|
165854
|
+
await db2.execute(statement);
|
|
165855
|
+
} catch (error89) {
|
|
165856
|
+
if (!(tolerateExisting && isDuplicateObjectError(error89))) {
|
|
165857
|
+
throw error89;
|
|
165858
|
+
}
|
|
165859
|
+
}
|
|
165860
|
+
}
|
|
165861
|
+
if (tolerateExisting) {
|
|
165862
|
+
await reconcileMissingColumns(db2, curJson);
|
|
165571
165863
|
}
|
|
165864
|
+
await writeStoredSnapshot(client, curJson);
|
|
165572
165865
|
} catch (error89) {
|
|
165573
|
-
console.error("[Sandbox] Schema
|
|
165866
|
+
console.error("[Sandbox] Schema sync failed:", error89);
|
|
165574
165867
|
throw error89;
|
|
165575
165868
|
}
|
|
165576
165869
|
}
|
|
@@ -165581,16 +165874,17 @@ async function setupDatabase(customPath) {
|
|
|
165581
165874
|
const client = dbPath === ":memory:" ? new We2 : new We2(dbPath);
|
|
165582
165875
|
await client._checkReady();
|
|
165583
165876
|
const db2 = drizzle(client, { schema: exports_tables_index });
|
|
165584
|
-
|
|
165585
|
-
await createDatabaseSchema(db2);
|
|
165586
|
-
}
|
|
165877
|
+
await syncDatabaseSchema(db2, client, dbFileExists);
|
|
165587
165878
|
return db2;
|
|
165588
165879
|
}
|
|
165880
|
+
var SCHEMA_META_TABLE = "_sandbox_schema_meta";
|
|
165881
|
+
var DUPLICATE_OBJECT_CODES;
|
|
165589
165882
|
var init_database = __esm(() => {
|
|
165590
165883
|
init_dist8();
|
|
165591
165884
|
init_pglite();
|
|
165592
165885
|
init_tables_index();
|
|
165593
165886
|
init_path_manager();
|
|
165887
|
+
DUPLICATE_OBJECT_CODES = new Set(["42701", "42710", "42723", "42P06", "42P07"]);
|
|
165594
165888
|
});
|
|
165595
165889
|
function setLogger(logger3) {
|
|
165596
165890
|
customLogger = logger3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/vite-plugin",
|
|
3
|
-
"version": "1.1.3-beta.
|
|
3
|
+
"version": "1.1.3-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"archiver": "^7.0.1",
|
|
21
21
|
"picocolors": "^1.1.1",
|
|
22
|
-
"playcademy": "0.27.1-beta.
|
|
22
|
+
"playcademy": "0.27.1-beta.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@electric-sql/pglite": "^0.3.16",
|
|
26
26
|
"@inquirer/prompts": "^7.8.6",
|
|
27
27
|
"@playcademy/constants": "0.0.1",
|
|
28
|
-
"@playcademy/sandbox": "0.6.
|
|
29
|
-
"@playcademy/sdk": "0.15.
|
|
28
|
+
"@playcademy/sandbox": "0.6.1-beta.4",
|
|
29
|
+
"@playcademy/sdk": "0.15.1-beta.2",
|
|
30
30
|
"@playcademy/types": "0.0.1",
|
|
31
31
|
"@playcademy/utils": "0.0.1",
|
|
32
32
|
"@types/archiver": "^6.0.3",
|