gencow 0.1.102 → 0.1.104
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/bin/gencow.mjs +18 -8
- package/core/index.js +6 -1
- package/package.json +1 -1
- package/server/index.js +72 -31
- package/server/index.js.map +2 -2
package/bin/gencow.mjs
CHANGED
|
@@ -1378,7 +1378,13 @@ ${hasPrompt ? `
|
|
|
1378
1378
|
|
|
1379
1379
|
if (isLocal) {
|
|
1380
1380
|
// ── 로컬 모드: 기존 동작 유지 ──
|
|
1381
|
-
|
|
1381
|
+
let hasDb = !!process.env.DATABASE_URL;
|
|
1382
|
+
if (!hasDb) {
|
|
1383
|
+
const envPath = resolve(process.cwd(), ".env");
|
|
1384
|
+
if (existsSync(envPath)) hasDb = readFileSync(envPath, "utf8").includes("DATABASE_URL=");
|
|
1385
|
+
}
|
|
1386
|
+
const targetDb = hasDb ? "local PG" : "local fallback: PGlite";
|
|
1387
|
+
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${targetDb})${RESET}\n`);
|
|
1382
1388
|
info("Pushing schema.ts → database (no migration files)...");
|
|
1383
1389
|
runInServer("pnpm db:push --force", buildEnv(config));
|
|
1384
1390
|
success("Schema pushed!");
|
|
@@ -1407,6 +1413,8 @@ ${hasPrompt ? `
|
|
|
1407
1413
|
// ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
|
|
1408
1414
|
const creds = requireCreds();
|
|
1409
1415
|
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
|
|
1416
|
+
warn("명령어 실행 전 먼저 gencow deploy 를 통해 최신 코드가 배포되어 있어야 합니다.");
|
|
1417
|
+
log("");
|
|
1410
1418
|
info("Pushing schema.ts → cloud database...");
|
|
1411
1419
|
|
|
1412
1420
|
// gencow/ 폴더를 tar.gz로 패키징
|
|
@@ -1434,8 +1442,8 @@ ${hasPrompt ? `
|
|
|
1434
1442
|
success("마이그레이션 생성 완료");
|
|
1435
1443
|
} catch (e) {
|
|
1436
1444
|
const msg = e.stderr?.toString() || e.message || "";
|
|
1437
|
-
if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
|
|
1438
|
-
|
|
1445
|
+
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
1446
|
+
log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 사용${RESET}`);
|
|
1439
1447
|
} else {
|
|
1440
1448
|
warn(`마이그레이션 생성 실패: ${msg.split("\n")[0]}`);
|
|
1441
1449
|
info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
|
|
@@ -1988,7 +1996,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1988
1996
|
msg.includes("nothing to migrate") ||
|
|
1989
1997
|
msg.includes("No changes detected")
|
|
1990
1998
|
) {
|
|
1991
|
-
|
|
1999
|
+
log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 유지${RESET}`);
|
|
1992
2000
|
} else {
|
|
1993
2001
|
warn(`마이그레이션 생성 경고: ${msg.split("\n")[0] || "unknown"}`);
|
|
1994
2002
|
warn("gencow/migrations/ 가 없거나 오래된 경우 플랫폼에서 스키마 적용이 스킵될 수 있습니다.");
|
|
@@ -2140,6 +2148,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2140
2148
|
method: "POST",
|
|
2141
2149
|
headers: {
|
|
2142
2150
|
"Content-Type": "application/octet-stream",
|
|
2151
|
+
"X-Deploy-Local-Dir": process.cwd(),
|
|
2143
2152
|
},
|
|
2144
2153
|
body: bundleBuffer,
|
|
2145
2154
|
});
|
|
@@ -2194,6 +2203,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2194
2203
|
method: "POST",
|
|
2195
2204
|
headers: {
|
|
2196
2205
|
"Content-Type": "application/octet-stream",
|
|
2206
|
+
"X-Deploy-Local-Dir": process.cwd(),
|
|
2197
2207
|
},
|
|
2198
2208
|
body: retryBuffer,
|
|
2199
2209
|
});
|
|
@@ -2490,8 +2500,8 @@ ${BOLD}Examples:${RESET}
|
|
|
2490
2500
|
success("마이그레이션 생성 완료");
|
|
2491
2501
|
} catch (e) {
|
|
2492
2502
|
const msg = e.stderr?.toString() || e.message || "";
|
|
2493
|
-
if (msg.includes("No schema changes") || msg.includes("nothing to migrate")) {
|
|
2494
|
-
|
|
2503
|
+
if (msg.includes("No schema changes") || msg.includes("nothing to migrate") || msg.includes("No changes detected")) {
|
|
2504
|
+
log(`${DIM} 스키마 변경 없음 — 기존 마이그레이션 사용${RESET}`);
|
|
2495
2505
|
} else {
|
|
2496
2506
|
warn(`마이그레이션 생성 실패: ${msg.split("\\n")[0]}`);
|
|
2497
2507
|
info("서버에서 기존 방식으로 스키마 적용을 시도합니다.");
|
|
@@ -2602,7 +2612,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2602
2612
|
|
|
2603
2613
|
const backendDeployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy?env=${envTarget}`, {
|
|
2604
2614
|
method: "POST",
|
|
2605
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
2615
|
+
headers: { "Content-Type": "application/octet-stream", "X-Deploy-Local-Dir": process.cwd() },
|
|
2606
2616
|
body: backendBuffer,
|
|
2607
2617
|
});
|
|
2608
2618
|
|
|
@@ -2685,7 +2695,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2685
2695
|
|
|
2686
2696
|
const deployRes = await platformFetch(creds, `/platform/apps/${appId}/deploy-static`, {
|
|
2687
2697
|
method: "POST",
|
|
2688
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
2698
|
+
headers: { "Content-Type": "application/octet-stream", "X-Deploy-Local-Dir": process.cwd() },
|
|
2689
2699
|
body: bundleBuffer,
|
|
2690
2700
|
});
|
|
2691
2701
|
|
package/core/index.js
CHANGED
|
@@ -1665,7 +1665,12 @@ function createScheduler() {
|
|
|
1665
1665
|
actions.set(name, handler);
|
|
1666
1666
|
},
|
|
1667
1667
|
async executeAction(name, args) {
|
|
1668
|
-
|
|
1668
|
+
try {
|
|
1669
|
+
await executeAction(name, args);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1672
|
+
console.error(`[scheduler] executeAction("${name}") failed: ${msg}`);
|
|
1673
|
+
}
|
|
1669
1674
|
}
|
|
1670
1675
|
};
|
|
1671
1676
|
}
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1673,7 +1673,12 @@ function createScheduler() {
|
|
|
1673
1673
|
actions.set(name21, handler);
|
|
1674
1674
|
},
|
|
1675
1675
|
async executeAction(name21, args) {
|
|
1676
|
-
|
|
1676
|
+
try {
|
|
1677
|
+
await executeAction(name21, args);
|
|
1678
|
+
} catch (error95) {
|
|
1679
|
+
const msg = error95 instanceof Error ? error95.message : String(error95);
|
|
1680
|
+
console.error(`[scheduler] executeAction("${name21}") failed: ${msg}`);
|
|
1681
|
+
}
|
|
1677
1682
|
}
|
|
1678
1683
|
};
|
|
1679
1684
|
}
|
|
@@ -82259,10 +82264,6 @@ if (IS_BAAS) {
|
|
|
82259
82264
|
const contentLength = c.res.headers.get("content-length");
|
|
82260
82265
|
if (contentLength) {
|
|
82261
82266
|
runtimeBuffer.add("bandwidth_bytes", parseInt(contentLength, 10));
|
|
82262
|
-
} else if (c.res.body) {
|
|
82263
|
-
const cloned = c.res.clone();
|
|
82264
|
-
const buf = await cloned.arrayBuffer();
|
|
82265
|
-
runtimeBuffer.add("bandwidth_bytes", buf.byteLength);
|
|
82266
82267
|
}
|
|
82267
82268
|
} catch {
|
|
82268
82269
|
}
|
|
@@ -82637,25 +82638,19 @@ async function main() {
|
|
|
82637
82638
|
// @ts-ignore
|
|
82638
82639
|
duplex: "half"
|
|
82639
82640
|
});
|
|
82641
|
+
const buf = await proxyRes.arrayBuffer();
|
|
82640
82642
|
if (appName) {
|
|
82641
|
-
|
|
82642
|
-
const cl = proxyRes.headers.get("content-length");
|
|
82643
|
-
if (cl) {
|
|
82644
|
-
addProxyMetric(appName, parseInt(cl, 10));
|
|
82645
|
-
} else if (proxyRes.body) {
|
|
82646
|
-
const buf = await proxyRes.arrayBuffer();
|
|
82647
|
-
addProxyMetric(appName, buf.byteLength);
|
|
82648
|
-
return new Response(buf, {
|
|
82649
|
-
status: proxyRes.status,
|
|
82650
|
-
headers: proxyRes.headers
|
|
82651
|
-
});
|
|
82652
|
-
}
|
|
82653
|
-
} catch {
|
|
82654
|
-
}
|
|
82643
|
+
addProxyMetric(appName, buf.byteLength);
|
|
82655
82644
|
}
|
|
82656
|
-
|
|
82645
|
+
const resHeaders = new Headers(proxyRes.headers);
|
|
82646
|
+
if (buf.byteLength > 0) {
|
|
82647
|
+
resHeaders.set("content-length", String(buf.byteLength));
|
|
82648
|
+
} else {
|
|
82649
|
+
resHeaders.delete("content-length");
|
|
82650
|
+
}
|
|
82651
|
+
return new Response(buf, {
|
|
82657
82652
|
status: proxyRes.status,
|
|
82658
|
-
headers:
|
|
82653
|
+
headers: resHeaders
|
|
82659
82654
|
});
|
|
82660
82655
|
}
|
|
82661
82656
|
async function serveStaticFile(c, dataDir, pathname, appName) {
|
|
@@ -82771,6 +82766,22 @@ async function main() {
|
|
|
82771
82766
|
if (sleepingApps.length > 0) {
|
|
82772
82767
|
console.log(`[platform] Registered ${sleepingApps.length} sleeping app(s) into memory for future wake \u2713`);
|
|
82773
82768
|
}
|
|
82769
|
+
const allAppsForCronSync = [...runningApps, ...sleepingApps];
|
|
82770
|
+
if (allAppsForCronSync.length > 0) {
|
|
82771
|
+
const { syncCronJobs: _syncCronJobs } = await import(resolve5(functionsPath, "../src/provisioner.ts"));
|
|
82772
|
+
let cronSyncCount = 0;
|
|
82773
|
+
for (const row of allAppsForCronSync) {
|
|
82774
|
+
try {
|
|
82775
|
+
await _syncCronJobs(row.id, row.name, row.dataDir);
|
|
82776
|
+
cronSyncCount++;
|
|
82777
|
+
} catch (e) {
|
|
82778
|
+
console.warn(`[cron] ${row.name}: boot-time cron sync failed:`, e.message);
|
|
82779
|
+
}
|
|
82780
|
+
}
|
|
82781
|
+
if (cronSyncCount > 0) {
|
|
82782
|
+
console.log(`[platform] Boot-time cron sync complete for ${cronSyncCount}/${allAppsForCronSync.length} app(s) \u2713`);
|
|
82783
|
+
}
|
|
82784
|
+
}
|
|
82774
82785
|
} catch (e) {
|
|
82775
82786
|
const msg = e instanceof Error ? e.message : String(e);
|
|
82776
82787
|
console.error("[platform] Auto-recovery failed:", msg);
|
|
@@ -82808,15 +82819,27 @@ async function main() {
|
|
|
82808
82819
|
try {
|
|
82809
82820
|
const { canSleep: _canSleep, sleepApp: _sleepApp, getLastAccess: _getLastAccess, getAppMeta: _getAppMeta } = await import(resolve5(functionsPath, "../src/provisioner.ts"));
|
|
82810
82821
|
const metaMap = _getAppMeta();
|
|
82822
|
+
const allApps = await db.select({ name: appsTable.name, status: appsTable.status }).from(appsTable);
|
|
82823
|
+
const dbStatusMap = new Map(allApps.map((a) => [a.name, a.status]));
|
|
82811
82824
|
for (const [appName] of metaMap) {
|
|
82812
82825
|
if (sleepCount >= MAX_SLEEP_PER_CYCLE) break;
|
|
82826
|
+
const dbStatus = dbStatusMap.get(appName);
|
|
82827
|
+
if (dbStatus === "sleeping") {
|
|
82828
|
+
console.log(`[sleep] SWEEP: ${appName} is "sleeping" in DB but active in memory \u2014 enforcing sleep...`);
|
|
82829
|
+
try {
|
|
82830
|
+
await _sleepApp(appName);
|
|
82831
|
+
sleepCount++;
|
|
82832
|
+
} catch (e) {
|
|
82833
|
+
console.error(`[sleep] Failed to sweep ghost app ${appName}:`, e);
|
|
82834
|
+
}
|
|
82835
|
+
continue;
|
|
82836
|
+
}
|
|
82813
82837
|
const last = _getLastAccess(appName) || 0;
|
|
82814
82838
|
const canSlp = _canSleep(appName);
|
|
82815
82839
|
if (!canSlp) continue;
|
|
82816
82840
|
const idleMin = Math.floor((Date.now() - last) / 6e4);
|
|
82817
82841
|
try {
|
|
82818
82842
|
await _sleepApp(appName);
|
|
82819
|
-
await db.update(appsTable).set({ status: "sleeping" }).where(eqOp(appsTable.name, appName));
|
|
82820
82843
|
console.log(`[sleep] ${appName}: idle ${idleMin}m \u2014 sleeping \u2713`);
|
|
82821
82844
|
sleepCount++;
|
|
82822
82845
|
} catch (e) {
|
|
@@ -82909,6 +82932,8 @@ async function main() {
|
|
|
82909
82932
|
console.error(`[cron] ${label}: failed (${elapsed}s) \u2014 ${errMsg}`);
|
|
82910
82933
|
}
|
|
82911
82934
|
}
|
|
82935
|
+
let _cronEmptyCount = 0;
|
|
82936
|
+
const CRON_EMPTY_LOG_INTERVAL = 5;
|
|
82912
82937
|
setInterval(async () => {
|
|
82913
82938
|
try {
|
|
82914
82939
|
const dueJobs = await rawSql(
|
|
@@ -82921,15 +82946,31 @@ async function main() {
|
|
|
82921
82946
|
ORDER BY cj.next_run_at ASC
|
|
82922
82947
|
LIMIT 50`
|
|
82923
82948
|
);
|
|
82924
|
-
if (dueJobs.length === 0)
|
|
82925
|
-
|
|
82926
|
-
|
|
82927
|
-
|
|
82928
|
-
await Promise.allSettled(batch.map((job) => executeCronJob(job)));
|
|
82929
|
-
if (i + CRON_CONCURRENCY < dueJobs.length) {
|
|
82930
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
82949
|
+
if (dueJobs.length === 0) {
|
|
82950
|
+
_cronEmptyCount++;
|
|
82951
|
+
if (_cronEmptyCount % CRON_EMPTY_LOG_INTERVAL === 0) {
|
|
82952
|
+
console.log(`[cron] No due jobs (${_cronEmptyCount} consecutive empty polls)`);
|
|
82931
82953
|
}
|
|
82954
|
+
return;
|
|
82932
82955
|
}
|
|
82956
|
+
_cronEmptyCount = 0;
|
|
82957
|
+
console.log(`[cron] ${dueJobs.length} due cron job(s) found`);
|
|
82958
|
+
for (const job of dueJobs) {
|
|
82959
|
+
const nextRun = cronCalculateNextRun2(job.schedule);
|
|
82960
|
+
await rawSql(
|
|
82961
|
+
`UPDATE cron_jobs SET next_run_at = $1 WHERE id = $2`,
|
|
82962
|
+
[nextRun, job.id]
|
|
82963
|
+
);
|
|
82964
|
+
}
|
|
82965
|
+
void (async () => {
|
|
82966
|
+
for (let i = 0; i < dueJobs.length; i += CRON_CONCURRENCY) {
|
|
82967
|
+
const batch = dueJobs.slice(i, i + CRON_CONCURRENCY);
|
|
82968
|
+
await Promise.allSettled(batch.map((job) => executeCronJob(job)));
|
|
82969
|
+
if (i + CRON_CONCURRENCY < dueJobs.length) {
|
|
82970
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
82971
|
+
}
|
|
82972
|
+
}
|
|
82973
|
+
})();
|
|
82933
82974
|
} catch (err) {
|
|
82934
82975
|
console.error(`[cron] Scheduler error:`, err.message);
|
|
82935
82976
|
}
|
|
@@ -83253,7 +83294,7 @@ async function main() {
|
|
|
83253
83294
|
if (!IS_BAAS && Array.isArray(result) && result.length >= 2) {
|
|
83254
83295
|
checkMixedUserIds(name21, result);
|
|
83255
83296
|
}
|
|
83256
|
-
return c.json(result);
|
|
83297
|
+
return c.json(result ?? null);
|
|
83257
83298
|
} catch (err) {
|
|
83258
83299
|
const status = err?.code === "FUNCTION_TIMEOUT" ? 408 : err instanceof GencowValidationError ? 400 : 500;
|
|
83259
83300
|
return c.json({ error: err.message, ...err?.code ? { code: err.code } : {} }, status);
|
|
@@ -83288,7 +83329,7 @@ async function main() {
|
|
|
83288
83329
|
name21
|
|
83289
83330
|
);
|
|
83290
83331
|
await invalidateQueries(mut.invalidates, ctx);
|
|
83291
|
-
return c.json(result, 201);
|
|
83332
|
+
return c.json(result ?? null, 201);
|
|
83292
83333
|
} catch (err) {
|
|
83293
83334
|
const status = err?.code === "FUNCTION_TIMEOUT" ? 408 : err instanceof GencowValidationError ? 400 : 500;
|
|
83294
83335
|
return c.json({ error: err.message, ...err?.code ? { code: err.code } : {} }, status);
|