@playcademy/sandbox 0.6.1-beta.2 → 0.6.1-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/cli.js +338 -48
- package/dist/server.js +338 -48
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1080,7 +1080,7 @@ var package_default;
|
|
|
1080
1080
|
var init_package = __esm(() => {
|
|
1081
1081
|
package_default = {
|
|
1082
1082
|
name: "@playcademy/sandbox",
|
|
1083
|
-
version: "0.6.1-beta.
|
|
1083
|
+
version: "0.6.1-beta.3",
|
|
1084
1084
|
description: "Local development server for Playcademy game development",
|
|
1085
1085
|
type: "module",
|
|
1086
1086
|
exports: {
|
|
@@ -11330,7 +11330,7 @@ var init_table6 = __esm(() => {
|
|
|
11330
11330
|
});
|
|
11331
11331
|
|
|
11332
11332
|
// ../data/src/domains/timeback/table.ts
|
|
11333
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications;
|
|
11333
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11334
11334
|
var init_table7 = __esm(() => {
|
|
11335
11335
|
init_drizzle_orm();
|
|
11336
11336
|
init_pg_core();
|
|
@@ -11382,6 +11382,22 @@ var init_table7 = __esm(() => {
|
|
|
11382
11382
|
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
11383
11383
|
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
11384
11384
|
]);
|
|
11385
|
+
gameTimebackActivityCompletions = pgTable("game_timeback_activity_completions", {
|
|
11386
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
11387
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
11388
|
+
courseId: text("course_id").notNull(),
|
|
11389
|
+
studentId: text("student_id").notNull(),
|
|
11390
|
+
runId: uuid("run_id").notNull(),
|
|
11391
|
+
activityId: text("activity_id").notNull(),
|
|
11392
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
11393
|
+
reservedAt: timestamp("reserved_at", { withTimezone: true, mode: "string" }).notNull().defaultNow(),
|
|
11394
|
+
xpAwarded: doublePrecision("xp_awarded"),
|
|
11395
|
+
masteredUnitsApplied: integer("mastered_units_applied"),
|
|
11396
|
+
pctCompleteApp: doublePrecision("pct_complete_app"),
|
|
11397
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
11398
|
+
}, (table3) => [
|
|
11399
|
+
uniqueIndex("game_timeback_activity_completions_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId, table3.activityId)
|
|
11400
|
+
]);
|
|
11385
11401
|
});
|
|
11386
11402
|
|
|
11387
11403
|
// ../data/src/tables.index.ts
|
|
@@ -11399,6 +11415,7 @@ __export(exports_tables_index, {
|
|
|
11399
11415
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11400
11416
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
11401
11417
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11418
|
+
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
11402
11419
|
gameScoresRelations: () => gameScoresRelations,
|
|
11403
11420
|
gameScores: () => gameScores,
|
|
11404
11421
|
gamePlatformEnum: () => gamePlatformEnum,
|
|
@@ -24677,6 +24694,10 @@ var init_constants2 = __esm(() => {
|
|
|
24677
24694
|
GAME_WORKER_DOMAIN_PRODUCTION = GAME_WORKER_DOMAINS.production;
|
|
24678
24695
|
GAME_WORKER_DOMAIN_STAGING = GAME_WORKER_DOMAINS.staging;
|
|
24679
24696
|
});
|
|
24697
|
+
|
|
24698
|
+
// ../cloudflare/src/playcademy/provider/assets-config.ts
|
|
24699
|
+
var init_assets_config = () => {};
|
|
24700
|
+
|
|
24680
24701
|
// ../cloudflare/src/playcademy/provider/bindings.ts
|
|
24681
24702
|
var init_bindings = __esm(() => {
|
|
24682
24703
|
init_constants2();
|
|
@@ -24693,6 +24714,7 @@ var init_provider = __esm(() => {
|
|
|
24693
24714
|
init_src();
|
|
24694
24715
|
init_core();
|
|
24695
24716
|
init_constants2();
|
|
24717
|
+
init_assets_config();
|
|
24696
24718
|
init_bindings();
|
|
24697
24719
|
init_helpers();
|
|
24698
24720
|
});
|
|
@@ -27135,12 +27157,21 @@ var init_tunnel = __esm(() => {
|
|
|
27135
27157
|
METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
|
|
27136
27158
|
});
|
|
27137
27159
|
|
|
27160
|
+
// ../utils/src/stages.ts
|
|
27161
|
+
function isPreviewStage(stage) {
|
|
27162
|
+
return PREVIEW_STAGE_PATTERN.test(stage);
|
|
27163
|
+
}
|
|
27164
|
+
var PREVIEW_STAGE_PATTERN;
|
|
27165
|
+
var init_stages = __esm(() => {
|
|
27166
|
+
PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
|
|
27167
|
+
});
|
|
27168
|
+
|
|
27138
27169
|
// ../api-core/src/utils/deployment.util.ts
|
|
27139
27170
|
function getDeploymentId(gameSlug, sstStage) {
|
|
27140
27171
|
if (sstStage === "production") {
|
|
27141
27172
|
return gameSlug;
|
|
27142
27173
|
}
|
|
27143
|
-
if (sstStage === "dev" || sstStage
|
|
27174
|
+
if (sstStage === "dev" || isPreviewStage(sstStage)) {
|
|
27144
27175
|
return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
|
|
27145
27176
|
}
|
|
27146
27177
|
return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
|
|
@@ -27160,6 +27191,7 @@ async function generateDeploymentHash(code) {
|
|
|
27160
27191
|
}
|
|
27161
27192
|
var init_deployment_util = __esm(() => {
|
|
27162
27193
|
init_src();
|
|
27194
|
+
init_stages();
|
|
27163
27195
|
});
|
|
27164
27196
|
|
|
27165
27197
|
// ../api-core/src/services/deploy.service.ts
|
|
@@ -74823,7 +74855,8 @@ class ActivityRecord {
|
|
|
74823
74855
|
courseName,
|
|
74824
74856
|
subject: progressData.subject,
|
|
74825
74857
|
appName: progressData.appName,
|
|
74826
|
-
sensorUrl: progressData.sensorUrl
|
|
74858
|
+
sensorUrl: progressData.sensorUrl,
|
|
74859
|
+
eventId: progressData.completionHistoryEventId
|
|
74827
74860
|
});
|
|
74828
74861
|
}
|
|
74829
74862
|
if (masteryProgress?.masteryRevoked) {
|
|
@@ -74848,6 +74881,7 @@ class ActivityRecord {
|
|
|
74848
74881
|
subject: progressData.subject,
|
|
74849
74882
|
appName: progressData.appName,
|
|
74850
74883
|
sensorUrl: progressData.sensorUrl,
|
|
74884
|
+
eventId: progressData.eventId,
|
|
74851
74885
|
extensions: extensions || progressData.extensions,
|
|
74852
74886
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
74853
74887
|
}).catch((error88) => {
|
|
@@ -74871,6 +74905,7 @@ class ActivityRecord {
|
|
|
74871
74905
|
}
|
|
74872
74906
|
async emitCourseCompletionHistoryEvent(data) {
|
|
74873
74907
|
await this.events.emitActivityEvent({
|
|
74908
|
+
eventId: data.eventId,
|
|
74874
74909
|
studentId: data.studentId,
|
|
74875
74910
|
studentEmail: data.studentEmail,
|
|
74876
74911
|
gameId: data.gameId,
|
|
@@ -74986,6 +75021,7 @@ class ActivitySession {
|
|
|
74986
75021
|
subject: sessionData.subject,
|
|
74987
75022
|
appName: sessionData.appName,
|
|
74988
75023
|
sensorUrl: sessionData.sensorUrl,
|
|
75024
|
+
eventId: sessionData.eventId,
|
|
74989
75025
|
...runId ? { runId } : {},
|
|
74990
75026
|
...extensions ? { extensions } : {}
|
|
74991
75027
|
});
|
|
@@ -75252,6 +75288,9 @@ class ActivityEvents {
|
|
|
75252
75288
|
} : {},
|
|
75253
75289
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
75254
75290
|
});
|
|
75291
|
+
if (data.eventId) {
|
|
75292
|
+
event.id = data.eventId;
|
|
75293
|
+
}
|
|
75255
75294
|
await this.core.api.caliper.events.send(data.sensorUrl, [event]);
|
|
75256
75295
|
}
|
|
75257
75296
|
async emitTimeSpentEvent(data) {
|
|
@@ -75283,6 +75322,9 @@ class ActivityEvents {
|
|
|
75283
75322
|
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
75284
75323
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
75285
75324
|
});
|
|
75325
|
+
if (data.eventId) {
|
|
75326
|
+
event.id = data.eventId;
|
|
75327
|
+
}
|
|
75286
75328
|
const wireEvent = data.extensions ? { ...event, generated: { ...event.generated, extensions: data.extensions } } : event;
|
|
75287
75329
|
await this.core.api.caliper.events.send(data.sensorUrl, [wireEvent]);
|
|
75288
75330
|
}
|
|
@@ -80315,6 +80357,7 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
80315
80357
|
});
|
|
80316
80358
|
|
|
80317
80359
|
// ../api-core/src/services/timeback.service.ts
|
|
80360
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
80318
80361
|
async function findGameTimebackIntegrationForUpdate(db2, condition) {
|
|
80319
80362
|
const [integration] = await db2.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
80320
80363
|
return integration;
|
|
@@ -80337,6 +80380,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
80337
80380
|
static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
80338
80381
|
static processedHeartbeatWindows = new Map;
|
|
80339
80382
|
static inFlightHeartbeatWindows = new Map;
|
|
80383
|
+
static COMPLETION_RESERVATION_STALE_SECONDS = 5 * 60;
|
|
80384
|
+
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
80385
|
+
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
80340
80386
|
deps;
|
|
80341
80387
|
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
80342
80388
|
for (const [key, timestamp6] of this.processedHeartbeatWindows) {
|
|
@@ -80361,6 +80407,136 @@ var init_timeback_service = __esm(async () => {
|
|
|
80361
80407
|
static clearInFlightHeartbeatWindow(key) {
|
|
80362
80408
|
this.inFlightHeartbeatWindows.delete(key);
|
|
80363
80409
|
}
|
|
80410
|
+
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
80411
|
+
const hash2 = createHash2("sha256").update([
|
|
80412
|
+
"playcademy",
|
|
80413
|
+
"timeback",
|
|
80414
|
+
kind,
|
|
80415
|
+
key.gameId,
|
|
80416
|
+
key.courseId,
|
|
80417
|
+
key.studentId,
|
|
80418
|
+
key.runId,
|
|
80419
|
+
key.activityId,
|
|
80420
|
+
discriminator
|
|
80421
|
+
].join("\x00")).digest("hex");
|
|
80422
|
+
const variant = (Number.parseInt(hash2.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
80423
|
+
const uuid9 = [
|
|
80424
|
+
hash2.slice(0, 8),
|
|
80425
|
+
hash2.slice(8, 12),
|
|
80426
|
+
`5${hash2.slice(13, 16)}`,
|
|
80427
|
+
`${variant}${hash2.slice(18, 20)}`,
|
|
80428
|
+
hash2.slice(20, 32)
|
|
80429
|
+
].join("-");
|
|
80430
|
+
return `urn:uuid:${uuid9}`;
|
|
80431
|
+
}
|
|
80432
|
+
static async reserveCompletion(db2, key) {
|
|
80433
|
+
const [reserved] = await db2.insert(gameTimebackActivityCompletions).values({
|
|
80434
|
+
gameId: key.gameId,
|
|
80435
|
+
courseId: key.courseId,
|
|
80436
|
+
studentId: key.studentId,
|
|
80437
|
+
runId: key.runId,
|
|
80438
|
+
activityId: key.activityId
|
|
80439
|
+
}).onConflictDoUpdate({
|
|
80440
|
+
target: [
|
|
80441
|
+
gameTimebackActivityCompletions.gameId,
|
|
80442
|
+
gameTimebackActivityCompletions.courseId,
|
|
80443
|
+
gameTimebackActivityCompletions.studentId,
|
|
80444
|
+
gameTimebackActivityCompletions.runId,
|
|
80445
|
+
gameTimebackActivityCompletions.activityId
|
|
80446
|
+
],
|
|
80447
|
+
set: { reservedAt: sql`now()` },
|
|
80448
|
+
setWhere: sql`${gameTimebackActivityCompletions.completedAt} IS NULL AND ${gameTimebackActivityCompletions.reservedAt} < now() - make_interval(secs => ${TimebackService.COMPLETION_RESERVATION_STALE_SECONDS})`
|
|
80449
|
+
}).returning({
|
|
80450
|
+
id: gameTimebackActivityCompletions.id,
|
|
80451
|
+
reservedAt: gameTimebackActivityCompletions.reservedAt,
|
|
80452
|
+
takenOver: sql`${gameTimebackActivityCompletions.reservedAt} <> ${gameTimebackActivityCompletions.createdAt}`
|
|
80453
|
+
});
|
|
80454
|
+
if (reserved) {
|
|
80455
|
+
return {
|
|
80456
|
+
status: "reserved",
|
|
80457
|
+
token: { id: reserved.id, reservedAt: reserved.reservedAt },
|
|
80458
|
+
takenOver: reserved.takenOver
|
|
80459
|
+
};
|
|
80460
|
+
}
|
|
80461
|
+
const existing = await db2.query.gameTimebackActivityCompletions.findFirst({
|
|
80462
|
+
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)),
|
|
80463
|
+
columns: {
|
|
80464
|
+
completedAt: true,
|
|
80465
|
+
xpAwarded: true,
|
|
80466
|
+
masteredUnitsApplied: true,
|
|
80467
|
+
pctCompleteApp: true
|
|
80468
|
+
}
|
|
80469
|
+
});
|
|
80470
|
+
if (!existing?.completedAt) {
|
|
80471
|
+
return { status: "in_flight" };
|
|
80472
|
+
}
|
|
80473
|
+
return {
|
|
80474
|
+
status: "duplicate",
|
|
80475
|
+
award: existing.xpAwarded !== null ? {
|
|
80476
|
+
xpAwarded: existing.xpAwarded,
|
|
80477
|
+
masteredUnitsApplied: existing.masteredUnitsApplied ?? 0,
|
|
80478
|
+
pctCompleteApp: existing.pctCompleteApp
|
|
80479
|
+
} : null
|
|
80480
|
+
};
|
|
80481
|
+
}
|
|
80482
|
+
static async acquireCompletionReservation(db2, key) {
|
|
80483
|
+
let reserveOutcome = await this.reserveCompletion(db2, key);
|
|
80484
|
+
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
80485
|
+
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
80486
|
+
reserveOutcome = await this.reserveCompletion(db2, key);
|
|
80487
|
+
}
|
|
80488
|
+
if (reserveOutcome.status === "duplicate") {
|
|
80489
|
+
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
80490
|
+
return reserveOutcome;
|
|
80491
|
+
}
|
|
80492
|
+
if (reserveOutcome.status === "in_flight") {
|
|
80493
|
+
setAttribute("app.timeback.end_activity_status", "completion_reservation_in_flight");
|
|
80494
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
80495
|
+
}
|
|
80496
|
+
if (reserveOutcome.takenOver) {
|
|
80497
|
+
setAttribute("app.timeback.completion_reservation_takeover", true);
|
|
80498
|
+
addEvent("timeback.completion_reservation_takeover", {
|
|
80499
|
+
"app.timeback.run_id": key.runId,
|
|
80500
|
+
"app.timeback.activity_id": key.activityId
|
|
80501
|
+
});
|
|
80502
|
+
}
|
|
80503
|
+
return { status: "reserved", token: reserveOutcome.token };
|
|
80504
|
+
}
|
|
80505
|
+
static async runWithCompletionReservation(db2, key, emit, awardFromResult) {
|
|
80506
|
+
const acquired = await this.acquireCompletionReservation(db2, key);
|
|
80507
|
+
if (acquired.status === "duplicate") {
|
|
80508
|
+
return acquired;
|
|
80509
|
+
}
|
|
80510
|
+
const logContext = { run_id: key.runId, activity_id: key.activityId };
|
|
80511
|
+
let result;
|
|
80512
|
+
try {
|
|
80513
|
+
result = await emit();
|
|
80514
|
+
} catch (error88) {
|
|
80515
|
+
try {
|
|
80516
|
+
await this.rollbackCompletion(db2, acquired.token);
|
|
80517
|
+
} catch (rollbackError) {
|
|
80518
|
+
logTimebackError("rollback completion reservation", rollbackError, logContext);
|
|
80519
|
+
}
|
|
80520
|
+
throw error88;
|
|
80521
|
+
}
|
|
80522
|
+
try {
|
|
80523
|
+
await this.confirmCompletion(db2, acquired.token, awardFromResult(result));
|
|
80524
|
+
} catch (confirmError) {
|
|
80525
|
+
logTimebackError("confirm completion reservation", confirmError, logContext);
|
|
80526
|
+
}
|
|
80527
|
+
return { status: "emitted", result };
|
|
80528
|
+
}
|
|
80529
|
+
static async confirmCompletion(db2, token, award) {
|
|
80530
|
+
await db2.update(gameTimebackActivityCompletions).set({
|
|
80531
|
+
completedAt: sql`now()`,
|
|
80532
|
+
xpAwarded: award.xpAwarded,
|
|
80533
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
80534
|
+
pctCompleteApp: award.pctCompleteApp
|
|
80535
|
+
}).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
80536
|
+
}
|
|
80537
|
+
static async rollbackCompletion(db2, token) {
|
|
80538
|
+
await db2.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
80539
|
+
}
|
|
80364
80540
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
80365
80541
|
const base = extensions ?? {};
|
|
80366
80542
|
const existingPlaycademy = base.playcademy;
|
|
@@ -81262,16 +81438,25 @@ var init_timeback_service = __esm(async () => {
|
|
|
81262
81438
|
attributes: courseAttributes
|
|
81263
81439
|
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81264
81440
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
81265
|
-
const
|
|
81441
|
+
const dedupeKey = runId !== undefined ? {
|
|
81442
|
+
gameId,
|
|
81443
|
+
courseId: runtimeCourseId,
|
|
81444
|
+
studentId,
|
|
81445
|
+
runId,
|
|
81446
|
+
activityId: activityData.activityId
|
|
81447
|
+
} : undefined;
|
|
81448
|
+
setAttributes({
|
|
81449
|
+
...courseAttributes,
|
|
81450
|
+
"app.timeback.grade": activityData.grade,
|
|
81451
|
+
"app.timeback.subject": activityData.subject,
|
|
81452
|
+
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
81453
|
+
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
81454
|
+
"app.timeback.score_pct": scorePercentage,
|
|
81455
|
+
"app.timeback.xp_requested": xpEarned
|
|
81456
|
+
});
|
|
81457
|
+
const courseTotalXp = integration.totalXp;
|
|
81458
|
+
const activityEventContext = {
|
|
81266
81459
|
gameId,
|
|
81267
|
-
score: scorePercentage,
|
|
81268
|
-
totalQuestions: scoreData.totalQuestions,
|
|
81269
|
-
correctQuestions: scoreData.correctQuestions,
|
|
81270
|
-
durationSeconds: timingData.durationSeconds,
|
|
81271
|
-
xpEarned,
|
|
81272
|
-
masteredUnits,
|
|
81273
|
-
masteredUnitsAbsolute,
|
|
81274
|
-
extensions: extensionsWithResumeId,
|
|
81275
81460
|
activityId: activityData.activityId,
|
|
81276
81461
|
activityName: activityData.activityName,
|
|
81277
81462
|
subject: activityData.subject,
|
|
@@ -81280,45 +81465,92 @@ var init_timeback_service = __esm(async () => {
|
|
|
81280
81465
|
courseId: runtimeCourseId,
|
|
81281
81466
|
courseName: runtimeCourseName,
|
|
81282
81467
|
studentEmail: activityData.studentEmail,
|
|
81283
|
-
|
|
81468
|
+
extensions: extensionsWithResumeId,
|
|
81284
81469
|
...runId ? { runId } : {}
|
|
81285
|
-
}
|
|
81470
|
+
};
|
|
81471
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
81472
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
81473
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
81474
|
+
function emitRecord() {
|
|
81475
|
+
return client.activity.record(runtimeCourseId, studentId, {
|
|
81476
|
+
...activityEventContext,
|
|
81477
|
+
eventId: completionEventId,
|
|
81478
|
+
completionHistoryEventId,
|
|
81479
|
+
score: scorePercentage,
|
|
81480
|
+
totalQuestions: scoreData.totalQuestions,
|
|
81481
|
+
correctQuestions: scoreData.correctQuestions,
|
|
81482
|
+
durationSeconds: timingData.durationSeconds,
|
|
81483
|
+
xpEarned,
|
|
81484
|
+
masteredUnits,
|
|
81485
|
+
masteredUnitsAbsolute,
|
|
81486
|
+
courseTotalXp
|
|
81487
|
+
});
|
|
81488
|
+
}
|
|
81489
|
+
let result;
|
|
81490
|
+
let blockedByDedupeGuard = false;
|
|
81491
|
+
let duplicateAward = null;
|
|
81492
|
+
if (dedupeKey) {
|
|
81493
|
+
const outcome = await TimebackService.runWithCompletionReservation(db2, dedupeKey, emitRecord, (emitted) => ({
|
|
81494
|
+
xpAwarded: emitted.xpAwarded,
|
|
81495
|
+
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
81496
|
+
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
81497
|
+
}));
|
|
81498
|
+
if (outcome.status === "duplicate") {
|
|
81499
|
+
blockedByDedupeGuard = true;
|
|
81500
|
+
duplicateAward = outcome.award;
|
|
81501
|
+
} else {
|
|
81502
|
+
result = outcome.result;
|
|
81503
|
+
}
|
|
81504
|
+
} else {
|
|
81505
|
+
result = await emitRecord();
|
|
81506
|
+
}
|
|
81286
81507
|
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
81287
81508
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
81288
|
-
const
|
|
81289
|
-
|
|
81290
|
-
|
|
81291
|
-
|
|
81292
|
-
|
|
81293
|
-
|
|
81294
|
-
|
|
81295
|
-
|
|
81296
|
-
|
|
81297
|
-
|
|
81298
|
-
|
|
81299
|
-
|
|
81300
|
-
|
|
81301
|
-
|
|
81302
|
-
|
|
81303
|
-
|
|
81304
|
-
}
|
|
81509
|
+
const sessionEndAttempted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
81510
|
+
let sessionEndEmitted = false;
|
|
81511
|
+
if (sessionEndAttempted) {
|
|
81512
|
+
try {
|
|
81513
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81514
|
+
...activityEventContext,
|
|
81515
|
+
eventId: sessionEndEventId,
|
|
81516
|
+
activeTimeSeconds: sessionEndActiveSeconds,
|
|
81517
|
+
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {}
|
|
81518
|
+
});
|
|
81519
|
+
sessionEndEmitted = true;
|
|
81520
|
+
} catch (sessionError) {
|
|
81521
|
+
logTimebackError("emit session end after completion", sessionError, {
|
|
81522
|
+
run_id: runId,
|
|
81523
|
+
activity_id: activityData.activityId
|
|
81524
|
+
});
|
|
81525
|
+
}
|
|
81305
81526
|
}
|
|
81306
81527
|
setAttributes({
|
|
81307
|
-
|
|
81308
|
-
"app.timeback.grade": activityData.grade,
|
|
81309
|
-
"app.timeback.subject": activityData.subject,
|
|
81310
|
-
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
81311
|
-
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
81312
|
-
"app.timeback.score_pct": scorePercentage,
|
|
81528
|
+
"app.timeback.session_end_attempted": sessionEndAttempted,
|
|
81313
81529
|
"app.timeback.session_end_emitted": sessionEndEmitted,
|
|
81314
81530
|
"app.timeback.active_time_seconds": sessionEndActiveSeconds,
|
|
81315
81531
|
"app.timeback.inactive_time_seconds": sessionEndInactiveSeconds ?? 0,
|
|
81316
|
-
"app.timeback.
|
|
81317
|
-
"app.timeback.xp_awarded": result.xpAwarded,
|
|
81532
|
+
"app.timeback.xp_awarded": result?.xpAwarded ?? 0,
|
|
81318
81533
|
"app.timeback.mastered_units_requested": masteredUnits,
|
|
81319
81534
|
"app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
|
|
81320
|
-
"app.timeback.mastered_units_applied": result
|
|
81535
|
+
"app.timeback.mastered_units_applied": result?.masteredUnitsApplied ?? 0,
|
|
81536
|
+
...blockedByDedupeGuard ? {
|
|
81537
|
+
"app.timeback.xp_already_awarded": duplicateAward?.xpAwarded ?? 0,
|
|
81538
|
+
"app.timeback.mastered_units_already_applied": duplicateAward?.masteredUnitsApplied ?? 0
|
|
81539
|
+
} : {}
|
|
81321
81540
|
});
|
|
81541
|
+
if (blockedByDedupeGuard) {
|
|
81542
|
+
return {
|
|
81543
|
+
status: "ok",
|
|
81544
|
+
courseId: runtimeCourseId,
|
|
81545
|
+
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
81546
|
+
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
81547
|
+
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
81548
|
+
blockedByDedupeGuard: true
|
|
81549
|
+
};
|
|
81550
|
+
}
|
|
81551
|
+
if (!result) {
|
|
81552
|
+
throw new InternalError("Completion emit did not produce a result");
|
|
81553
|
+
}
|
|
81322
81554
|
return {
|
|
81323
81555
|
status: "ok",
|
|
81324
81556
|
courseId: runtimeCourseId,
|
|
@@ -137748,17 +137980,75 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
137748
137980
|
|
|
137749
137981
|
// src/database/index.ts
|
|
137750
137982
|
import fs3 from "node:fs";
|
|
137751
|
-
|
|
137983
|
+
function isDuplicateObjectError(error89) {
|
|
137984
|
+
const code = error89.code;
|
|
137985
|
+
if (code && DUPLICATE_OBJECT_CODES.has(code)) {
|
|
137986
|
+
return true;
|
|
137987
|
+
}
|
|
137988
|
+
return /already exists/i.test(errorMessage(error89));
|
|
137989
|
+
}
|
|
137990
|
+
async function reconcileMissingColumns(db2, snapshot) {
|
|
137991
|
+
const tables = snapshot.tables ?? {};
|
|
137992
|
+
for (const table9 of Object.values(tables)) {
|
|
137993
|
+
for (const column6 of Object.values(table9.columns ?? {})) {
|
|
137994
|
+
const clauses = [
|
|
137995
|
+
`ALTER TABLE "${table9.name}" ADD COLUMN IF NOT EXISTS "${column6.name}" ${column6.type}`
|
|
137996
|
+
];
|
|
137997
|
+
if (column6.default !== undefined) {
|
|
137998
|
+
clauses.push(`DEFAULT ${column6.default}`);
|
|
137999
|
+
}
|
|
138000
|
+
if (column6.notNull && !column6.primaryKey) {
|
|
138001
|
+
clauses.push("NOT NULL");
|
|
138002
|
+
}
|
|
138003
|
+
try {
|
|
138004
|
+
await db2.execute(clauses.join(" "));
|
|
138005
|
+
} catch (error89) {
|
|
138006
|
+
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.`);
|
|
138007
|
+
}
|
|
138008
|
+
}
|
|
138009
|
+
}
|
|
138010
|
+
}
|
|
138011
|
+
async function ensureSchemaMetaTable(client) {
|
|
138012
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${SCHEMA_META_TABLE} (
|
|
138013
|
+
id integer PRIMARY KEY CHECK (id = 1),
|
|
138014
|
+
snapshot jsonb NOT NULL,
|
|
138015
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
138016
|
+
)`);
|
|
138017
|
+
}
|
|
138018
|
+
async function readStoredSnapshot(client) {
|
|
138019
|
+
const result = await client.query(`SELECT snapshot FROM ${SCHEMA_META_TABLE} WHERE id = 1`);
|
|
138020
|
+
return result.rows[0]?.snapshot ?? null;
|
|
138021
|
+
}
|
|
138022
|
+
async function writeStoredSnapshot(client, snapshot) {
|
|
138023
|
+
await client.query(`INSERT INTO ${SCHEMA_META_TABLE} (id, snapshot, updated_at)
|
|
138024
|
+
VALUES (1, $1, now())
|
|
138025
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $1, updated_at = now()`, [JSON.stringify(snapshot)]);
|
|
138026
|
+
}
|
|
138027
|
+
async function syncDatabaseSchema(db2, client, isExistingDb) {
|
|
137752
138028
|
try {
|
|
137753
138029
|
const { generateDrizzleJson: generateDrizzleJson2, generateMigration: generateMigration2 } = await Promise.resolve().then(() => (init_api4(), exports_api));
|
|
137754
|
-
|
|
138030
|
+
await ensureSchemaMetaTable(client);
|
|
138031
|
+
const stored = isExistingDb ? await readStoredSnapshot(client) : null;
|
|
138032
|
+
const emptyJson = generateDrizzleJson2({});
|
|
138033
|
+
const prevJson = stored ?? emptyJson;
|
|
137755
138034
|
const curJson = generateDrizzleJson2(exports_tables_index, prevJson.id, undefined, "snake_case");
|
|
137756
138035
|
const statements = await generateMigration2(prevJson, curJson);
|
|
138036
|
+
const tolerateExisting = isExistingDb && stored === null;
|
|
137757
138037
|
for (const statement of statements) {
|
|
137758
|
-
|
|
138038
|
+
try {
|
|
138039
|
+
await db2.execute(statement);
|
|
138040
|
+
} catch (error89) {
|
|
138041
|
+
if (!(tolerateExisting && isDuplicateObjectError(error89))) {
|
|
138042
|
+
throw error89;
|
|
138043
|
+
}
|
|
138044
|
+
}
|
|
138045
|
+
}
|
|
138046
|
+
if (tolerateExisting) {
|
|
138047
|
+
await reconcileMissingColumns(db2, curJson);
|
|
137759
138048
|
}
|
|
138049
|
+
await writeStoredSnapshot(client, curJson);
|
|
137760
138050
|
} catch (error89) {
|
|
137761
|
-
console.error("[Sandbox] Schema
|
|
138051
|
+
console.error("[Sandbox] Schema sync failed:", error89);
|
|
137762
138052
|
throw error89;
|
|
137763
138053
|
}
|
|
137764
138054
|
}
|
|
@@ -137769,16 +138059,16 @@ async function setupDatabase(customPath) {
|
|
|
137769
138059
|
const client = dbPath === ":memory:" ? new We2 : new We2(dbPath);
|
|
137770
138060
|
await client._checkReady();
|
|
137771
138061
|
const db2 = drizzle(client, { schema: exports_tables_index });
|
|
137772
|
-
|
|
137773
|
-
await createDatabaseSchema(db2);
|
|
137774
|
-
}
|
|
138062
|
+
await syncDatabaseSchema(db2, client, dbFileExists);
|
|
137775
138063
|
return db2;
|
|
137776
138064
|
}
|
|
138065
|
+
var SCHEMA_META_TABLE = "_sandbox_schema_meta", DUPLICATE_OBJECT_CODES;
|
|
137777
138066
|
var init_database = __esm(() => {
|
|
137778
138067
|
init_dist8();
|
|
137779
138068
|
init_pglite();
|
|
137780
138069
|
init_tables_index();
|
|
137781
138070
|
init_path_manager();
|
|
138071
|
+
DUPLICATE_OBJECT_CODES = new Set(["42701", "42710", "42723", "42P06", "42P07"]);
|
|
137782
138072
|
});
|
|
137783
138073
|
|
|
137784
138074
|
// src/lib/logging/adapter.ts
|
package/dist/server.js
CHANGED
|
@@ -1079,7 +1079,7 @@ var package_default;
|
|
|
1079
1079
|
var init_package = __esm(() => {
|
|
1080
1080
|
package_default = {
|
|
1081
1081
|
name: "@playcademy/sandbox",
|
|
1082
|
-
version: "0.6.1-beta.
|
|
1082
|
+
version: "0.6.1-beta.3",
|
|
1083
1083
|
description: "Local development server for Playcademy game development",
|
|
1084
1084
|
type: "module",
|
|
1085
1085
|
exports: {
|
|
@@ -11329,7 +11329,7 @@ var init_table6 = __esm(() => {
|
|
|
11329
11329
|
});
|
|
11330
11330
|
|
|
11331
11331
|
// ../data/src/domains/timeback/table.ts
|
|
11332
|
-
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications;
|
|
11332
|
+
var gameTimebackIntegrationStatusEnum, gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications, gameTimebackActivityCompletions;
|
|
11333
11333
|
var init_table7 = __esm(() => {
|
|
11334
11334
|
init_drizzle_orm();
|
|
11335
11335
|
init_pg_core();
|
|
@@ -11381,6 +11381,22 @@ var init_table7 = __esm(() => {
|
|
|
11381
11381
|
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
11382
11382
|
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
11383
11383
|
]);
|
|
11384
|
+
gameTimebackActivityCompletions = pgTable("game_timeback_activity_completions", {
|
|
11385
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
11386
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
11387
|
+
courseId: text("course_id").notNull(),
|
|
11388
|
+
studentId: text("student_id").notNull(),
|
|
11389
|
+
runId: uuid("run_id").notNull(),
|
|
11390
|
+
activityId: text("activity_id").notNull(),
|
|
11391
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
11392
|
+
reservedAt: timestamp("reserved_at", { withTimezone: true, mode: "string" }).notNull().defaultNow(),
|
|
11393
|
+
xpAwarded: doublePrecision("xp_awarded"),
|
|
11394
|
+
masteredUnitsApplied: integer("mastered_units_applied"),
|
|
11395
|
+
pctCompleteApp: doublePrecision("pct_complete_app"),
|
|
11396
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
11397
|
+
}, (table3) => [
|
|
11398
|
+
uniqueIndex("game_timeback_activity_completions_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId, table3.activityId)
|
|
11399
|
+
]);
|
|
11384
11400
|
});
|
|
11385
11401
|
|
|
11386
11402
|
// ../data/src/tables.index.ts
|
|
@@ -11398,6 +11414,7 @@ __export(exports_tables_index, {
|
|
|
11398
11414
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11399
11415
|
gameTimebackIntegrationStatusEnum: () => gameTimebackIntegrationStatusEnum,
|
|
11400
11416
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11417
|
+
gameTimebackActivityCompletions: () => gameTimebackActivityCompletions,
|
|
11401
11418
|
gameScoresRelations: () => gameScoresRelations,
|
|
11402
11419
|
gameScores: () => gameScores,
|
|
11403
11420
|
gamePlatformEnum: () => gamePlatformEnum,
|
|
@@ -24676,6 +24693,10 @@ var init_constants2 = __esm(() => {
|
|
|
24676
24693
|
GAME_WORKER_DOMAIN_PRODUCTION = GAME_WORKER_DOMAINS.production;
|
|
24677
24694
|
GAME_WORKER_DOMAIN_STAGING = GAME_WORKER_DOMAINS.staging;
|
|
24678
24695
|
});
|
|
24696
|
+
|
|
24697
|
+
// ../cloudflare/src/playcademy/provider/assets-config.ts
|
|
24698
|
+
var init_assets_config = () => {};
|
|
24699
|
+
|
|
24679
24700
|
// ../cloudflare/src/playcademy/provider/bindings.ts
|
|
24680
24701
|
var init_bindings = __esm(() => {
|
|
24681
24702
|
init_constants2();
|
|
@@ -24692,6 +24713,7 @@ var init_provider = __esm(() => {
|
|
|
24692
24713
|
init_src();
|
|
24693
24714
|
init_core();
|
|
24694
24715
|
init_constants2();
|
|
24716
|
+
init_assets_config();
|
|
24695
24717
|
init_bindings();
|
|
24696
24718
|
init_helpers();
|
|
24697
24719
|
});
|
|
@@ -27134,12 +27156,21 @@ var init_tunnel = __esm(() => {
|
|
|
27134
27156
|
METRICS_BASE = `http://127.0.0.1:${TUNNEL_METRICS_PORT}`;
|
|
27135
27157
|
});
|
|
27136
27158
|
|
|
27159
|
+
// ../utils/src/stages.ts
|
|
27160
|
+
function isPreviewStage(stage) {
|
|
27161
|
+
return PREVIEW_STAGE_PATTERN.test(stage);
|
|
27162
|
+
}
|
|
27163
|
+
var PREVIEW_STAGE_PATTERN;
|
|
27164
|
+
var init_stages = __esm(() => {
|
|
27165
|
+
PREVIEW_STAGE_PATTERN = /^pr-\d+$/;
|
|
27166
|
+
});
|
|
27167
|
+
|
|
27137
27168
|
// ../api-core/src/utils/deployment.util.ts
|
|
27138
27169
|
function getDeploymentId(gameSlug, sstStage) {
|
|
27139
27170
|
if (sstStage === "production") {
|
|
27140
27171
|
return gameSlug;
|
|
27141
27172
|
}
|
|
27142
|
-
if (sstStage === "dev" || sstStage
|
|
27173
|
+
if (sstStage === "dev" || isPreviewStage(sstStage)) {
|
|
27143
27174
|
return `${WORKER_NAMING.STAGING_PREFIX}${gameSlug}`;
|
|
27144
27175
|
}
|
|
27145
27176
|
return `${WORKER_NAMING.LOCAL_PREFIX}${sstStage}-${gameSlug}`;
|
|
@@ -27159,6 +27190,7 @@ async function generateDeploymentHash(code) {
|
|
|
27159
27190
|
}
|
|
27160
27191
|
var init_deployment_util = __esm(() => {
|
|
27161
27192
|
init_src();
|
|
27193
|
+
init_stages();
|
|
27162
27194
|
});
|
|
27163
27195
|
|
|
27164
27196
|
// ../api-core/src/services/deploy.service.ts
|
|
@@ -74822,7 +74854,8 @@ class ActivityRecord {
|
|
|
74822
74854
|
courseName,
|
|
74823
74855
|
subject: progressData.subject,
|
|
74824
74856
|
appName: progressData.appName,
|
|
74825
|
-
sensorUrl: progressData.sensorUrl
|
|
74857
|
+
sensorUrl: progressData.sensorUrl,
|
|
74858
|
+
eventId: progressData.completionHistoryEventId
|
|
74826
74859
|
});
|
|
74827
74860
|
}
|
|
74828
74861
|
if (masteryProgress?.masteryRevoked) {
|
|
@@ -74847,6 +74880,7 @@ class ActivityRecord {
|
|
|
74847
74880
|
subject: progressData.subject,
|
|
74848
74881
|
appName: progressData.appName,
|
|
74849
74882
|
sensorUrl: progressData.sensorUrl,
|
|
74883
|
+
eventId: progressData.eventId,
|
|
74850
74884
|
extensions: extensions || progressData.extensions,
|
|
74851
74885
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
74852
74886
|
}).catch((error88) => {
|
|
@@ -74870,6 +74904,7 @@ class ActivityRecord {
|
|
|
74870
74904
|
}
|
|
74871
74905
|
async emitCourseCompletionHistoryEvent(data) {
|
|
74872
74906
|
await this.events.emitActivityEvent({
|
|
74907
|
+
eventId: data.eventId,
|
|
74873
74908
|
studentId: data.studentId,
|
|
74874
74909
|
studentEmail: data.studentEmail,
|
|
74875
74910
|
gameId: data.gameId,
|
|
@@ -74985,6 +75020,7 @@ class ActivitySession {
|
|
|
74985
75020
|
subject: sessionData.subject,
|
|
74986
75021
|
appName: sessionData.appName,
|
|
74987
75022
|
sensorUrl: sessionData.sensorUrl,
|
|
75023
|
+
eventId: sessionData.eventId,
|
|
74988
75024
|
...runId ? { runId } : {},
|
|
74989
75025
|
...extensions ? { extensions } : {}
|
|
74990
75026
|
});
|
|
@@ -75251,6 +75287,9 @@ class ActivityEvents {
|
|
|
75251
75287
|
} : {},
|
|
75252
75288
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
75253
75289
|
});
|
|
75290
|
+
if (data.eventId) {
|
|
75291
|
+
event.id = data.eventId;
|
|
75292
|
+
}
|
|
75254
75293
|
await this.core.api.caliper.events.send(data.sensorUrl, [event]);
|
|
75255
75294
|
}
|
|
75256
75295
|
async emitTimeSpentEvent(data) {
|
|
@@ -75282,6 +75321,9 @@ class ActivityEvents {
|
|
|
75282
75321
|
...data.runId ? { session: `urn:uuid:${data.runId}` } : {},
|
|
75283
75322
|
...eventExtensions ? { extensions: eventExtensions } : {}
|
|
75284
75323
|
});
|
|
75324
|
+
if (data.eventId) {
|
|
75325
|
+
event.id = data.eventId;
|
|
75326
|
+
}
|
|
75285
75327
|
const wireEvent = data.extensions ? { ...event, generated: { ...event.generated, extensions: data.extensions } } : event;
|
|
75286
75328
|
await this.core.api.caliper.events.send(data.sensorUrl, [wireEvent]);
|
|
75287
75329
|
}
|
|
@@ -80314,6 +80356,7 @@ var init_timeback_removed_integration_util = __esm(() => {
|
|
|
80314
80356
|
});
|
|
80315
80357
|
|
|
80316
80358
|
// ../api-core/src/services/timeback.service.ts
|
|
80359
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
80317
80360
|
async function findGameTimebackIntegrationForUpdate(db2, condition) {
|
|
80318
80361
|
const [integration] = await db2.select().from(gameTimebackIntegrations).where(condition).limit(1).for("update");
|
|
80319
80362
|
return integration;
|
|
@@ -80336,6 +80379,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
80336
80379
|
static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
80337
80380
|
static processedHeartbeatWindows = new Map;
|
|
80338
80381
|
static inFlightHeartbeatWindows = new Map;
|
|
80382
|
+
static COMPLETION_RESERVATION_STALE_SECONDS = 5 * 60;
|
|
80383
|
+
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
80384
|
+
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
80339
80385
|
deps;
|
|
80340
80386
|
static cleanHeartbeatDedupeCache(now2 = Date.now()) {
|
|
80341
80387
|
for (const [key, timestamp6] of this.processedHeartbeatWindows) {
|
|
@@ -80360,6 +80406,136 @@ var init_timeback_service = __esm(async () => {
|
|
|
80360
80406
|
static clearInFlightHeartbeatWindow(key) {
|
|
80361
80407
|
this.inFlightHeartbeatWindows.delete(key);
|
|
80362
80408
|
}
|
|
80409
|
+
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
80410
|
+
const hash2 = createHash2("sha256").update([
|
|
80411
|
+
"playcademy",
|
|
80412
|
+
"timeback",
|
|
80413
|
+
kind,
|
|
80414
|
+
key.gameId,
|
|
80415
|
+
key.courseId,
|
|
80416
|
+
key.studentId,
|
|
80417
|
+
key.runId,
|
|
80418
|
+
key.activityId,
|
|
80419
|
+
discriminator
|
|
80420
|
+
].join("\x00")).digest("hex");
|
|
80421
|
+
const variant = (Number.parseInt(hash2.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0");
|
|
80422
|
+
const uuid9 = [
|
|
80423
|
+
hash2.slice(0, 8),
|
|
80424
|
+
hash2.slice(8, 12),
|
|
80425
|
+
`5${hash2.slice(13, 16)}`,
|
|
80426
|
+
`${variant}${hash2.slice(18, 20)}`,
|
|
80427
|
+
hash2.slice(20, 32)
|
|
80428
|
+
].join("-");
|
|
80429
|
+
return `urn:uuid:${uuid9}`;
|
|
80430
|
+
}
|
|
80431
|
+
static async reserveCompletion(db2, key) {
|
|
80432
|
+
const [reserved] = await db2.insert(gameTimebackActivityCompletions).values({
|
|
80433
|
+
gameId: key.gameId,
|
|
80434
|
+
courseId: key.courseId,
|
|
80435
|
+
studentId: key.studentId,
|
|
80436
|
+
runId: key.runId,
|
|
80437
|
+
activityId: key.activityId
|
|
80438
|
+
}).onConflictDoUpdate({
|
|
80439
|
+
target: [
|
|
80440
|
+
gameTimebackActivityCompletions.gameId,
|
|
80441
|
+
gameTimebackActivityCompletions.courseId,
|
|
80442
|
+
gameTimebackActivityCompletions.studentId,
|
|
80443
|
+
gameTimebackActivityCompletions.runId,
|
|
80444
|
+
gameTimebackActivityCompletions.activityId
|
|
80445
|
+
],
|
|
80446
|
+
set: { reservedAt: sql`now()` },
|
|
80447
|
+
setWhere: sql`${gameTimebackActivityCompletions.completedAt} IS NULL AND ${gameTimebackActivityCompletions.reservedAt} < now() - make_interval(secs => ${TimebackService.COMPLETION_RESERVATION_STALE_SECONDS})`
|
|
80448
|
+
}).returning({
|
|
80449
|
+
id: gameTimebackActivityCompletions.id,
|
|
80450
|
+
reservedAt: gameTimebackActivityCompletions.reservedAt,
|
|
80451
|
+
takenOver: sql`${gameTimebackActivityCompletions.reservedAt} <> ${gameTimebackActivityCompletions.createdAt}`
|
|
80452
|
+
});
|
|
80453
|
+
if (reserved) {
|
|
80454
|
+
return {
|
|
80455
|
+
status: "reserved",
|
|
80456
|
+
token: { id: reserved.id, reservedAt: reserved.reservedAt },
|
|
80457
|
+
takenOver: reserved.takenOver
|
|
80458
|
+
};
|
|
80459
|
+
}
|
|
80460
|
+
const existing = await db2.query.gameTimebackActivityCompletions.findFirst({
|
|
80461
|
+
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)),
|
|
80462
|
+
columns: {
|
|
80463
|
+
completedAt: true,
|
|
80464
|
+
xpAwarded: true,
|
|
80465
|
+
masteredUnitsApplied: true,
|
|
80466
|
+
pctCompleteApp: true
|
|
80467
|
+
}
|
|
80468
|
+
});
|
|
80469
|
+
if (!existing?.completedAt) {
|
|
80470
|
+
return { status: "in_flight" };
|
|
80471
|
+
}
|
|
80472
|
+
return {
|
|
80473
|
+
status: "duplicate",
|
|
80474
|
+
award: existing.xpAwarded !== null ? {
|
|
80475
|
+
xpAwarded: existing.xpAwarded,
|
|
80476
|
+
masteredUnitsApplied: existing.masteredUnitsApplied ?? 0,
|
|
80477
|
+
pctCompleteApp: existing.pctCompleteApp
|
|
80478
|
+
} : null
|
|
80479
|
+
};
|
|
80480
|
+
}
|
|
80481
|
+
static async acquireCompletionReservation(db2, key) {
|
|
80482
|
+
let reserveOutcome = await this.reserveCompletion(db2, key);
|
|
80483
|
+
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
80484
|
+
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
80485
|
+
reserveOutcome = await this.reserveCompletion(db2, key);
|
|
80486
|
+
}
|
|
80487
|
+
if (reserveOutcome.status === "duplicate") {
|
|
80488
|
+
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
80489
|
+
return reserveOutcome;
|
|
80490
|
+
}
|
|
80491
|
+
if (reserveOutcome.status === "in_flight") {
|
|
80492
|
+
setAttribute("app.timeback.end_activity_status", "completion_reservation_in_flight");
|
|
80493
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
80494
|
+
}
|
|
80495
|
+
if (reserveOutcome.takenOver) {
|
|
80496
|
+
setAttribute("app.timeback.completion_reservation_takeover", true);
|
|
80497
|
+
addEvent("timeback.completion_reservation_takeover", {
|
|
80498
|
+
"app.timeback.run_id": key.runId,
|
|
80499
|
+
"app.timeback.activity_id": key.activityId
|
|
80500
|
+
});
|
|
80501
|
+
}
|
|
80502
|
+
return { status: "reserved", token: reserveOutcome.token };
|
|
80503
|
+
}
|
|
80504
|
+
static async runWithCompletionReservation(db2, key, emit, awardFromResult) {
|
|
80505
|
+
const acquired = await this.acquireCompletionReservation(db2, key);
|
|
80506
|
+
if (acquired.status === "duplicate") {
|
|
80507
|
+
return acquired;
|
|
80508
|
+
}
|
|
80509
|
+
const logContext = { run_id: key.runId, activity_id: key.activityId };
|
|
80510
|
+
let result;
|
|
80511
|
+
try {
|
|
80512
|
+
result = await emit();
|
|
80513
|
+
} catch (error88) {
|
|
80514
|
+
try {
|
|
80515
|
+
await this.rollbackCompletion(db2, acquired.token);
|
|
80516
|
+
} catch (rollbackError) {
|
|
80517
|
+
logTimebackError("rollback completion reservation", rollbackError, logContext);
|
|
80518
|
+
}
|
|
80519
|
+
throw error88;
|
|
80520
|
+
}
|
|
80521
|
+
try {
|
|
80522
|
+
await this.confirmCompletion(db2, acquired.token, awardFromResult(result));
|
|
80523
|
+
} catch (confirmError) {
|
|
80524
|
+
logTimebackError("confirm completion reservation", confirmError, logContext);
|
|
80525
|
+
}
|
|
80526
|
+
return { status: "emitted", result };
|
|
80527
|
+
}
|
|
80528
|
+
static async confirmCompletion(db2, token, award) {
|
|
80529
|
+
await db2.update(gameTimebackActivityCompletions).set({
|
|
80530
|
+
completedAt: sql`now()`,
|
|
80531
|
+
xpAwarded: award.xpAwarded,
|
|
80532
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
80533
|
+
pctCompleteApp: award.pctCompleteApp
|
|
80534
|
+
}).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
80535
|
+
}
|
|
80536
|
+
static async rollbackCompletion(db2, token) {
|
|
80537
|
+
await db2.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
80538
|
+
}
|
|
80363
80539
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
80364
80540
|
const base = extensions ?? {};
|
|
80365
80541
|
const existingPlaycademy = base.playcademy;
|
|
@@ -81261,16 +81437,25 @@ var init_timeback_service = __esm(async () => {
|
|
|
81261
81437
|
attributes: courseAttributes
|
|
81262
81438
|
} = TimebackService.resolveRuntimeCourse(integration.courseId, activityData.courseId, activityData.courseName);
|
|
81263
81439
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
81264
|
-
const
|
|
81440
|
+
const dedupeKey = runId !== undefined ? {
|
|
81441
|
+
gameId,
|
|
81442
|
+
courseId: runtimeCourseId,
|
|
81443
|
+
studentId,
|
|
81444
|
+
runId,
|
|
81445
|
+
activityId: activityData.activityId
|
|
81446
|
+
} : undefined;
|
|
81447
|
+
setAttributes({
|
|
81448
|
+
...courseAttributes,
|
|
81449
|
+
"app.timeback.grade": activityData.grade,
|
|
81450
|
+
"app.timeback.subject": activityData.subject,
|
|
81451
|
+
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
81452
|
+
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
81453
|
+
"app.timeback.score_pct": scorePercentage,
|
|
81454
|
+
"app.timeback.xp_requested": xpEarned
|
|
81455
|
+
});
|
|
81456
|
+
const courseTotalXp = integration.totalXp;
|
|
81457
|
+
const activityEventContext = {
|
|
81265
81458
|
gameId,
|
|
81266
|
-
score: scorePercentage,
|
|
81267
|
-
totalQuestions: scoreData.totalQuestions,
|
|
81268
|
-
correctQuestions: scoreData.correctQuestions,
|
|
81269
|
-
durationSeconds: timingData.durationSeconds,
|
|
81270
|
-
xpEarned,
|
|
81271
|
-
masteredUnits,
|
|
81272
|
-
masteredUnitsAbsolute,
|
|
81273
|
-
extensions: extensionsWithResumeId,
|
|
81274
81459
|
activityId: activityData.activityId,
|
|
81275
81460
|
activityName: activityData.activityName,
|
|
81276
81461
|
subject: activityData.subject,
|
|
@@ -81279,45 +81464,92 @@ var init_timeback_service = __esm(async () => {
|
|
|
81279
81464
|
courseId: runtimeCourseId,
|
|
81280
81465
|
courseName: runtimeCourseName,
|
|
81281
81466
|
studentEmail: activityData.studentEmail,
|
|
81282
|
-
|
|
81467
|
+
extensions: extensionsWithResumeId,
|
|
81283
81468
|
...runId ? { runId } : {}
|
|
81284
|
-
}
|
|
81469
|
+
};
|
|
81470
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
81471
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
81472
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
81473
|
+
function emitRecord() {
|
|
81474
|
+
return client.activity.record(runtimeCourseId, studentId, {
|
|
81475
|
+
...activityEventContext,
|
|
81476
|
+
eventId: completionEventId,
|
|
81477
|
+
completionHistoryEventId,
|
|
81478
|
+
score: scorePercentage,
|
|
81479
|
+
totalQuestions: scoreData.totalQuestions,
|
|
81480
|
+
correctQuestions: scoreData.correctQuestions,
|
|
81481
|
+
durationSeconds: timingData.durationSeconds,
|
|
81482
|
+
xpEarned,
|
|
81483
|
+
masteredUnits,
|
|
81484
|
+
masteredUnitsAbsolute,
|
|
81485
|
+
courseTotalXp
|
|
81486
|
+
});
|
|
81487
|
+
}
|
|
81488
|
+
let result;
|
|
81489
|
+
let blockedByDedupeGuard = false;
|
|
81490
|
+
let duplicateAward = null;
|
|
81491
|
+
if (dedupeKey) {
|
|
81492
|
+
const outcome = await TimebackService.runWithCompletionReservation(db2, dedupeKey, emitRecord, (emitted) => ({
|
|
81493
|
+
xpAwarded: emitted.xpAwarded,
|
|
81494
|
+
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
81495
|
+
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
81496
|
+
}));
|
|
81497
|
+
if (outcome.status === "duplicate") {
|
|
81498
|
+
blockedByDedupeGuard = true;
|
|
81499
|
+
duplicateAward = outcome.award;
|
|
81500
|
+
} else {
|
|
81501
|
+
result = outcome.result;
|
|
81502
|
+
}
|
|
81503
|
+
} else {
|
|
81504
|
+
result = await emitRecord();
|
|
81505
|
+
}
|
|
81285
81506
|
const sessionEndActiveSeconds = sessionTimingData?.activeSeconds ?? timingData.durationSeconds;
|
|
81286
81507
|
const sessionEndInactiveSeconds = sessionTimingData?.inactiveSeconds;
|
|
81287
|
-
const
|
|
81288
|
-
|
|
81289
|
-
|
|
81290
|
-
|
|
81291
|
-
|
|
81292
|
-
|
|
81293
|
-
|
|
81294
|
-
|
|
81295
|
-
|
|
81296
|
-
|
|
81297
|
-
|
|
81298
|
-
|
|
81299
|
-
|
|
81300
|
-
|
|
81301
|
-
|
|
81302
|
-
|
|
81303
|
-
}
|
|
81508
|
+
const sessionEndAttempted = sessionEndActiveSeconds > 0 || (sessionEndInactiveSeconds ?? 0) > 0;
|
|
81509
|
+
let sessionEndEmitted = false;
|
|
81510
|
+
if (sessionEndAttempted) {
|
|
81511
|
+
try {
|
|
81512
|
+
await client.activity.session(runtimeCourseId, studentId, {
|
|
81513
|
+
...activityEventContext,
|
|
81514
|
+
eventId: sessionEndEventId,
|
|
81515
|
+
activeTimeSeconds: sessionEndActiveSeconds,
|
|
81516
|
+
...sessionEndInactiveSeconds !== undefined ? { inactiveTimeSeconds: sessionEndInactiveSeconds } : {}
|
|
81517
|
+
});
|
|
81518
|
+
sessionEndEmitted = true;
|
|
81519
|
+
} catch (sessionError) {
|
|
81520
|
+
logTimebackError("emit session end after completion", sessionError, {
|
|
81521
|
+
run_id: runId,
|
|
81522
|
+
activity_id: activityData.activityId
|
|
81523
|
+
});
|
|
81524
|
+
}
|
|
81304
81525
|
}
|
|
81305
81526
|
setAttributes({
|
|
81306
|
-
|
|
81307
|
-
"app.timeback.grade": activityData.grade,
|
|
81308
|
-
"app.timeback.subject": activityData.subject,
|
|
81309
|
-
"app.timeback.total_questions": scoreData.totalQuestions,
|
|
81310
|
-
"app.timeback.correct_questions": scoreData.correctQuestions,
|
|
81311
|
-
"app.timeback.score_pct": scorePercentage,
|
|
81527
|
+
"app.timeback.session_end_attempted": sessionEndAttempted,
|
|
81312
81528
|
"app.timeback.session_end_emitted": sessionEndEmitted,
|
|
81313
81529
|
"app.timeback.active_time_seconds": sessionEndActiveSeconds,
|
|
81314
81530
|
"app.timeback.inactive_time_seconds": sessionEndInactiveSeconds ?? 0,
|
|
81315
|
-
"app.timeback.
|
|
81316
|
-
"app.timeback.xp_awarded": result.xpAwarded,
|
|
81531
|
+
"app.timeback.xp_awarded": result?.xpAwarded ?? 0,
|
|
81317
81532
|
"app.timeback.mastered_units_requested": masteredUnits,
|
|
81318
81533
|
"app.timeback.mastered_units_absolute_requested": masteredUnitsAbsolute,
|
|
81319
|
-
"app.timeback.mastered_units_applied": result
|
|
81534
|
+
"app.timeback.mastered_units_applied": result?.masteredUnitsApplied ?? 0,
|
|
81535
|
+
...blockedByDedupeGuard ? {
|
|
81536
|
+
"app.timeback.xp_already_awarded": duplicateAward?.xpAwarded ?? 0,
|
|
81537
|
+
"app.timeback.mastered_units_already_applied": duplicateAward?.masteredUnitsApplied ?? 0
|
|
81538
|
+
} : {}
|
|
81320
81539
|
});
|
|
81540
|
+
if (blockedByDedupeGuard) {
|
|
81541
|
+
return {
|
|
81542
|
+
status: "ok",
|
|
81543
|
+
courseId: runtimeCourseId,
|
|
81544
|
+
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
81545
|
+
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
81546
|
+
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
81547
|
+
blockedByDedupeGuard: true
|
|
81548
|
+
};
|
|
81549
|
+
}
|
|
81550
|
+
if (!result) {
|
|
81551
|
+
throw new InternalError("Completion emit did not produce a result");
|
|
81552
|
+
}
|
|
81321
81553
|
return {
|
|
81322
81554
|
status: "ok",
|
|
81323
81555
|
courseId: runtimeCourseId,
|
|
@@ -137747,17 +137979,75 @@ The unique index ${source_default.underline.blue(name22)} on the ${source_defaul
|
|
|
137747
137979
|
|
|
137748
137980
|
// src/database/index.ts
|
|
137749
137981
|
import fs3 from "node:fs";
|
|
137750
|
-
|
|
137982
|
+
function isDuplicateObjectError(error89) {
|
|
137983
|
+
const code = error89.code;
|
|
137984
|
+
if (code && DUPLICATE_OBJECT_CODES.has(code)) {
|
|
137985
|
+
return true;
|
|
137986
|
+
}
|
|
137987
|
+
return /already exists/i.test(errorMessage(error89));
|
|
137988
|
+
}
|
|
137989
|
+
async function reconcileMissingColumns(db2, snapshot) {
|
|
137990
|
+
const tables = snapshot.tables ?? {};
|
|
137991
|
+
for (const table9 of Object.values(tables)) {
|
|
137992
|
+
for (const column6 of Object.values(table9.columns ?? {})) {
|
|
137993
|
+
const clauses = [
|
|
137994
|
+
`ALTER TABLE "${table9.name}" ADD COLUMN IF NOT EXISTS "${column6.name}" ${column6.type}`
|
|
137995
|
+
];
|
|
137996
|
+
if (column6.default !== undefined) {
|
|
137997
|
+
clauses.push(`DEFAULT ${column6.default}`);
|
|
137998
|
+
}
|
|
137999
|
+
if (column6.notNull && !column6.primaryKey) {
|
|
138000
|
+
clauses.push("NOT NULL");
|
|
138001
|
+
}
|
|
138002
|
+
try {
|
|
138003
|
+
await db2.execute(clauses.join(" "));
|
|
138004
|
+
} catch (error89) {
|
|
138005
|
+
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.`);
|
|
138006
|
+
}
|
|
138007
|
+
}
|
|
138008
|
+
}
|
|
138009
|
+
}
|
|
138010
|
+
async function ensureSchemaMetaTable(client) {
|
|
138011
|
+
await client.query(`CREATE TABLE IF NOT EXISTS ${SCHEMA_META_TABLE} (
|
|
138012
|
+
id integer PRIMARY KEY CHECK (id = 1),
|
|
138013
|
+
snapshot jsonb NOT NULL,
|
|
138014
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
138015
|
+
)`);
|
|
138016
|
+
}
|
|
138017
|
+
async function readStoredSnapshot(client) {
|
|
138018
|
+
const result = await client.query(`SELECT snapshot FROM ${SCHEMA_META_TABLE} WHERE id = 1`);
|
|
138019
|
+
return result.rows[0]?.snapshot ?? null;
|
|
138020
|
+
}
|
|
138021
|
+
async function writeStoredSnapshot(client, snapshot) {
|
|
138022
|
+
await client.query(`INSERT INTO ${SCHEMA_META_TABLE} (id, snapshot, updated_at)
|
|
138023
|
+
VALUES (1, $1, now())
|
|
138024
|
+
ON CONFLICT (id) DO UPDATE SET snapshot = $1, updated_at = now()`, [JSON.stringify(snapshot)]);
|
|
138025
|
+
}
|
|
138026
|
+
async function syncDatabaseSchema(db2, client, isExistingDb) {
|
|
137751
138027
|
try {
|
|
137752
138028
|
const { generateDrizzleJson: generateDrizzleJson2, generateMigration: generateMigration2 } = await Promise.resolve().then(() => (init_api4(), exports_api));
|
|
137753
|
-
|
|
138029
|
+
await ensureSchemaMetaTable(client);
|
|
138030
|
+
const stored = isExistingDb ? await readStoredSnapshot(client) : null;
|
|
138031
|
+
const emptyJson = generateDrizzleJson2({});
|
|
138032
|
+
const prevJson = stored ?? emptyJson;
|
|
137754
138033
|
const curJson = generateDrizzleJson2(exports_tables_index, prevJson.id, undefined, "snake_case");
|
|
137755
138034
|
const statements = await generateMigration2(prevJson, curJson);
|
|
138035
|
+
const tolerateExisting = isExistingDb && stored === null;
|
|
137756
138036
|
for (const statement of statements) {
|
|
137757
|
-
|
|
138037
|
+
try {
|
|
138038
|
+
await db2.execute(statement);
|
|
138039
|
+
} catch (error89) {
|
|
138040
|
+
if (!(tolerateExisting && isDuplicateObjectError(error89))) {
|
|
138041
|
+
throw error89;
|
|
138042
|
+
}
|
|
138043
|
+
}
|
|
138044
|
+
}
|
|
138045
|
+
if (tolerateExisting) {
|
|
138046
|
+
await reconcileMissingColumns(db2, curJson);
|
|
137758
138047
|
}
|
|
138048
|
+
await writeStoredSnapshot(client, curJson);
|
|
137759
138049
|
} catch (error89) {
|
|
137760
|
-
console.error("[Sandbox] Schema
|
|
138050
|
+
console.error("[Sandbox] Schema sync failed:", error89);
|
|
137761
138051
|
throw error89;
|
|
137762
138052
|
}
|
|
137763
138053
|
}
|
|
@@ -137768,16 +138058,16 @@ async function setupDatabase(customPath) {
|
|
|
137768
138058
|
const client = dbPath === ":memory:" ? new We2 : new We2(dbPath);
|
|
137769
138059
|
await client._checkReady();
|
|
137770
138060
|
const db2 = drizzle(client, { schema: exports_tables_index });
|
|
137771
|
-
|
|
137772
|
-
await createDatabaseSchema(db2);
|
|
137773
|
-
}
|
|
138061
|
+
await syncDatabaseSchema(db2, client, dbFileExists);
|
|
137774
138062
|
return db2;
|
|
137775
138063
|
}
|
|
138064
|
+
var SCHEMA_META_TABLE = "_sandbox_schema_meta", DUPLICATE_OBJECT_CODES;
|
|
137776
138065
|
var init_database = __esm(() => {
|
|
137777
138066
|
init_dist8();
|
|
137778
138067
|
init_pglite();
|
|
137779
138068
|
init_tables_index();
|
|
137780
138069
|
init_path_manager();
|
|
138070
|
+
DUPLICATE_OBJECT_CODES = new Set(["42701", "42710", "42723", "42P06", "42P07"]);
|
|
137781
138071
|
});
|
|
137782
138072
|
|
|
137783
138073
|
// src/lib/logging/adapter.ts
|