@meetploy/cli 1.17.0 → 1.17.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/dashboard-dist/assets/{main-B-euJxpr.css → main-5Kt9I_hM.css} +1 -1
- package/dist/dashboard-dist/assets/main-BFhQn9QT.js +354 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/dev.js +244 -5
- package/dist/index.js +464 -6
- package/package.json +1 -3
- package/dist/dashboard-dist/assets/main-duAiLjPq.js +0 -339
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getCookie, deleteCookie, setCookie } from 'hono/cookie';
|
|
|
13
13
|
import { URL, fileURLToPath } from 'url';
|
|
14
14
|
import { serve } from '@hono/node-server';
|
|
15
15
|
import { Hono } from 'hono';
|
|
16
|
-
import
|
|
16
|
+
import { DatabaseSync, backup } from 'node:sqlite';
|
|
17
17
|
import { spawn, exec } from 'child_process';
|
|
18
18
|
import createClient from 'openapi-fetch';
|
|
19
19
|
import { createServer } from 'http';
|
|
@@ -219,6 +219,7 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
|
|
|
219
219
|
validateBindings(config.fs, "fs", configFile);
|
|
220
220
|
validateBindings(config.workflow, "workflow", configFile);
|
|
221
221
|
validateCronBindings(config.cron, configFile);
|
|
222
|
+
validateBindings(config.timer, "timer", configFile);
|
|
222
223
|
if (config.ai !== void 0 && typeof config.ai !== "boolean") {
|
|
223
224
|
throw new Error(`'ai' in ${configFile} must be a boolean`);
|
|
224
225
|
}
|
|
@@ -294,7 +295,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
|
|
|
294
295
|
return validatePloyConfig(config, configFile, validationOptions);
|
|
295
296
|
}
|
|
296
297
|
function hasBindings(config) {
|
|
297
|
-
return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.cron ?? config.ai ?? config.auth);
|
|
298
|
+
return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.cron ?? config.timer ?? config.ai ?? config.auth);
|
|
298
299
|
}
|
|
299
300
|
function parseDotEnv(content) {
|
|
300
301
|
const result = {};
|
|
@@ -1020,6 +1021,72 @@ export function initializeState(stateName: string, serviceUrl: string): StateBin
|
|
|
1020
1021
|
}
|
|
1021
1022
|
});
|
|
1022
1023
|
|
|
1024
|
+
// ../emulator/dist/runtime/timer-runtime.js
|
|
1025
|
+
var TIMER_RUNTIME_CODE;
|
|
1026
|
+
var init_timer_runtime = __esm({
|
|
1027
|
+
"../emulator/dist/runtime/timer-runtime.js"() {
|
|
1028
|
+
TIMER_RUNTIME_CODE = `
|
|
1029
|
+
export function initializeTimer(timerName, serviceUrl) {
|
|
1030
|
+
function parseOptions(options) {
|
|
1031
|
+
if (options === undefined || options === null) {
|
|
1032
|
+
return { payload: undefined, intervalMs: undefined };
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof options === "object" && options !== null && ("payload" in options || "intervalMs" in options)) {
|
|
1035
|
+
return { payload: options.payload, intervalMs: options.intervalMs };
|
|
1036
|
+
}
|
|
1037
|
+
return { payload: options, intervalMs: undefined };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
async set(id, scheduledTime, options) {
|
|
1042
|
+
const ts = typeof scheduledTime === "number" ? scheduledTime : scheduledTime.getTime();
|
|
1043
|
+
const { payload, intervalMs } = parseOptions(options);
|
|
1044
|
+
const response = await fetch(serviceUrl + "/timer/set", {
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
headers: { "Content-Type": "application/json" },
|
|
1047
|
+
body: JSON.stringify({ timerName, id, scheduledTime: ts, payload, intervalMs }),
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
if (!response.ok) {
|
|
1051
|
+
const errorText = await response.text();
|
|
1052
|
+
throw new Error("Timer set failed: " + errorText);
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
async get(id) {
|
|
1057
|
+
const response = await fetch(serviceUrl + "/timer/get", {
|
|
1058
|
+
method: "POST",
|
|
1059
|
+
headers: { "Content-Type": "application/json" },
|
|
1060
|
+
body: JSON.stringify({ timerName, id }),
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
if (!response.ok) {
|
|
1064
|
+
const errorText = await response.text();
|
|
1065
|
+
throw new Error("Timer get failed: " + errorText);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const result = await response.json();
|
|
1069
|
+
return result.timer ?? null;
|
|
1070
|
+
},
|
|
1071
|
+
|
|
1072
|
+
async delete(id) {
|
|
1073
|
+
const response = await fetch(serviceUrl + "/timer/delete", {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
headers: { "Content-Type": "application/json" },
|
|
1076
|
+
body: JSON.stringify({ timerName, id }),
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
const errorText = await response.text();
|
|
1081
|
+
throw new Error("Timer delete failed: " + errorText);
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
`;
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1023
1090
|
// ../emulator/dist/runtime/workflow-runtime.js
|
|
1024
1091
|
var WORKFLOW_RUNTIME_CODE;
|
|
1025
1092
|
var init_workflow_runtime = __esm({
|
|
@@ -1270,6 +1337,12 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1270
1337
|
bindings.push(` ${bindingName}: initializeWorkflow("${workflowName}", "${mockServiceUrl}"),`);
|
|
1271
1338
|
}
|
|
1272
1339
|
}
|
|
1340
|
+
if (config.timer) {
|
|
1341
|
+
imports.push('import { initializeTimer } from "__ploy_timer_runtime__";');
|
|
1342
|
+
for (const [bindingName, timerName] of Object.entries(config.timer)) {
|
|
1343
|
+
bindings.push(` ${bindingName}: initializeTimer("${timerName}", "${mockServiceUrl}"),`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1273
1346
|
imports.push('import userWorker from "__ploy_user_worker__";');
|
|
1274
1347
|
const workflowHandlerCode = config.workflow ? `
|
|
1275
1348
|
// Handle workflow execution requests
|
|
@@ -1329,6 +1402,32 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1329
1402
|
headers: { "Content-Type": "application/json" }
|
|
1330
1403
|
});
|
|
1331
1404
|
}` : "";
|
|
1405
|
+
const timerHandlerCode = config.timer ? `
|
|
1406
|
+
// Handle timer fire delivery
|
|
1407
|
+
if (request.headers.get("X-Ploy-Timer-Fire") === "true") {
|
|
1408
|
+
const timerId = request.headers.get("X-Ploy-Timer-Id");
|
|
1409
|
+
const timerName = request.headers.get("X-Ploy-Timer-Name");
|
|
1410
|
+
const timerTime = request.headers.get("X-Ploy-Timer-Time");
|
|
1411
|
+
if (timerId && userWorker.timer) {
|
|
1412
|
+
const body = await request.json().catch(() => ({}));
|
|
1413
|
+
try {
|
|
1414
|
+
await userWorker.timer({
|
|
1415
|
+
id: timerId,
|
|
1416
|
+
scheduledTime: parseInt(timerTime || "0", 10),
|
|
1417
|
+
payload: body.payload,
|
|
1418
|
+
intervalMs: body.intervalMs
|
|
1419
|
+
}, injectedEnv, ctx);
|
|
1420
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
1421
|
+
headers: { "Content-Type": "application/json" }
|
|
1422
|
+
});
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
return new Response(JSON.stringify({ success: false, error: String(error) }), {
|
|
1425
|
+
status: 500,
|
|
1426
|
+
headers: { "Content-Type": "application/json" }
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}` : "";
|
|
1332
1431
|
const queueHandlerCode = config.queue ? `
|
|
1333
1432
|
// Handle queue message delivery
|
|
1334
1433
|
if (request.headers.get("X-Ploy-Queue-Delivery") === "true") {
|
|
@@ -1373,6 +1472,7 @@ export default {
|
|
|
1373
1472
|
async fetch(request, env, ctx) {
|
|
1374
1473
|
const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
|
|
1375
1474
|
${cronHandlerCode}
|
|
1475
|
+
${timerHandlerCode}
|
|
1376
1476
|
${workflowHandlerCode}
|
|
1377
1477
|
${queueHandlerCode}
|
|
1378
1478
|
|
|
@@ -1420,6 +1520,10 @@ function createRuntimePlugin(_config) {
|
|
|
1420
1520
|
path: "__ploy_workflow_runtime__",
|
|
1421
1521
|
namespace: "ploy-runtime"
|
|
1422
1522
|
}));
|
|
1523
|
+
build2.onResolve({ filter: /^__ploy_timer_runtime__$/ }, () => ({
|
|
1524
|
+
path: "__ploy_timer_runtime__",
|
|
1525
|
+
namespace: "ploy-runtime"
|
|
1526
|
+
}));
|
|
1423
1527
|
build2.onLoad({ filter: /^__ploy_db_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1424
1528
|
contents: DB_RUNTIME_CODE,
|
|
1425
1529
|
loader: "ts"
|
|
@@ -1444,6 +1548,10 @@ function createRuntimePlugin(_config) {
|
|
|
1444
1548
|
contents: WORKFLOW_RUNTIME_CODE,
|
|
1445
1549
|
loader: "ts"
|
|
1446
1550
|
}));
|
|
1551
|
+
build2.onLoad({ filter: /^__ploy_timer_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1552
|
+
contents: TIMER_RUNTIME_CODE,
|
|
1553
|
+
loader: "ts"
|
|
1554
|
+
}));
|
|
1447
1555
|
}
|
|
1448
1556
|
};
|
|
1449
1557
|
}
|
|
@@ -1485,6 +1593,7 @@ var init_bundler = __esm({
|
|
|
1485
1593
|
init_fs_runtime();
|
|
1486
1594
|
init_queue_runtime();
|
|
1487
1595
|
init_state_runtime();
|
|
1596
|
+
init_timer_runtime();
|
|
1488
1597
|
init_workflow_runtime();
|
|
1489
1598
|
NODE_BUILTINS = [
|
|
1490
1599
|
"assert",
|
|
@@ -2257,6 +2366,7 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2257
2366
|
fs: config.fs,
|
|
2258
2367
|
workflow: config.workflow,
|
|
2259
2368
|
cron: config.cron,
|
|
2369
|
+
timer: config.timer,
|
|
2260
2370
|
auth: config.auth
|
|
2261
2371
|
});
|
|
2262
2372
|
});
|
|
@@ -2811,6 +2921,93 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2811
2921
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2812
2922
|
}
|
|
2813
2923
|
});
|
|
2924
|
+
app.get("/api/timer/:binding/entries", (c) => {
|
|
2925
|
+
const binding = c.req.param("binding");
|
|
2926
|
+
const timerName = config.timer?.[binding];
|
|
2927
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
2928
|
+
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
2929
|
+
if (!timerName) {
|
|
2930
|
+
return c.json({ error: "Timer binding not found" }, 404);
|
|
2931
|
+
}
|
|
2932
|
+
try {
|
|
2933
|
+
const db = dbManager2.emulatorDb;
|
|
2934
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM timer_entries WHERE timer_name = ?`).get(timerName).count;
|
|
2935
|
+
const entries = db.prepare(`SELECT id, scheduled_time, payload, interval_ms, status, created_at
|
|
2936
|
+
FROM timer_entries
|
|
2937
|
+
WHERE timer_name = ?
|
|
2938
|
+
ORDER BY scheduled_time DESC
|
|
2939
|
+
LIMIT ? OFFSET ?`).all(timerName, limit, offset);
|
|
2940
|
+
return c.json({
|
|
2941
|
+
entries: entries.map((e) => ({
|
|
2942
|
+
id: e.id,
|
|
2943
|
+
scheduledTime: new Date(e.scheduled_time * 1e3).toISOString(),
|
|
2944
|
+
payload: e.payload ? JSON.parse(e.payload) : null,
|
|
2945
|
+
intervalMs: e.interval_ms,
|
|
2946
|
+
status: e.status.toUpperCase(),
|
|
2947
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
2948
|
+
})),
|
|
2949
|
+
total,
|
|
2950
|
+
limit,
|
|
2951
|
+
offset
|
|
2952
|
+
});
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
app.get("/api/timer/:binding/metrics", (c) => {
|
|
2958
|
+
const binding = c.req.param("binding");
|
|
2959
|
+
const timerName = config.timer?.[binding];
|
|
2960
|
+
if (!timerName) {
|
|
2961
|
+
return c.json({ error: "Timer binding not found" }, 404);
|
|
2962
|
+
}
|
|
2963
|
+
try {
|
|
2964
|
+
const db = dbManager2.emulatorDb;
|
|
2965
|
+
const metrics = { pending: 0, fired: 0 };
|
|
2966
|
+
const statusCounts = db.prepare(`SELECT status, COUNT(*) as count
|
|
2967
|
+
FROM timer_entries
|
|
2968
|
+
WHERE timer_name = ?
|
|
2969
|
+
GROUP BY status`).all(timerName);
|
|
2970
|
+
for (const row of statusCounts) {
|
|
2971
|
+
if (row.status === "pending") {
|
|
2972
|
+
metrics.pending = row.count;
|
|
2973
|
+
} else if (row.status === "fired") {
|
|
2974
|
+
metrics.fired = row.count;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return c.json({ metrics });
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
app.get("/api/timer/:binding/executions", (c) => {
|
|
2983
|
+
const binding = c.req.param("binding");
|
|
2984
|
+
const timerName = config.timer?.[binding];
|
|
2985
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
2986
|
+
if (!timerName) {
|
|
2987
|
+
return c.json({ error: "Timer binding not found" }, 404);
|
|
2988
|
+
}
|
|
2989
|
+
try {
|
|
2990
|
+
const db = dbManager2.emulatorDb;
|
|
2991
|
+
const executions = db.prepare(`SELECT id, timer_id, scheduled_time, status, error, duration_ms, created_at
|
|
2992
|
+
FROM timer_executions
|
|
2993
|
+
WHERE timer_name = ?
|
|
2994
|
+
ORDER BY created_at DESC
|
|
2995
|
+
LIMIT ?`).all(timerName, limit);
|
|
2996
|
+
return c.json({
|
|
2997
|
+
executions: executions.map((e) => ({
|
|
2998
|
+
id: e.id,
|
|
2999
|
+
timerId: e.timer_id,
|
|
3000
|
+
scheduledTime: new Date(e.scheduled_time * 1e3).toISOString(),
|
|
3001
|
+
status: e.status.toUpperCase(),
|
|
3002
|
+
error: e.error,
|
|
3003
|
+
durationMs: e.duration_ms,
|
|
3004
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
3005
|
+
}))
|
|
3006
|
+
});
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
2814
3011
|
if (hasDashboard) {
|
|
2815
3012
|
app.get("/assets/*", (c) => {
|
|
2816
3013
|
const path = c.req.path;
|
|
@@ -2943,7 +3140,7 @@ function createDbHandler(getDatabase) {
|
|
|
2943
3140
|
return c.json(results);
|
|
2944
3141
|
}
|
|
2945
3142
|
if (method === "dump") {
|
|
2946
|
-
const buffer = db.
|
|
3143
|
+
const buffer = await db.dumpToBuffer();
|
|
2947
3144
|
return new Response(new Uint8Array(buffer), {
|
|
2948
3145
|
headers: {
|
|
2949
3146
|
"Content-Type": "application/octet-stream"
|
|
@@ -3299,6 +3496,135 @@ var init_state_service = __esm({
|
|
|
3299
3496
|
"../emulator/dist/services/state-service.js"() {
|
|
3300
3497
|
}
|
|
3301
3498
|
});
|
|
3499
|
+
|
|
3500
|
+
// ../emulator/dist/services/timer-service.js
|
|
3501
|
+
function createTimerHandlers(db) {
|
|
3502
|
+
const setHandler = async (c) => {
|
|
3503
|
+
try {
|
|
3504
|
+
const body = await c.req.json();
|
|
3505
|
+
const { timerName, id, scheduledTime, payload, intervalMs } = body;
|
|
3506
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3507
|
+
const scheduledTimeSec = Math.floor(scheduledTime / 1e3);
|
|
3508
|
+
db.prepare(`INSERT OR REPLACE INTO timer_entries (id, timer_name, scheduled_time, payload, interval_ms, status, created_at)
|
|
3509
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?)`).run(id, timerName, scheduledTimeSec, payload !== void 0 ? JSON.stringify(payload) : null, intervalMs ?? null, now);
|
|
3510
|
+
return c.json({ success: true });
|
|
3511
|
+
} catch (err) {
|
|
3512
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3513
|
+
return c.json({ success: false, error: message }, 500);
|
|
3514
|
+
}
|
|
3515
|
+
};
|
|
3516
|
+
const getHandler = async (c) => {
|
|
3517
|
+
try {
|
|
3518
|
+
const body = await c.req.json();
|
|
3519
|
+
const { timerName, id } = body;
|
|
3520
|
+
const row = db.prepare(`SELECT id, scheduled_time, payload, interval_ms FROM timer_entries
|
|
3521
|
+
WHERE timer_name = ? AND id = ? AND status = 'pending'`).get(timerName, id);
|
|
3522
|
+
if (!row) {
|
|
3523
|
+
return c.json({ timer: null });
|
|
3524
|
+
}
|
|
3525
|
+
return c.json({
|
|
3526
|
+
timer: {
|
|
3527
|
+
id: row.id,
|
|
3528
|
+
scheduledTime: row.scheduled_time * 1e3,
|
|
3529
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0,
|
|
3530
|
+
intervalMs: row.interval_ms ?? void 0
|
|
3531
|
+
}
|
|
3532
|
+
});
|
|
3533
|
+
} catch (err) {
|
|
3534
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3535
|
+
return c.json({ success: false, error: message }, 500);
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
const deleteHandler = async (c) => {
|
|
3539
|
+
try {
|
|
3540
|
+
const body = await c.req.json();
|
|
3541
|
+
const { timerName, id } = body;
|
|
3542
|
+
db.prepare(`DELETE FROM timer_entries WHERE timer_name = ? AND id = ?`).run(timerName, id);
|
|
3543
|
+
return c.json({ success: true });
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3546
|
+
return c.json({ success: false, error: message }, 500);
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
return {
|
|
3550
|
+
setHandler,
|
|
3551
|
+
getHandler,
|
|
3552
|
+
deleteHandler
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
3555
|
+
function createTimerProcessor(db, workerUrl) {
|
|
3556
|
+
let interval = null;
|
|
3557
|
+
async function processTimers() {
|
|
3558
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3559
|
+
const rows = db.prepare(`SELECT id, timer_name, scheduled_time, payload, interval_ms
|
|
3560
|
+
FROM timer_entries
|
|
3561
|
+
WHERE status = 'pending' AND scheduled_time <= ?
|
|
3562
|
+
ORDER BY scheduled_time ASC
|
|
3563
|
+
LIMIT 50`).all(now);
|
|
3564
|
+
for (const row of rows) {
|
|
3565
|
+
const startTime = Date.now();
|
|
3566
|
+
try {
|
|
3567
|
+
const response = await fetch(workerUrl, {
|
|
3568
|
+
method: "POST",
|
|
3569
|
+
headers: {
|
|
3570
|
+
"Content-Type": "application/json",
|
|
3571
|
+
"X-Ploy-Timer-Fire": "true",
|
|
3572
|
+
"X-Ploy-Timer-Id": row.id,
|
|
3573
|
+
"X-Ploy-Timer-Name": row.timer_name,
|
|
3574
|
+
"X-Ploy-Timer-Time": String(row.scheduled_time * 1e3)
|
|
3575
|
+
},
|
|
3576
|
+
body: JSON.stringify({
|
|
3577
|
+
scheduledTime: row.scheduled_time * 1e3,
|
|
3578
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0,
|
|
3579
|
+
intervalMs: row.interval_ms ?? void 0
|
|
3580
|
+
})
|
|
3581
|
+
});
|
|
3582
|
+
const durationMs = Date.now() - startTime;
|
|
3583
|
+
if (response.ok) {
|
|
3584
|
+
if (row.interval_ms) {
|
|
3585
|
+
const nextTimeSec = row.scheduled_time + Math.floor(row.interval_ms / 1e3);
|
|
3586
|
+
db.prepare(`UPDATE timer_entries SET scheduled_time = ? WHERE id = ? AND timer_name = ?`).run(nextTimeSec, row.id, row.timer_name);
|
|
3587
|
+
} else {
|
|
3588
|
+
db.prepare(`UPDATE timer_entries SET status = 'fired' WHERE id = ? AND timer_name = ?`).run(row.id, row.timer_name);
|
|
3589
|
+
}
|
|
3590
|
+
db.prepare(`INSERT INTO timer_executions (timer_id, timer_name, scheduled_time, status, duration_ms, created_at)
|
|
3591
|
+
VALUES (?, ?, ?, 'completed', ?, ?)`).run(row.id, row.timer_name, row.scheduled_time, durationMs, now);
|
|
3592
|
+
} else {
|
|
3593
|
+
db.prepare(`INSERT INTO timer_executions (timer_id, timer_name, scheduled_time, status, error, duration_ms, created_at)
|
|
3594
|
+
VALUES (?, ?, ?, 'failed', ?, ?, ?)`).run(row.id, row.timer_name, row.scheduled_time, `HTTP ${String(response.status)}`, durationMs, now);
|
|
3595
|
+
db.prepare(`UPDATE timer_entries SET scheduled_time = ? WHERE id = ? AND timer_name = ?`).run(now + 5, row.id, row.timer_name);
|
|
3596
|
+
}
|
|
3597
|
+
} catch (err) {
|
|
3598
|
+
const durationMs = Date.now() - startTime;
|
|
3599
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3600
|
+
db.prepare(`INSERT INTO timer_executions (timer_id, timer_name, scheduled_time, status, error, duration_ms, created_at)
|
|
3601
|
+
VALUES (?, ?, ?, 'failed', ?, ?, ?)`).run(row.id, row.timer_name, row.scheduled_time, errorMessage, durationMs, now);
|
|
3602
|
+
db.prepare(`UPDATE timer_entries SET scheduled_time = ? WHERE id = ? AND timer_name = ?`).run(now + 5, row.id, row.timer_name);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
return {
|
|
3607
|
+
start() {
|
|
3608
|
+
if (interval) {
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
interval = setInterval(() => {
|
|
3612
|
+
processTimers().catch(() => {
|
|
3613
|
+
});
|
|
3614
|
+
}, 1e3);
|
|
3615
|
+
},
|
|
3616
|
+
stop() {
|
|
3617
|
+
if (interval) {
|
|
3618
|
+
clearInterval(interval);
|
|
3619
|
+
interval = null;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
var init_timer_service = __esm({
|
|
3625
|
+
"../emulator/dist/services/timer-service.js"() {
|
|
3626
|
+
}
|
|
3627
|
+
});
|
|
3302
3628
|
function createWorkflowHandlers(db, workerUrl) {
|
|
3303
3629
|
const triggerHandler = async (c) => {
|
|
3304
3630
|
try {
|
|
@@ -3521,6 +3847,12 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3521
3847
|
app.post("/fs/delete", fsHandlers.deleteHandler);
|
|
3522
3848
|
app.post("/fs/list", fsHandlers.listHandler);
|
|
3523
3849
|
}
|
|
3850
|
+
if (config.timer) {
|
|
3851
|
+
const timerHandlers = createTimerHandlers(dbManager2.emulatorDb);
|
|
3852
|
+
app.post("/timer/set", timerHandlers.setHandler);
|
|
3853
|
+
app.post("/timer/get", timerHandlers.getHandler);
|
|
3854
|
+
app.post("/timer/delete", timerHandlers.deleteHandler);
|
|
3855
|
+
}
|
|
3524
3856
|
if (config.auth) {
|
|
3525
3857
|
const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
|
|
3526
3858
|
app.post("/auth/signup", authHandlers.signupHandler);
|
|
@@ -3560,20 +3892,82 @@ var init_mock_server = __esm({
|
|
|
3560
3892
|
init_fs_service();
|
|
3561
3893
|
init_queue_service();
|
|
3562
3894
|
init_state_service();
|
|
3895
|
+
init_timer_service();
|
|
3563
3896
|
init_workflow_service();
|
|
3564
3897
|
DEFAULT_MOCK_SERVER_PORT = 4003;
|
|
3565
3898
|
}
|
|
3566
3899
|
});
|
|
3900
|
+
function openDatabase(filepath) {
|
|
3901
|
+
const db = new DatabaseSync(filepath);
|
|
3902
|
+
return {
|
|
3903
|
+
prepare(sql) {
|
|
3904
|
+
const stmt = db.prepare(sql);
|
|
3905
|
+
return {
|
|
3906
|
+
run(...params) {
|
|
3907
|
+
const result = stmt.run(...params);
|
|
3908
|
+
return {
|
|
3909
|
+
changes: Number(result.changes),
|
|
3910
|
+
lastInsertRowid: result.lastInsertRowid
|
|
3911
|
+
};
|
|
3912
|
+
},
|
|
3913
|
+
get(...params) {
|
|
3914
|
+
return stmt.get(...params);
|
|
3915
|
+
},
|
|
3916
|
+
all(...params) {
|
|
3917
|
+
return stmt.all(...params);
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
},
|
|
3921
|
+
exec(sql) {
|
|
3922
|
+
db.exec(sql);
|
|
3923
|
+
},
|
|
3924
|
+
close() {
|
|
3925
|
+
db.close();
|
|
3926
|
+
},
|
|
3927
|
+
pragma(pragma) {
|
|
3928
|
+
db.exec(`PRAGMA ${pragma}`);
|
|
3929
|
+
},
|
|
3930
|
+
transaction(fn) {
|
|
3931
|
+
return () => {
|
|
3932
|
+
db.exec("BEGIN");
|
|
3933
|
+
try {
|
|
3934
|
+
const result = fn();
|
|
3935
|
+
db.exec("COMMIT");
|
|
3936
|
+
return result;
|
|
3937
|
+
} catch (e) {
|
|
3938
|
+
db.exec("ROLLBACK");
|
|
3939
|
+
throw e;
|
|
3940
|
+
}
|
|
3941
|
+
};
|
|
3942
|
+
},
|
|
3943
|
+
async dumpToBuffer() {
|
|
3944
|
+
const tempPath = join(tmpdir(), `ploy-dump-${randomUUID()}.db`);
|
|
3945
|
+
try {
|
|
3946
|
+
await backup(db, tempPath);
|
|
3947
|
+
return readFileSync(tempPath);
|
|
3948
|
+
} finally {
|
|
3949
|
+
try {
|
|
3950
|
+
unlinkSync(tempPath);
|
|
3951
|
+
} catch {
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
var init_sqlite_db = __esm({
|
|
3958
|
+
"../emulator/dist/utils/sqlite-db.js"() {
|
|
3959
|
+
}
|
|
3960
|
+
});
|
|
3567
3961
|
function initializeDatabases(projectDir) {
|
|
3568
3962
|
const dataDir = ensureDataDir(projectDir);
|
|
3569
3963
|
const d1Databases = /* @__PURE__ */ new Map();
|
|
3570
|
-
const emulatorDb =
|
|
3964
|
+
const emulatorDb = openDatabase(join(dataDir, "emulator.db"));
|
|
3571
3965
|
emulatorDb.pragma("journal_mode = WAL");
|
|
3572
3966
|
emulatorDb.exec(EMULATOR_SCHEMA);
|
|
3573
3967
|
function getD1Database(bindingName) {
|
|
3574
3968
|
let db = d1Databases.get(bindingName);
|
|
3575
3969
|
if (!db) {
|
|
3576
|
-
db =
|
|
3970
|
+
db = openDatabase(join(dataDir, "db", `${bindingName}.db`));
|
|
3577
3971
|
db.pragma("journal_mode = WAL");
|
|
3578
3972
|
d1Databases.set(bindingName, db);
|
|
3579
3973
|
}
|
|
@@ -3596,6 +3990,7 @@ var EMULATOR_SCHEMA;
|
|
|
3596
3990
|
var init_sqlite = __esm({
|
|
3597
3991
|
"../emulator/dist/utils/sqlite.js"() {
|
|
3598
3992
|
init_paths();
|
|
3993
|
+
init_sqlite_db();
|
|
3599
3994
|
EMULATOR_SCHEMA = `
|
|
3600
3995
|
-- Queue messages table
|
|
3601
3996
|
CREATE TABLE IF NOT EXISTS queue_messages (
|
|
@@ -3731,6 +4126,36 @@ CREATE TABLE IF NOT EXISTS cron_executions (
|
|
|
3731
4126
|
|
|
3732
4127
|
CREATE INDEX IF NOT EXISTS idx_cron_executions_name
|
|
3733
4128
|
ON cron_executions(cron_name, triggered_at);
|
|
4129
|
+
|
|
4130
|
+
-- Timer entries table (durable timers, optionally recurring)
|
|
4131
|
+
CREATE TABLE IF NOT EXISTS timer_entries (
|
|
4132
|
+
id TEXT NOT NULL,
|
|
4133
|
+
timer_name TEXT NOT NULL,
|
|
4134
|
+
scheduled_time INTEGER NOT NULL,
|
|
4135
|
+
payload TEXT,
|
|
4136
|
+
interval_ms INTEGER,
|
|
4137
|
+
status TEXT DEFAULT 'pending',
|
|
4138
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
4139
|
+
PRIMARY KEY (timer_name, id)
|
|
4140
|
+
);
|
|
4141
|
+
|
|
4142
|
+
CREATE INDEX IF NOT EXISTS idx_timer_entries_status_time
|
|
4143
|
+
ON timer_entries(status, scheduled_time);
|
|
4144
|
+
|
|
4145
|
+
-- Timer execution log
|
|
4146
|
+
CREATE TABLE IF NOT EXISTS timer_executions (
|
|
4147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4148
|
+
timer_id TEXT NOT NULL,
|
|
4149
|
+
timer_name TEXT NOT NULL,
|
|
4150
|
+
scheduled_time INTEGER NOT NULL,
|
|
4151
|
+
status TEXT DEFAULT 'pending',
|
|
4152
|
+
error TEXT,
|
|
4153
|
+
duration_ms INTEGER,
|
|
4154
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
4155
|
+
);
|
|
4156
|
+
|
|
4157
|
+
CREATE INDEX IF NOT EXISTS idx_timer_executions_name
|
|
4158
|
+
ON timer_executions(timer_name, created_at);
|
|
3734
4159
|
`;
|
|
3735
4160
|
}
|
|
3736
4161
|
});
|
|
@@ -3750,6 +4175,7 @@ var init_emulator = __esm({
|
|
|
3750
4175
|
init_cron_service();
|
|
3751
4176
|
init_mock_server();
|
|
3752
4177
|
init_queue_service();
|
|
4178
|
+
init_timer_service();
|
|
3753
4179
|
init_logger();
|
|
3754
4180
|
init_paths();
|
|
3755
4181
|
init_sqlite();
|
|
@@ -3764,6 +4190,7 @@ var init_emulator = __esm({
|
|
|
3764
4190
|
fileWatcher = null;
|
|
3765
4191
|
queueProcessor = null;
|
|
3766
4192
|
cronScheduler = null;
|
|
4193
|
+
timerProcessor = null;
|
|
3767
4194
|
resolvedEnvVars = {};
|
|
3768
4195
|
constructor(options = {}) {
|
|
3769
4196
|
const port = options.port ?? 8787;
|
|
@@ -3837,6 +4264,12 @@ var init_emulator = __esm({
|
|
|
3837
4264
|
this.cronScheduler.start();
|
|
3838
4265
|
debug("Cron scheduler started", this.options.verbose);
|
|
3839
4266
|
}
|
|
4267
|
+
if (this.config.timer) {
|
|
4268
|
+
const workerUrl2 = `http://${this.options.host}:${String(this.options.port)}`;
|
|
4269
|
+
this.timerProcessor = createTimerProcessor(this.dbManager.emulatorDb, workerUrl2);
|
|
4270
|
+
this.timerProcessor.start();
|
|
4271
|
+
debug("Timer processor started", this.options.verbose);
|
|
4272
|
+
}
|
|
3840
4273
|
success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
|
|
3841
4274
|
log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
|
|
3842
4275
|
if (Object.keys(this.resolvedEnvVars).length > 0) {
|
|
@@ -3862,6 +4295,9 @@ var init_emulator = __esm({
|
|
|
3862
4295
|
log(` Cron: ${name} = ${expr}`);
|
|
3863
4296
|
}
|
|
3864
4297
|
}
|
|
4298
|
+
if (this.config.timer) {
|
|
4299
|
+
log(` Timer bindings: ${Object.keys(this.config.timer).join(", ")}`);
|
|
4300
|
+
}
|
|
3865
4301
|
this.setupSignalHandlers();
|
|
3866
4302
|
} catch (err) {
|
|
3867
4303
|
error(`Failed to start emulator: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3990,6 +4426,10 @@ var init_emulator = __esm({
|
|
|
3990
4426
|
this.cronScheduler.stop();
|
|
3991
4427
|
this.cronScheduler = null;
|
|
3992
4428
|
}
|
|
4429
|
+
if (this.timerProcessor) {
|
|
4430
|
+
this.timerProcessor.stop();
|
|
4431
|
+
this.timerProcessor = null;
|
|
4432
|
+
}
|
|
3993
4433
|
if (this.queueProcessor) {
|
|
3994
4434
|
this.queueProcessor.stop();
|
|
3995
4435
|
this.queueProcessor = null;
|
|
@@ -5885,6 +6325,24 @@ ${varProps}
|
|
|
5885
6325
|
properties.push(` ${bindingName}: WorkflowBinding;`);
|
|
5886
6326
|
}
|
|
5887
6327
|
}
|
|
6328
|
+
if (config.fs) {
|
|
6329
|
+
const fsKeys = Object.keys(config.fs);
|
|
6330
|
+
if (fsKeys.length > 0) {
|
|
6331
|
+
imports.push("FileStorageBinding");
|
|
6332
|
+
for (const bindingName of fsKeys) {
|
|
6333
|
+
properties.push(` ${bindingName}: FileStorageBinding;`);
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
if (config.timer) {
|
|
6338
|
+
const timerKeys = Object.keys(config.timer);
|
|
6339
|
+
if (timerKeys.length > 0) {
|
|
6340
|
+
imports.push("TimerBinding");
|
|
6341
|
+
for (const bindingName of timerKeys) {
|
|
6342
|
+
properties.push(` ${bindingName}: TimerBinding;`);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
5888
6346
|
const lines = [
|
|
5889
6347
|
"// This file is auto-generated by `ploy types`. Do not edit manually.",
|
|
5890
6348
|
""
|
|
@@ -5926,7 +6384,7 @@ async function typesCommand(options = {}) {
|
|
|
5926
6384
|
console.error("Error: ploy.yaml not found in current directory");
|
|
5927
6385
|
process.exit(1);
|
|
5928
6386
|
}
|
|
5929
|
-
const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow || config.cron;
|
|
6387
|
+
const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow || config.fs || config.timer || config.cron;
|
|
5930
6388
|
if (!hasBindings2) {
|
|
5931
6389
|
console.log("No bindings found in ploy.yaml. Generating empty Env.");
|
|
5932
6390
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetploy/cli",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@hono/node-server": "^1.13.0",
|
|
39
|
-
"better-sqlite3": "^11.8.1",
|
|
40
39
|
"chokidar": "^4.0.0",
|
|
41
40
|
"esbuild": "^0.24.0",
|
|
42
41
|
"hono": "^4.7.0",
|
|
@@ -48,7 +47,6 @@
|
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@meetploy/emulator": "*",
|
|
50
49
|
"@meetploy/tools": "*",
|
|
51
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
52
50
|
"api": "*",
|
|
53
51
|
"openapi-typescript": "7.8.0",
|
|
54
52
|
"tsup": "^8.0.0"
|