@playcademy/vite-plugin 1.1.2 → 1.1.3-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.js +335 -48
- 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.2",
|
|
23749
|
+
version: "1.1.3-beta.2",
|
|
23750
23750
|
type: "module",
|
|
23751
23751
|
exports: {
|
|
23752
23752
|
".": {
|
|
@@ -24245,6 +24245,7 @@ import { Buffer as Buffer2 } from "node:buffer";
|
|
|
24245
24245
|
import { gzipSync } from "node:zlib";
|
|
24246
24246
|
import { stdout } from "process";
|
|
24247
24247
|
import { createHash } from "node:crypto";
|
|
24248
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
24248
24249
|
import crypto3 from "node:crypto";
|
|
24249
24250
|
import * as s3 from "fs";
|
|
24250
24251
|
import * as o3 from "path";
|
|
@@ -25345,7 +25346,7 @@ var package_default2;
|
|
|
25345
25346
|
var init_package = __esm(() => {
|
|
25346
25347
|
package_default2 = {
|
|
25347
25348
|
name: "@playcademy/sandbox",
|
|
25348
|
-
version: "0.6.
|
|
25349
|
+
version: "0.6.1-beta.3",
|
|
25349
25350
|
description: "Local development server for Playcademy game development",
|
|
25350
25351
|
type: "module",
|
|
25351
25352
|
exports: {
|
|
@@ -35620,6 +35621,7 @@ var gameTimebackIntegrationStatusEnum;
|
|
|
35620
35621
|
var gameTimebackIntegrations;
|
|
35621
35622
|
var gameTimebackAssessmentTests;
|
|
35622
35623
|
var gameTimebackMetricDiscrepancyVerifications;
|
|
35624
|
+
var gameTimebackActivityCompletions;
|
|
35623
35625
|
var init_table7 = __esm(() => {
|
|
35624
35626
|
init_drizzle_orm();
|
|
35625
35627
|
init_pg_core();
|
|
@@ -35671,6 +35673,22 @@ var init_table7 = __esm(() => {
|
|
|
35671
35673
|
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
35672
35674
|
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
35673
35675
|
]);
|
|
35676
|
+
gameTimebackActivityCompletions = pgTable("game_timeback_activity_completions", {
|
|
35677
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
35678
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
35679
|
+
courseId: text("course_id").notNull(),
|
|
35680
|
+
studentId: text("student_id").notNull(),
|
|
35681
|
+
runId: uuid("run_id").notNull(),
|
|
35682
|
+
activityId: text("activity_id").notNull(),
|
|
35683
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
35684
|
+
reservedAt: timestamp("reserved_at", { withTimezone: true, mode: "string" }).notNull().defaultNow(),
|
|
35685
|
+
xpAwarded: doublePrecision("xp_awarded"),
|
|
35686
|
+
masteredUnitsApplied: integer("mastered_units_applied"),
|
|
35687
|
+
pctCompleteApp: doublePrecision("pct_complete_app"),
|
|
35688
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
35689
|
+
}, (table3) => [
|
|
35690
|
+
uniqueIndex("game_timeback_activity_completions_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId, table3.activityId)
|
|
35691
|
+
]);
|
|
35674
35692
|
});
|
|
35675
35693
|
var exports_tables_index = {};
|
|
35676
35694
|
__export(exports_tables_index, {
|
|
@@ -35686,6 +35704,7 @@ __export(exports_tables_index, {
|
|
|
35686
35704
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
35687
35705
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
35688
35706
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
35707
|
+
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
35689
35708
|
gameScoresRelations: () => gameScoresRelations,
|
|
35690
35709
|
gameScores: () => gameScores,
|
|
35691
35710
|
gamePlatformEnum: () => gamePlatformEnum,
|
|
@@ -48688,6 +48707,7 @@ var init_constants2 = __esm(() => {
|
|
|
48688
48707
|
GAME_WORKER_DOMAIN_PRODUCTION = GAME_WORKER_DOMAINS.production;
|
|
48689
48708
|
GAME_WORKER_DOMAIN_STAGING = GAME_WORKER_DOMAINS.staging;
|
|
48690
48709
|
});
|
|
48710
|
+
var init_assets_config = () => {};
|
|
48691
48711
|
var init_bindings = __esm(() => {
|
|
48692
48712
|
init_constants2();
|
|
48693
48713
|
});
|
|
@@ -48699,6 +48719,7 @@ var init_provider = __esm(() => {
|
|
|
48699
48719
|
init_src();
|
|
48700
48720
|
init_core();
|
|
48701
48721
|
init_constants2();
|
|
48722
|
+
init_assets_config();
|
|
48702
48723
|
init_bindings();
|
|
48703
48724
|
init_helpers();
|
|
48704
48725
|
});
|
|
@@ -51121,11 +51142,18 @@ var METRICS_BASE;
|
|
|
51121
51142
|
var init_tunnel = __esm(() => {
|
|
51122
51143
|
METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
|
|
51123
51144
|
});
|
|
51145
|
+
function isPreviewStage(stage) {
|
|
51146
|
+
return PREVIEW_STAGE_PATTERN.test(stage);
|
|
51147
|
+
}
|
|
51148
|
+
var PREVIEW_STAGE_PATTERN;
|
|
51149
|
+
var init_stages = __esm(() => {
|
|
51150
|
+
PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
|
|
51151
|
+
});
|
|
51124
51152
|
function getDeploymentId(gameSlug, sstStage) {
|
|
51125
51153
|
if (sstStage === "production") {
|
|
51126
51154
|
return gameSlug;
|
|
51127
51155
|
}
|
|
51128
|
-
if (sstStage === "dev" || sstStage
|
|
51156
|
+
if (sstStage === "dev" || isPreviewStage(sstStage)) {
|
|
51129
51157
|
return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
|
|
51130
51158
|
}
|
|
51131
51159
|
return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
|
|
@@ -51145,6 +51173,7 @@ async function generateDeploymentHash(code) {
|
|
|
51145
51173
|
}
|
|
51146
51174
|
var init_deployment_util = __esm(() => {
|
|
51147
51175
|
init_src();
|
|
51176
|
+
init_stages();
|
|
51148
51177
|
});
|
|
51149
51178
|
|
|
51150
51179
|
class DeployService {
|
|
@@ -100025,7 +100054,8 @@ class ActivityRecord {
|
|
|
100025
100054
|
courseName,
|
|
100026
100055
|
subject: progressData.subject,
|
|
100027
100056
|
appName: progressData.appName,
|
|
100028
|
-
sensorUrl: progressData.sensorUrl
|
|
100057
|
+
sensorUrl: progressData.sensorUrl,
|
|
100058
|
+
eventId: progressData.completionHistoryEventId
|
|
100029
100059
|
});
|
|
100030
100060
|
}
|
|
100031
100061
|
if (masteryProgress?.masteryRevoked) {
|
|
@@ -100050,6 +100080,7 @@ class ActivityRecord {
|
|
|
100050
100080
|
subject: progressData.subject,
|
|
100051
100081
|
appName: progressData.appName,
|
|
100052
100082
|
sensorUrl: progressData.sensorUrl,
|
|
100083
|
+
eventId: progressData.eventId,
|
|
100053
100084
|
extensions: extensions || progressData.extensions,
|
|
100054
100085
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
100055
100086
|
}).catch((error88) => {
|
|
@@ -100073,6 +100104,7 @@ class ActivityRecord {
|
|
|
100073
100104
|
}
|
|
100074
100105
|
async emitCourseCompletionHistoryEvent(data) {
|
|
100075
100106
|
await this.events.emitActivityEvent({
|
|
100107
|
+
eventId: data.eventId,
|
|
100076
100108
|
studentId: data.studentId,
|
|
100077
100109
|
studentEmail: data.studentEmail,
|
|
100078
100110
|
gameId: data.gameId,
|
|
@@ -100188,6 +100220,7 @@ class ActivitySession {
|
|
|
100188
100220
|
subject: sessionData.subject,
|
|
100189
100221
|
appName: sessionData.appName,
|
|
100190
100222
|
sensorUrl: sessionData.sensorUrl,
|
|
100223
|
+
eventId: sessionData.eventId,
|
|
100191
100224
|
...runId ? { runId } : {},
|
|
100192
100225
|
...extensions ? { extensions } : {}
|
|
100193
100226
|
});
|
|
@@ -100454,6 +100487,9 @@ class ActivityEvents {
|
|
|
100454
100487
|
} : {},
|
|
100455
100488
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100456
100489
|
});
|
|
100490
|
+
if (data.eventId) {
|
|
100491
|
+
event.id = data.eventId;
|
|
100492
|
+
}
|
|
100457
100493
|
await this.core.api.caliper.events.send(data.sensorUrl, [event]);
|
|
100458
100494
|
}
|
|
100459
100495
|
async emitTimeSpentEvent(data) {
|
|
@@ -100485,6 +100521,9 @@ class ActivityEvents {
|
|
|
100485
100521
|
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
100486
100522
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100487
100523
|
});
|
|
100524
|
+
if (data.eventId) {
|
|
100525
|
+
event.id = data.eventId;
|
|
100526
|
+
}
|
|
100488
100527
|
const wireEvent = data.extensions ? { ...event, generated: { ...event.generated, extensions: data.extensions } } : event;
|
|
100489
100528
|
await this.core.api.caliper.events.send(data.sensorUrl, [wireEvent]);
|
|
100490
100529
|
}
|
|
@@ -105542,6 +105581,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
105542
105581
|
static HEARTBEAT_DEDUPE_TTL_MS = 300000;
|
|
105543
105582
|
static processedHeartbeatWindows = new Map;
|
|
105544
105583
|
static inFlightHeartbeatWindows = new Map;
|
|
105584
|
+
static COMPLETION_RESERVATION_STALE_SECONDS = 300;
|
|
105585
|
+
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
105586
|
+
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
105545
105587
|
deps;
|
|
105546
105588
|
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
105547
105589
|
for (const [key, timestamp6] of this.processedHeartbeatWindows) {
|
|
@@ -105566,6 +105608,136 @@ var init_timeback_service = __esm(async () => {
|
|
|
105566
105608
|
static clearInFlightHeartbeatWindow(key) {
|
|
105567
105609
|
this.inFlightHeartbeatWindows.delete(key);
|
|
105568
105610
|
}
|
|
105611
|
+
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
105612
|
+
const hash2 = createHash2("sha256").update([
|
|
105613
|
+
"playcademy",
|
|
105614
|
+
"timeback",
|
|
105615
|
+
kind,
|
|
105616
|
+
key.gameId,
|
|
105617
|
+
key.courseId,
|
|
105618
|
+
key.studentId,
|
|
105619
|
+
key.runId,
|
|
105620
|
+
key.activityId,
|
|
105621
|
+
discriminator
|
|
105622
|
+
].join("\x00")).digest("hex");
|
|
105623
|
+
const variant = (Number.parseInt(hash2.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
105624
|
+
const uuid9 = [
|
|
105625
|
+
hash2.slice(0, 8),
|
|
105626
|
+
hash2.slice(8, 12),
|
|
105627
|
+
`5${hash2.slice(13, 16)}`,
|
|
105628
|
+
`${variant}${hash2.slice(18, 20)}`,
|
|
105629
|
+
hash2.slice(20, 32)
|
|
105630
|
+
].join("-");
|
|
105631
|
+
return `urn:uuid:${uuid9}`;
|
|
105632
|
+
}
|
|
105633
|
+
static async reserveCompletion(db2, key) {
|
|
105634
|
+
const [reserved] = await db2.insert(gameTimebackActivityCompletions).values({
|
|
105635
|
+
gameId: key.gameId,
|
|
105636
|
+
courseId: key.courseId,
|
|
105637
|
+
studentId: key.studentId,
|
|
105638
|
+
runId: key.runId,
|
|
105639
|
+
activityId: key.activityId
|
|
105640
|
+
}).onConflictDoUpdate({
|
|
105641
|
+
target: [
|
|
105642
|
+
gameTimebackActivityCompletions.gameId,
|
|
105643
|
+
gameTimebackActivityCompletions.courseId,
|
|
105644
|
+
gameTimebackActivityCompletions.studentId,
|
|
105645
|
+
gameTimebackActivityCompletions.runId,
|
|
105646
|
+
gameTimebackActivityCompletions.activityId
|
|
105647
|
+
],
|
|
105648
|
+
set: { reservedAt: sql`now()` },
|
|
105649
|
+
setWhere: sql`${gameTimebackActivityCompletions.completedAt} IS NULL AND ${gameTimebackActivityCompletions.reservedAt} < now() - make_interval(secs => ${TimebackService2.COMPLETION_RESERVATION_STALE_SECONDS})`
|
|
105650
|
+
}).returning({
|
|
105651
|
+
id: gameTimebackActivityCompletions.id,
|
|
105652
|
+
reservedAt: gameTimebackActivityCompletions.reservedAt,
|
|
105653
|
+
takenOver: sql`${gameTimebackActivityCompletions.reservedAt} <> ${gameTimebackActivityCompletions.createdAt}`
|
|
105654
|
+
});
|
|
105655
|
+
if (reserved) {
|
|
105656
|
+
return {
|
|
105657
|
+
status: "reserved",
|
|
105658
|
+
token: { id: reserved.id, reservedAt: reserved.reservedAt },
|
|
105659
|
+
takenOver: reserved.takenOver
|
|
105660
|
+
};
|
|
105661
|
+
}
|
|
105662
|
+
const existing = await db2.query.gameTimebackActivityCompletions.findFirst({
|
|
105663
|
+
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)),
|
|
105664
|
+
columns: {
|
|
105665
|
+
completedAt: true,
|
|
105666
|
+
xpAwarded: true,
|
|
105667
|
+
masteredUnitsApplied: true,
|
|
105668
|
+
pctCompleteApp: true
|
|
105669
|
+
}
|
|
105670
|
+
});
|
|
105671
|
+
if (!existing?.completedAt) {
|
|
105672
|
+
return { status: "in_flight" };
|
|
105673
|
+
}
|
|
105674
|
+
return {
|
|
105675
|
+
status: "duplicate",
|
|
105676
|
+
award: existing.xpAwarded !== null ? {
|
|
105677
|
+
xpAwarded: existing.xpAwarded,
|
|
105678
|
+
masteredUnitsApplied: existing.masteredUnitsApplied ?? 0,
|
|
105679
|
+
pctCompleteApp: existing.pctCompleteApp
|
|
105680
|
+
} : null
|
|
105681
|
+
};
|
|
105682
|
+
}
|
|
105683
|
+
static async acquireCompletionReservation(db2, key) {
|
|
105684
|
+
let reserveOutcome = await this.reserveCompletion(db2, key);
|
|
105685
|
+
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
105686
|
+
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
105687
|
+
reserveOutcome = await this.reserveCompletion(db2, key);
|
|
105688
|
+
}
|
|
105689
|
+
if (reserveOutcome.status === "duplicate") {
|
|
105690
|
+
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
105691
|
+
return reserveOutcome;
|
|
105692
|
+
}
|
|
105693
|
+
if (reserveOutcome.status === "in_flight") {
|
|
105694
|
+
setAttribute("app.timeback.end_activity_status", "completion_reservation_in_flight");
|
|
105695
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
105696
|
+
}
|
|
105697
|
+
if (reserveOutcome.takenOver) {
|
|
105698
|
+
setAttribute("app.timeback.completion_reservation_takeover", true);
|
|
105699
|
+
addEvent("timeback.completion_reservation_takeover", {
|
|
105700
|
+
"app.timeback.run_id": key.runId,
|
|
105701
|
+
"app.timeback.activity_id": key.activityId
|
|
105702
|
+
});
|
|
105703
|
+
}
|
|
105704
|
+
return { status: "reserved", token: reserveOutcome.token };
|
|
105705
|
+
}
|
|
105706
|
+
static async runWithCompletionReservation(db2, key, emit, awardFromResult) {
|
|
105707
|
+
const acquired = await this.acquireCompletionReservation(db2, key);
|
|
105708
|
+
if (acquired.status === "duplicate") {
|
|
105709
|
+
return acquired;
|
|
105710
|
+
}
|
|
105711
|
+
const logContext = { run_id: key.runId, activity_id: key.activityId };
|
|
105712
|
+
let result;
|
|
105713
|
+
try {
|
|
105714
|
+
result = await emit();
|
|
105715
|
+
} catch (error88) {
|
|
105716
|
+
try {
|
|
105717
|
+
await this.rollbackCompletion(db2, acquired.token);
|
|
105718
|
+
} catch (rollbackError) {
|
|
105719
|
+
logTimebackError("rollback completion reservation", rollbackError, logContext);
|
|
105720
|
+
}
|
|
105721
|
+
throw error88;
|
|
105722
|
+
}
|
|
105723
|
+
try {
|
|
105724
|
+
await this.confirmCompletion(db2, acquired.token, awardFromResult(result));
|
|
105725
|
+
} catch (confirmError) {
|
|
105726
|
+
logTimebackError("confirm completion reservation", confirmError, logContext);
|
|
105727
|
+
}
|
|
105728
|
+
return { status: "emitted", result };
|
|
105729
|
+
}
|
|
105730
|
+
static async confirmCompletion(db2, token, award) {
|
|
105731
|
+
await db2.update(gameTimebackActivityCompletions).set({
|
|
105732
|
+
completedAt: sql`now()`,
|
|
105733
|
+
xpAwarded: award.xpAwarded,
|
|
105734
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
105735
|
+
pctCompleteApp: award.pctCompleteApp
|
|
105736
|
+
}).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
105737
|
+
}
|
|
105738
|
+
static async rollbackCompletion(db2, token) {
|
|
105739
|
+
await db2.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
105740
|
+
}
|
|
105569
105741
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
105570
105742
|
const base = extensions ?? {};
|
|
105571
105743
|
const existingPlaycademy = base.playcademy;
|
|
@@ -106467,16 +106639,25 @@ var init_timeback_service = __esm(async () => {
|
|
|
106467
106639
|
attributes: courseAttributes
|
|
106468
106640
|
} = TimebackService2.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
106469
106641
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
106470
|
-
const
|
|
106642
|
+
const dedupeKey = runId !== undefined ? {
|
|
106643
|
+
gameId,
|
|
106644
|
+
courseId: runtimeCourseId,
|
|
106645
|
+
studentId,
|
|
106646
|
+
runId,
|
|
106647
|
+
activityId: activityData.activityId
|
|
106648
|
+
} : undefined;
|
|
106649
|
+
setAttributes({
|
|
106650
|
+
...courseAttributes,
|
|
106651
|
+
"app.timeback.grade": activityData.grade,
|
|
106652
|
+
"app.timeback.subject": activityData.subject,
|
|
106653
|
+
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
106654
|
+
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
106655
|
+
"app.timeback.score_pct": scorePercentage,
|
|
106656
|
+
"app.timeback.xp_requested": xpEarned
|
|
106657
|
+
});
|
|
106658
|
+
const courseTotalXp = integration.totalXp;
|
|
106659
|
+
const activityEventContext = {
|
|
106471
106660
|
gameId,
|
|
106472
|
-
score: scorePercentage,
|
|
106473
|
-
totalQuestions: scoreData.totalQuestions,
|
|
106474
|
-
correctQuestions: scoreData.correctQuestions,
|
|
106475
|
-
durationSeconds: timingData.durationSeconds,
|
|
106476
|
-
xpEarned,
|
|
106477
|
-
masteredUnits,
|
|
106478
|
-
masteredUnitsAbsolute,
|
|
106479
|
-
extensions: extensionsWithResumeId,
|
|
106480
106661
|
activityId: activityData.activityId,
|
|
106481
106662
|
activityName: activityData.activityName,
|
|
106482
106663
|
subject: activityData.subject,
|
|
@@ -106485,45 +106666,92 @@ var init_timeback_service = __esm(async () => {
|
|
|
106485
106666
|
courseId: runtimeCourseId,
|
|
106486
106667
|
courseName: runtimeCourseName,
|
|
106487
106668
|
studentEmail: activityData.studentEmail,
|
|
106488
|
-
|
|
106669
|
+
extensions: extensionsWithResumeId,
|
|
106489
106670
|
...runId ? { runId } : {}
|
|
106490
|
-
}
|
|
106671
|
+
};
|
|
106672
|
+
const completionEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
106673
|
+
const sessionEndEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
106674
|
+
const completionHistoryEventId = dedupeKey ? TimebackService2.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
106675
|
+
function emitRecord() {
|
|
106676
|
+
return client.activity.record(runtimeCourseId, studentId, {
|
|
106677
|
+
...activityEventContext,
|
|
106678
|
+
eventId: completionEventId,
|
|
106679
|
+
completionHistoryEventId,
|
|
106680
|
+
score: scorePercentage,
|
|
106681
|
+
totalQuestions: scoreData.totalQuestions,
|
|
106682
|
+
correctQuestions: scoreData.correctQuestions,
|
|
106683
|
+
durationSeconds: timingData.durationSeconds,
|
|
106684
|
+
xpEarned,
|
|
106685
|
+
masteredUnits,
|
|
106686
|
+
masteredUnitsAbsolute,
|
|
106687
|
+
courseTotalXp
|
|
106688
|
+
});
|
|
106689
|
+
}
|
|
106690
|
+
let result;
|
|
106691
|
+
let blockedByDedupeGuard = false;
|
|
106692
|
+
let duplicateAward = null;
|
|
106693
|
+
if (dedupeKey) {
|
|
106694
|
+
const outcome = await TimebackService2.runWithCompletionReservation(db2, dedupeKey, emitRecord, (emitted) => ({
|
|
106695
|
+
xpAwarded: emitted.xpAwarded,
|
|
106696
|
+
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
106697
|
+
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
106698
|
+
}));
|
|
106699
|
+
if (outcome.status === "duplicate") {
|
|
106700
|
+
blockedByDedupeGuard = true;
|
|
106701
|
+
duplicateAward = outcome.award;
|
|
106702
|
+
} else {
|
|
106703
|
+
result = outcome.result;
|
|
106704
|
+
}
|
|
106705
|
+
} else {
|
|
106706
|
+
result = await emitRecord();
|
|
106707
|
+
}
|
|
106491
106708
|
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
106492
106709
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
106493
|
-
const
|
|
106494
|
-
|
|
106495
|
-
|
|
106496
|
-
|
|
106497
|
-
|
|
106498
|
-
|
|
106499
|
-
|
|
106500
|
-
|
|
106501
|
-
|
|
106502
|
-
|
|
106503
|
-
|
|
106504
|
-
|
|
106505
|
-
|
|
106506
|
-
|
|
106507
|
-
|
|
106508
|
-
|
|
106509
|
-
}
|
|
106710
|
+
const sessionEndAttempted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
106711
|
+
let sessionEndEmitted = false;
|
|
106712
|
+
if (sessionEndAttempted) {
|
|
106713
|
+
try {
|
|
106714
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
106715
|
+
...activityEventContext,
|
|
106716
|
+
eventId: sessionEndEventId,
|
|
106717
|
+
activeTimeSeconds: sessionEndActiveSeconds,
|
|
106718
|
+
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {}
|
|
106719
|
+
});
|
|
106720
|
+
sessionEndEmitted = true;
|
|
106721
|
+
} catch (sessionError) {
|
|
106722
|
+
logTimebackError("emit session end after completion", sessionError, {
|
|
106723
|
+
run_id: runId,
|
|
106724
|
+
activity_id: activityData.activityId
|
|
106725
|
+
});
|
|
106726
|
+
}
|
|
106510
106727
|
}
|
|
106511
106728
|
setAttributes({
|
|
106512
|
-
|
|
106513
|
-
"app.timeback.grade": activityData.grade,
|
|
106514
|
-
"app.timeback.subject": activityData.subject,
|
|
106515
|
-
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
106516
|
-
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
106517
|
-
"app.timeback.score_pct": scorePercentage,
|
|
106729
|
+
"app.timeback.session_end_attempted": sessionEndAttempted,
|
|
106518
106730
|
"app.timeback.session_end_emitted": sessionEndEmitted,
|
|
106519
106731
|
"app.timeback.active_time_seconds": sessionEndActiveSeconds,
|
|
106520
106732
|
"app.timeback.inactive_time_seconds": sessionEndInactiveSeconds ?? 0,
|
|
106521
|
-
"app.timeback.
|
|
106522
|
-
"app.timeback.xp_awarded": result.xpAwarded,
|
|
106733
|
+
"app.timeback.xp_awarded": result?.xpAwarded ?? 0,
|
|
106523
106734
|
"app.timeback.mastered_units_requested": masteredUnits,
|
|
106524
106735
|
"app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
|
|
106525
|
-
"app.timeback.mastered_units_applied": result
|
|
106736
|
+
"app.timeback.mastered_units_applied": result?.masteredUnitsApplied ?? 0,
|
|
106737
|
+
...blockedByDedupeGuard ? {
|
|
106738
|
+
"app.timeback.xp_already_awarded": duplicateAward?.xpAwarded ?? 0,
|
|
106739
|
+
"app.timeback.mastered_units_already_applied": duplicateAward?.masteredUnitsApplied ?? 0
|
|
106740
|
+
} : {}
|
|
106526
106741
|
});
|
|
106742
|
+
if (blockedByDedupeGuard) {
|
|
106743
|
+
return {
|
|
106744
|
+
status: "ok",
|
|
106745
|
+
courseId: runtimeCourseId,
|
|
106746
|
+
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
106747
|
+
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
106748
|
+
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
106749
|
+
blockedByDedupeGuard: true
|
|
106750
|
+
};
|
|
106751
|
+
}
|
|
106752
|
+
if (!result) {
|
|
106753
|
+
throw new InternalError("Completion emit did not produce a result");
|
|
106754
|
+
}
|
|
106527
106755
|
return {
|
|
106528
106756
|
status: "ok",
|
|
106529
106757
|
courseId: runtimeCourseId,
|
|
@@ -165558,17 +165786,75 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
165558
165786
|
init_sqliteSchema();
|
|
165559
165787
|
init_sqliteSerializer();
|
|
165560
165788
|
});
|
|
165561
|
-
|
|
165789
|
+
function isDuplicateObjectError(error89) {
|
|
165790
|
+
const code = error89.code;
|
|
165791
|
+
if (code && DUPLICATE_OBJECT_CODES.has(code)) {
|
|
165792
|
+
return true;
|
|
165793
|
+
}
|
|
165794
|
+
return /already exists/i.test(errorMessage(error89));
|
|
165795
|
+
}
|
|
165796
|
+
async function reconcileMissingColumns(db2, snapshot) {
|
|
165797
|
+
const tables = snapshot.tables ?? {};
|
|
165798
|
+
for (const table9 of Object.values(tables)) {
|
|
165799
|
+
for (const column6 of Object.values(table9.columns ?? {})) {
|
|
165800
|
+
const clauses = [
|
|
165801
|
+
`ALTER TABLE "${table9.name}" ADD COLUMN IF NOT EXISTS "${column6.name}" ${column6.type}`
|
|
165802
|
+
];
|
|
165803
|
+
if (column6.default !== undefined) {
|
|
165804
|
+
clauses.push(`DEFAULT ${column6.default}`);
|
|
165805
|
+
}
|
|
165806
|
+
if (column6.notNull && !column6.primaryKey) {
|
|
165807
|
+
clauses.push("NOT NULL");
|
|
165808
|
+
}
|
|
165809
|
+
try {
|
|
165810
|
+
await db2.execute(clauses.join(" "));
|
|
165811
|
+
} catch (error89) {
|
|
165812
|
+
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.`);
|
|
165813
|
+
}
|
|
165814
|
+
}
|
|
165815
|
+
}
|
|
165816
|
+
}
|
|
165817
|
+
async function ensureSchemaMetaTable(client) {
|
|
165818
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${SCHEMA_META_TABLE} (
|
|
165819
|
+
id integer PRIMARY KEY CHECK (id = 1),
|
|
165820
|
+
snapshot jsonb NOT NULL,
|
|
165821
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
165822
|
+
)`);
|
|
165823
|
+
}
|
|
165824
|
+
async function readStoredSnapshot(client) {
|
|
165825
|
+
const result = await client.query(`SELECT snapshot FROM ${SCHEMA_META_TABLE} WHERE id = 1`);
|
|
165826
|
+
return result.rows[0]?.snapshot ?? null;
|
|
165827
|
+
}
|
|
165828
|
+
async function writeStoredSnapshot(client, snapshot) {
|
|
165829
|
+
await client.query(`INSERT INTO ${SCHEMA_META_TABLE} (id, snapshot, updated_at)
|
|
165830
|
+
VALUES (1, $1, now())
|
|
165831
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $1, updated_at = now()`, [JSON.stringify(snapshot)]);
|
|
165832
|
+
}
|
|
165833
|
+
async function syncDatabaseSchema(db2, client, isExistingDb) {
|
|
165562
165834
|
try {
|
|
165563
165835
|
const { generateDrizzleJson: generateDrizzleJson2, generateMigration: generateMigration2 } = await Promise.resolve().then(() => (init_api4(), exports_api));
|
|
165564
|
-
|
|
165836
|
+
await ensureSchemaMetaTable(client);
|
|
165837
|
+
const stored = isExistingDb ? await readStoredSnapshot(client) : null;
|
|
165838
|
+
const emptyJson = generateDrizzleJson2({});
|
|
165839
|
+
const prevJson = stored ?? emptyJson;
|
|
165565
165840
|
const curJson = generateDrizzleJson2(exports_tables_index, prevJson.id, undefined, "snake_case");
|
|
165566
165841
|
const statements = await generateMigration2(prevJson, curJson);
|
|
165842
|
+
const tolerateExisting = isExistingDb && stored === null;
|
|
165567
165843
|
for (const statement of statements) {
|
|
165568
|
-
|
|
165844
|
+
try {
|
|
165845
|
+
await db2.execute(statement);
|
|
165846
|
+
} catch (error89) {
|
|
165847
|
+
if (!(tolerateExisting && isDuplicateObjectError(error89))) {
|
|
165848
|
+
throw error89;
|
|
165849
|
+
}
|
|
165850
|
+
}
|
|
165851
|
+
}
|
|
165852
|
+
if (tolerateExisting) {
|
|
165853
|
+
await reconcileMissingColumns(db2, curJson);
|
|
165569
165854
|
}
|
|
165855
|
+
await writeStoredSnapshot(client, curJson);
|
|
165570
165856
|
} catch (error89) {
|
|
165571
|
-
console.error("[Sandbox] Schema
|
|
165857
|
+
console.error("[Sandbox] Schema sync failed:", error89);
|
|
165572
165858
|
throw error89;
|
|
165573
165859
|
}
|
|
165574
165860
|
}
|
|
@@ -165579,16 +165865,17 @@ async function setupDatabase(customPath) {
|
|
|
165579
165865
|
const client = dbPath === ":memory:" ? new We2 : new We2(dbPath);
|
|
165580
165866
|
await client._checkReady();
|
|
165581
165867
|
const db2 = drizzle(client, { schema: exports_tables_index });
|
|
165582
|
-
|
|
165583
|
-
await createDatabaseSchema(db2);
|
|
165584
|
-
}
|
|
165868
|
+
await syncDatabaseSchema(db2, client, dbFileExists);
|
|
165585
165869
|
return db2;
|
|
165586
165870
|
}
|
|
165871
|
+
var SCHEMA_META_TABLE = "_sandbox_schema_meta";
|
|
165872
|
+
var DUPLICATE_OBJECT_CODES;
|
|
165587
165873
|
var init_database = __esm(() => {
|
|
165588
165874
|
init_dist8();
|
|
165589
165875
|
init_pglite();
|
|
165590
165876
|
init_tables_index();
|
|
165591
165877
|
init_path_manager();
|
|
165878
|
+
DUPLICATE_OBJECT_CODES = new Set(["42701", "42710", "42723", "42P06", "42P07"]);
|
|
165592
165879
|
});
|
|
165593
165880
|
function setLogger(logger3) {
|
|
165594
165881
|
customLogger = logger3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcademy/vite-plugin",
|
|
3
|
-
"version": "1.1.2",
|
|
3
|
+
"version": "1.1.3-beta.2",
|
|
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.
|
|
22
|
+
"playcademy": "0.27.1-beta.2"
|
|
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.3",
|
|
29
|
+
"@playcademy/sdk": "0.15.1-beta.1",
|
|
30
30
|
"@playcademy/types": "0.0.1",
|
|
31
31
|
"@playcademy/utils": "0.0.1",
|
|
32
32
|
"@types/archiver": "^6.0.3",
|