@playcademy/vite-plugin 1.1.3-beta.1 → 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 +333 -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.3-beta.
|
|
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,
|
|
@@ -51123,11 +51142,18 @@ var METRICS_BASE;
|
|
|
51123
51142
|
var init_tunnel = __esm(() => {
|
|
51124
51143
|
METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
|
|
51125
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
|
+
});
|
|
51126
51152
|
function getDeploymentId(gameSlug, sstStage) {
|
|
51127
51153
|
if (sstStage === "production") {
|
|
51128
51154
|
return gameSlug;
|
|
51129
51155
|
}
|
|
51130
|
-
if (sstStage === "dev" || sstStage
|
|
51156
|
+
if (sstStage === "dev" || isPreviewStage(sstStage)) {
|
|
51131
51157
|
return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
|
|
51132
51158
|
}
|
|
51133
51159
|
return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
|
|
@@ -51147,6 +51173,7 @@ async function generateDeploymentHash(code) {
|
|
|
51147
51173
|
}
|
|
51148
51174
|
var init_deployment_util = __esm(() => {
|
|
51149
51175
|
init_src();
|
|
51176
|
+
init_stages();
|
|
51150
51177
|
});
|
|
51151
51178
|
|
|
51152
51179
|
class DeployService {
|
|
@@ -100027,7 +100054,8 @@ class ActivityRecord {
|
|
|
100027
100054
|
courseName,
|
|
100028
100055
|
subject: progressData.subject,
|
|
100029
100056
|
appName: progressData.appName,
|
|
100030
|
-
sensorUrl: progressData.sensorUrl
|
|
100057
|
+
sensorUrl: progressData.sensorUrl,
|
|
100058
|
+
eventId: progressData.completionHistoryEventId
|
|
100031
100059
|
});
|
|
100032
100060
|
}
|
|
100033
100061
|
if (masteryProgress?.masteryRevoked) {
|
|
@@ -100052,6 +100080,7 @@ class ActivityRecord {
|
|
|
100052
100080
|
subject: progressData.subject,
|
|
100053
100081
|
appName: progressData.appName,
|
|
100054
100082
|
sensorUrl: progressData.sensorUrl,
|
|
100083
|
+
eventId: progressData.eventId,
|
|
100055
100084
|
extensions: extensions || progressData.extensions,
|
|
100056
100085
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
100057
100086
|
}).catch((error88) => {
|
|
@@ -100075,6 +100104,7 @@ class ActivityRecord {
|
|
|
100075
100104
|
}
|
|
100076
100105
|
async emitCourseCompletionHistoryEvent(data) {
|
|
100077
100106
|
await this.events.emitActivityEvent({
|
|
100107
|
+
eventId: data.eventId,
|
|
100078
100108
|
studentId: data.studentId,
|
|
100079
100109
|
studentEmail: data.studentEmail,
|
|
100080
100110
|
gameId: data.gameId,
|
|
@@ -100190,6 +100220,7 @@ class ActivitySession {
|
|
|
100190
100220
|
subject: sessionData.subject,
|
|
100191
100221
|
appName: sessionData.appName,
|
|
100192
100222
|
sensorUrl: sessionData.sensorUrl,
|
|
100223
|
+
eventId: sessionData.eventId,
|
|
100193
100224
|
...runId ? { runId } : {},
|
|
100194
100225
|
...extensions ? { extensions } : {}
|
|
100195
100226
|
});
|
|
@@ -100456,6 +100487,9 @@ class ActivityEvents {
|
|
|
100456
100487
|
} : {},
|
|
100457
100488
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100458
100489
|
});
|
|
100490
|
+
if (data.eventId) {
|
|
100491
|
+
event.id = data.eventId;
|
|
100492
|
+
}
|
|
100459
100493
|
await this.core.api.caliper.events.send(data.sensorUrl, [event]);
|
|
100460
100494
|
}
|
|
100461
100495
|
async emitTimeSpentEvent(data) {
|
|
@@ -100487,6 +100521,9 @@ class ActivityEvents {
|
|
|
100487
100521
|
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
100488
100522
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
100489
100523
|
});
|
|
100524
|
+
if (data.eventId) {
|
|
100525
|
+
event.id = data.eventId;
|
|
100526
|
+
}
|
|
100490
100527
|
const wireEvent = data.extensions ? { ...event, generated: { ...event.generated, extensions: data.extensions } } : event;
|
|
100491
100528
|
await this.core.api.caliper.events.send(data.sensorUrl, [wireEvent]);
|
|
100492
100529
|
}
|
|
@@ -105544,6 +105581,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
105544
105581
|
static HEARTBEAT_DEDUPE_TTL_MS = 300000;
|
|
105545
105582
|
static processedHeartbeatWindows = new Map;
|
|
105546
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;
|
|
105547
105587
|
deps;
|
|
105548
105588
|
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
105549
105589
|
for (const [key, timestamp6] of this.processedHeartbeatWindows) {
|
|
@@ -105568,6 +105608,136 @@ var init_timeback_service = __esm(async () => {
|
|
|
105568
105608
|
static clearInFlightHeartbeatWindow(key) {
|
|
105569
105609
|
this.inFlightHeartbeatWindows.delete(key);
|
|
105570
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
|
+
}
|
|
105571
105741
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
105572
105742
|
const base = extensions ?? {};
|
|
105573
105743
|
const existingPlaycademy = base.playcademy;
|
|
@@ -106469,16 +106639,25 @@ var init_timeback_service = __esm(async () => {
|
|
|
106469
106639
|
attributes: courseAttributes
|
|
106470
106640
|
} = TimebackService2.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
106471
106641
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
106472
|
-
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 = {
|
|
106473
106660
|
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
106661
|
activityId: activityData.activityId,
|
|
106483
106662
|
activityName: activityData.activityName,
|
|
106484
106663
|
subject: activityData.subject,
|
|
@@ -106487,45 +106666,92 @@ var init_timeback_service = __esm(async () => {
|
|
|
106487
106666
|
courseId: runtimeCourseId,
|
|
106488
106667
|
courseName: runtimeCourseName,
|
|
106489
106668
|
studentEmail: activityData.studentEmail,
|
|
106490
|
-
|
|
106669
|
+
extensions: extensionsWithResumeId,
|
|
106491
106670
|
...runId ? { runId } : {}
|
|
106492
|
-
}
|
|
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
|
+
}
|
|
106493
106708
|
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
106494
106709
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
106495
|
-
const
|
|
106496
|
-
|
|
106497
|
-
|
|
106498
|
-
|
|
106499
|
-
|
|
106500
|
-
|
|
106501
|
-
|
|
106502
|
-
|
|
106503
|
-
|
|
106504
|
-
|
|
106505
|
-
|
|
106506
|
-
|
|
106507
|
-
|
|
106508
|
-
|
|
106509
|
-
|
|
106510
|
-
|
|
106511
|
-
}
|
|
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
|
+
}
|
|
106512
106727
|
}
|
|
106513
106728
|
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,
|
|
106729
|
+
"app.timeback.session_end_attempted": sessionEndAttempted,
|
|
106520
106730
|
"app.timeback.session_end_emitted": sessionEndEmitted,
|
|
106521
106731
|
"app.timeback.active_time_seconds": sessionEndActiveSeconds,
|
|
106522
106732
|
"app.timeback.inactive_time_seconds": sessionEndInactiveSeconds ?? 0,
|
|
106523
|
-
"app.timeback.
|
|
106524
|
-
"app.timeback.xp_awarded": result.xpAwarded,
|
|
106733
|
+
"app.timeback.xp_awarded": result?.xpAwarded ?? 0,
|
|
106525
106734
|
"app.timeback.mastered_units_requested": masteredUnits,
|
|
106526
106735
|
"app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
|
|
106527
|
-
"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
|
+
} : {}
|
|
106528
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
|
+
}
|
|
106529
106755
|
return {
|
|
106530
106756
|
status: "ok",
|
|
106531
106757
|
courseId: runtimeCourseId,
|
|
@@ -165560,17 +165786,75 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
165560
165786
|
init_sqliteSchema();
|
|
165561
165787
|
init_sqliteSerializer();
|
|
165562
165788
|
});
|
|
165563
|
-
|
|
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) {
|
|
165564
165834
|
try {
|
|
165565
165835
|
const { generateDrizzleJson: generateDrizzleJson2, generateMigration: generateMigration2 } = await Promise.resolve().then(() => (init_api4(), exports_api));
|
|
165566
|
-
|
|
165836
|
+
await ensureSchemaMetaTable(client);
|
|
165837
|
+
const stored = isExistingDb ? await readStoredSnapshot(client) : null;
|
|
165838
|
+
const emptyJson = generateDrizzleJson2({});
|
|
165839
|
+
const prevJson = stored ?? emptyJson;
|
|
165567
165840
|
const curJson = generateDrizzleJson2(exports_tables_index, prevJson.id, undefined, "snake_case");
|
|
165568
165841
|
const statements = await generateMigration2(prevJson, curJson);
|
|
165842
|
+
const tolerateExisting = isExistingDb && stored === null;
|
|
165569
165843
|
for (const statement of statements) {
|
|
165570
|
-
|
|
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);
|
|
165571
165854
|
}
|
|
165855
|
+
await writeStoredSnapshot(client, curJson);
|
|
165572
165856
|
} catch (error89) {
|
|
165573
|
-
console.error("[Sandbox] Schema
|
|
165857
|
+
console.error("[Sandbox] Schema sync failed:", error89);
|
|
165574
165858
|
throw error89;
|
|
165575
165859
|
}
|
|
165576
165860
|
}
|
|
@@ -165581,16 +165865,17 @@ async function setupDatabase(customPath) {
|
|
|
165581
165865
|
const client = dbPath === ":memory:" ? new We2 : new We2(dbPath);
|
|
165582
165866
|
await client._checkReady();
|
|
165583
165867
|
const db2 = drizzle(client, { schema: exports_tables_index });
|
|
165584
|
-
|
|
165585
|
-
await createDatabaseSchema(db2);
|
|
165586
|
-
}
|
|
165868
|
+
await syncDatabaseSchema(db2, client, dbFileExists);
|
|
165587
165869
|
return db2;
|
|
165588
165870
|
}
|
|
165871
|
+
var SCHEMA_META_TABLE = "_sandbox_schema_meta";
|
|
165872
|
+
var DUPLICATE_OBJECT_CODES;
|
|
165589
165873
|
var init_database = __esm(() => {
|
|
165590
165874
|
init_dist8();
|
|
165591
165875
|
init_pglite();
|
|
165592
165876
|
init_tables_index();
|
|
165593
165877
|
init_path_manager();
|
|
165878
|
+
DUPLICATE_OBJECT_CODES = new Set(["42701", "42710", "42723", "42P06", "42P07"]);
|
|
165594
165879
|
});
|
|
165595
165880
|
function setLogger(logger3) {
|
|
165596
165881
|
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.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.1-beta.
|
|
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",
|