@meetploy/cli 1.16.0 → 1.17.1
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-Ch3tPHX5.js +354 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/dev.js +228 -1
- package/dist/index.js +684 -3
- package/package.json +1 -1
- package/dist/dashboard-dist/assets/main-duAiLjPq.js +0 -339
package/dist/index.js
CHANGED
|
@@ -147,6 +147,22 @@ function validateBindings(bindings, bindingType, configFile) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
function validateCronBindings(bindings, configFile) {
|
|
151
|
+
if (bindings === void 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const [bindingName, cronExpression] of Object.entries(bindings)) {
|
|
155
|
+
if (!BINDING_NAME_REGEX.test(bindingName)) {
|
|
156
|
+
throw new Error(`Invalid cron binding name '${bindingName}' in ${configFile}. Binding names must be uppercase with underscores (e.g., HOURLY_CLEANUP, DAILY_REPORT)`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof cronExpression !== "string") {
|
|
159
|
+
throw new Error(`Cron binding '${bindingName}' in ${configFile} must have a string value (cron expression)`);
|
|
160
|
+
}
|
|
161
|
+
if (!CRON_EXPRESSION_REGEX.test(cronExpression.trim())) {
|
|
162
|
+
throw new Error(`Invalid cron expression '${cronExpression}' for binding '${bindingName}' in ${configFile}. Must be a valid cron expression with 5 fields (e.g., "0 * * * *", "*/5 * * * *")`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
150
166
|
function validateRelativePath(path, fieldName, configFile) {
|
|
151
167
|
if (path === void 0) {
|
|
152
168
|
return void 0;
|
|
@@ -202,6 +218,8 @@ function validatePloyConfig(config, configFile = "ploy.yaml", options = {}) {
|
|
|
202
218
|
validateBindings(config.state, "state", configFile);
|
|
203
219
|
validateBindings(config.fs, "fs", configFile);
|
|
204
220
|
validateBindings(config.workflow, "workflow", configFile);
|
|
221
|
+
validateCronBindings(config.cron, configFile);
|
|
222
|
+
validateBindings(config.alarm, "alarm", configFile);
|
|
205
223
|
if (config.ai !== void 0 && typeof config.ai !== "boolean") {
|
|
206
224
|
throw new Error(`'ai' in ${configFile} must be a boolean`);
|
|
207
225
|
}
|
|
@@ -277,7 +295,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
|
|
|
277
295
|
return validatePloyConfig(config, configFile, validationOptions);
|
|
278
296
|
}
|
|
279
297
|
function hasBindings(config) {
|
|
280
|
-
return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.ai ?? config.auth);
|
|
298
|
+
return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.cron ?? config.alarm ?? config.ai ?? config.auth);
|
|
281
299
|
}
|
|
282
300
|
function parseDotEnv(content) {
|
|
283
301
|
const result = {};
|
|
@@ -339,7 +357,7 @@ function getWorkerEntryPoint(projectDir, config) {
|
|
|
339
357
|
}
|
|
340
358
|
throw new Error("Could not find worker entry point. Specify 'main' in ploy.yaml");
|
|
341
359
|
}
|
|
342
|
-
var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX;
|
|
360
|
+
var readFileAsync, DEFAULT_COMPATIBILITY_FLAGS, DEFAULT_COMPATIBILITY_DATE, BINDING_NAME_REGEX, RESOURCE_NAME_REGEX, CRON_EXPRESSION_REGEX;
|
|
343
361
|
var init_ploy_config = __esm({
|
|
344
362
|
"../tools/dist/ploy-config.js"() {
|
|
345
363
|
readFileAsync = promisify(readFile$1);
|
|
@@ -347,6 +365,7 @@ var init_ploy_config = __esm({
|
|
|
347
365
|
DEFAULT_COMPATIBILITY_DATE = "2025-04-02";
|
|
348
366
|
BINDING_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
|
|
349
367
|
RESOURCE_NAME_REGEX = /^[a-z][a-z0-9_]*$/;
|
|
368
|
+
CRON_EXPRESSION_REGEX = /^(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)\s+(\*|[0-9,\-/]+)$/;
|
|
350
369
|
}
|
|
351
370
|
});
|
|
352
371
|
|
|
@@ -379,6 +398,72 @@ var init_cli = __esm({
|
|
|
379
398
|
}
|
|
380
399
|
});
|
|
381
400
|
|
|
401
|
+
// ../emulator/dist/runtime/alarm-runtime.js
|
|
402
|
+
var ALARM_RUNTIME_CODE;
|
|
403
|
+
var init_alarm_runtime = __esm({
|
|
404
|
+
"../emulator/dist/runtime/alarm-runtime.js"() {
|
|
405
|
+
ALARM_RUNTIME_CODE = `
|
|
406
|
+
export function initializeAlarm(alarmName, serviceUrl) {
|
|
407
|
+
function parseOptions(options) {
|
|
408
|
+
if (options === undefined || options === null) {
|
|
409
|
+
return { payload: undefined, intervalMs: undefined };
|
|
410
|
+
}
|
|
411
|
+
if (typeof options === "object" && options !== null && ("payload" in options || "intervalMs" in options)) {
|
|
412
|
+
return { payload: options.payload, intervalMs: options.intervalMs };
|
|
413
|
+
}
|
|
414
|
+
return { payload: options, intervalMs: undefined };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
async set(id, scheduledTime, options) {
|
|
419
|
+
const ts = typeof scheduledTime === "number" ? scheduledTime : scheduledTime.getTime();
|
|
420
|
+
const { payload, intervalMs } = parseOptions(options);
|
|
421
|
+
const response = await fetch(serviceUrl + "/alarm/set", {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/json" },
|
|
424
|
+
body: JSON.stringify({ alarmName, id, scheduledTime: ts, payload, intervalMs }),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
const errorText = await response.text();
|
|
429
|
+
throw new Error("Alarm set failed: " + errorText);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
async get(id) {
|
|
434
|
+
const response = await fetch(serviceUrl + "/alarm/get", {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: { "Content-Type": "application/json" },
|
|
437
|
+
body: JSON.stringify({ alarmName, id }),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
const errorText = await response.text();
|
|
442
|
+
throw new Error("Alarm get failed: " + errorText);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const result = await response.json();
|
|
446
|
+
return result.alarm ?? null;
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
async delete(id) {
|
|
450
|
+
const response = await fetch(serviceUrl + "/alarm/delete", {
|
|
451
|
+
method: "POST",
|
|
452
|
+
headers: { "Content-Type": "application/json" },
|
|
453
|
+
body: JSON.stringify({ alarmName, id }),
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (!response.ok) {
|
|
457
|
+
const errorText = await response.text();
|
|
458
|
+
throw new Error("Alarm delete failed: " + errorText);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
382
467
|
// ../emulator/dist/runtime/cache-runtime.js
|
|
383
468
|
var CACHE_RUNTIME_CODE;
|
|
384
469
|
var init_cache_runtime = __esm({
|
|
@@ -1252,6 +1337,12 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1252
1337
|
bindings.push(` ${bindingName}: initializeWorkflow("${workflowName}", "${mockServiceUrl}"),`);
|
|
1253
1338
|
}
|
|
1254
1339
|
}
|
|
1340
|
+
if (config.alarm) {
|
|
1341
|
+
imports.push('import { initializeAlarm } from "__ploy_alarm_runtime__";');
|
|
1342
|
+
for (const [bindingName, alarmName] of Object.entries(config.alarm)) {
|
|
1343
|
+
bindings.push(` ${bindingName}: initializeAlarm("${alarmName}", "${mockServiceUrl}"),`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1255
1346
|
imports.push('import userWorker from "__ploy_user_worker__";');
|
|
1256
1347
|
const workflowHandlerCode = config.workflow ? `
|
|
1257
1348
|
// Handle workflow execution requests
|
|
@@ -1280,6 +1371,63 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
|
|
|
1280
1371
|
}
|
|
1281
1372
|
}
|
|
1282
1373
|
}` : "";
|
|
1374
|
+
const cronHandlerCode = config.cron ? `
|
|
1375
|
+
// Handle cron trigger delivery
|
|
1376
|
+
if (request.headers.get("X-Ploy-Cron-Trigger") === "true") {
|
|
1377
|
+
const cronExpression = request.headers.get("X-Ploy-Cron-Expression");
|
|
1378
|
+
const scheduledTime = parseInt(request.headers.get("X-Ploy-Cron-Scheduled-Time") || String(Date.now()), 10);
|
|
1379
|
+
|
|
1380
|
+
if (cronExpression && userWorker.scheduled) {
|
|
1381
|
+
let noRetryFlag = false;
|
|
1382
|
+
const event = {
|
|
1383
|
+
cron: cronExpression,
|
|
1384
|
+
scheduledTime,
|
|
1385
|
+
noRetry() { noRetryFlag = true; }
|
|
1386
|
+
};
|
|
1387
|
+
try {
|
|
1388
|
+
await userWorker.scheduled(event, injectedEnv, ctx);
|
|
1389
|
+
return new Response(JSON.stringify({ success: true, noRetry: noRetryFlag }), {
|
|
1390
|
+
headers: { "Content-Type": "application/json" }
|
|
1391
|
+
});
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
return new Response(JSON.stringify({ success: false, error: String(error) }), {
|
|
1394
|
+
status: 500,
|
|
1395
|
+
headers: { "Content-Type": "application/json" }
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return new Response(JSON.stringify({ success: false, error: "No scheduled handler defined" }), {
|
|
1401
|
+
status: 404,
|
|
1402
|
+
headers: { "Content-Type": "application/json" }
|
|
1403
|
+
});
|
|
1404
|
+
}` : "";
|
|
1405
|
+
const alarmHandlerCode = config.alarm ? `
|
|
1406
|
+
// Handle alarm fire delivery
|
|
1407
|
+
if (request.headers.get("X-Ploy-Alarm-Fire") === "true") {
|
|
1408
|
+
const alarmId = request.headers.get("X-Ploy-Alarm-Id");
|
|
1409
|
+
const alarmName = request.headers.get("X-Ploy-Alarm-Name");
|
|
1410
|
+
const alarmTime = request.headers.get("X-Ploy-Alarm-Time");
|
|
1411
|
+
if (alarmId && userWorker.alarm) {
|
|
1412
|
+
const body = await request.json().catch(() => ({}));
|
|
1413
|
+
try {
|
|
1414
|
+
await userWorker.alarm({
|
|
1415
|
+
id: alarmId,
|
|
1416
|
+
scheduledTime: parseInt(alarmTime || "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
|
+
}` : "";
|
|
1283
1431
|
const queueHandlerCode = config.queue ? `
|
|
1284
1432
|
// Handle queue message delivery
|
|
1285
1433
|
if (request.headers.get("X-Ploy-Queue-Delivery") === "true") {
|
|
@@ -1323,6 +1471,8 @@ ${bindings.join("\n")}
|
|
|
1323
1471
|
export default {
|
|
1324
1472
|
async fetch(request, env, ctx) {
|
|
1325
1473
|
const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
|
|
1474
|
+
${cronHandlerCode}
|
|
1475
|
+
${alarmHandlerCode}
|
|
1326
1476
|
${workflowHandlerCode}
|
|
1327
1477
|
${queueHandlerCode}
|
|
1328
1478
|
|
|
@@ -1370,6 +1520,10 @@ function createRuntimePlugin(_config) {
|
|
|
1370
1520
|
path: "__ploy_workflow_runtime__",
|
|
1371
1521
|
namespace: "ploy-runtime"
|
|
1372
1522
|
}));
|
|
1523
|
+
build2.onResolve({ filter: /^__ploy_alarm_runtime__$/ }, () => ({
|
|
1524
|
+
path: "__ploy_alarm_runtime__",
|
|
1525
|
+
namespace: "ploy-runtime"
|
|
1526
|
+
}));
|
|
1373
1527
|
build2.onLoad({ filter: /^__ploy_db_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1374
1528
|
contents: DB_RUNTIME_CODE,
|
|
1375
1529
|
loader: "ts"
|
|
@@ -1394,6 +1548,10 @@ function createRuntimePlugin(_config) {
|
|
|
1394
1548
|
contents: WORKFLOW_RUNTIME_CODE,
|
|
1395
1549
|
loader: "ts"
|
|
1396
1550
|
}));
|
|
1551
|
+
build2.onLoad({ filter: /^__ploy_alarm_runtime__$/, namespace: "ploy-runtime" }, () => ({
|
|
1552
|
+
contents: ALARM_RUNTIME_CODE,
|
|
1553
|
+
loader: "ts"
|
|
1554
|
+
}));
|
|
1397
1555
|
}
|
|
1398
1556
|
};
|
|
1399
1557
|
}
|
|
@@ -1430,6 +1588,7 @@ async function bundleWorker(options) {
|
|
|
1430
1588
|
var NODE_BUILTINS;
|
|
1431
1589
|
var init_bundler = __esm({
|
|
1432
1590
|
"../emulator/dist/bundler/bundler.js"() {
|
|
1591
|
+
init_alarm_runtime();
|
|
1433
1592
|
init_cache_runtime();
|
|
1434
1593
|
init_db_runtime();
|
|
1435
1594
|
init_fs_runtime();
|
|
@@ -1693,6 +1852,306 @@ var init_workerd_config = __esm({
|
|
|
1693
1852
|
"../emulator/dist/config/workerd-config.js"() {
|
|
1694
1853
|
}
|
|
1695
1854
|
});
|
|
1855
|
+
|
|
1856
|
+
// ../emulator/dist/services/alarm-service.js
|
|
1857
|
+
function createAlarmHandlers(db) {
|
|
1858
|
+
const setHandler = async (c) => {
|
|
1859
|
+
try {
|
|
1860
|
+
const body = await c.req.json();
|
|
1861
|
+
const { alarmName, id, scheduledTime, payload, intervalMs } = body;
|
|
1862
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1863
|
+
const scheduledTimeSec = Math.floor(scheduledTime / 1e3);
|
|
1864
|
+
db.prepare(`INSERT OR REPLACE INTO alarm_entries (id, alarm_name, scheduled_time, payload, interval_ms, status, created_at)
|
|
1865
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?)`).run(id, alarmName, scheduledTimeSec, payload !== void 0 ? JSON.stringify(payload) : null, intervalMs ?? null, now);
|
|
1866
|
+
return c.json({ success: true });
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1869
|
+
return c.json({ success: false, error: message }, 500);
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
const getHandler = async (c) => {
|
|
1873
|
+
try {
|
|
1874
|
+
const body = await c.req.json();
|
|
1875
|
+
const { alarmName, id } = body;
|
|
1876
|
+
const row = db.prepare(`SELECT id, scheduled_time, payload, interval_ms FROM alarm_entries
|
|
1877
|
+
WHERE alarm_name = ? AND id = ? AND status = 'pending'`).get(alarmName, id);
|
|
1878
|
+
if (!row) {
|
|
1879
|
+
return c.json({ alarm: null });
|
|
1880
|
+
}
|
|
1881
|
+
return c.json({
|
|
1882
|
+
alarm: {
|
|
1883
|
+
id: row.id,
|
|
1884
|
+
scheduledTime: row.scheduled_time * 1e3,
|
|
1885
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0,
|
|
1886
|
+
intervalMs: row.interval_ms ?? void 0
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1891
|
+
return c.json({ success: false, error: message }, 500);
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const deleteHandler = async (c) => {
|
|
1895
|
+
try {
|
|
1896
|
+
const body = await c.req.json();
|
|
1897
|
+
const { alarmName, id } = body;
|
|
1898
|
+
db.prepare(`DELETE FROM alarm_entries WHERE alarm_name = ? AND id = ?`).run(alarmName, id);
|
|
1899
|
+
return c.json({ success: true });
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1902
|
+
return c.json({ success: false, error: message }, 500);
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
return {
|
|
1906
|
+
setHandler,
|
|
1907
|
+
getHandler,
|
|
1908
|
+
deleteHandler
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
function createAlarmProcessor(db, workerUrl) {
|
|
1912
|
+
let interval = null;
|
|
1913
|
+
async function processAlarms() {
|
|
1914
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1915
|
+
const rows = db.prepare(`SELECT id, alarm_name, scheduled_time, payload, interval_ms
|
|
1916
|
+
FROM alarm_entries
|
|
1917
|
+
WHERE status = 'pending' AND scheduled_time <= ?
|
|
1918
|
+
ORDER BY scheduled_time ASC
|
|
1919
|
+
LIMIT 50`).all(now);
|
|
1920
|
+
for (const row of rows) {
|
|
1921
|
+
const startTime = Date.now();
|
|
1922
|
+
try {
|
|
1923
|
+
const response = await fetch(workerUrl, {
|
|
1924
|
+
method: "POST",
|
|
1925
|
+
headers: {
|
|
1926
|
+
"Content-Type": "application/json",
|
|
1927
|
+
"X-Ploy-Alarm-Fire": "true",
|
|
1928
|
+
"X-Ploy-Alarm-Id": row.id,
|
|
1929
|
+
"X-Ploy-Alarm-Name": row.alarm_name,
|
|
1930
|
+
"X-Ploy-Alarm-Time": String(row.scheduled_time * 1e3)
|
|
1931
|
+
},
|
|
1932
|
+
body: JSON.stringify({
|
|
1933
|
+
scheduledTime: row.scheduled_time * 1e3,
|
|
1934
|
+
payload: row.payload ? JSON.parse(row.payload) : void 0,
|
|
1935
|
+
intervalMs: row.interval_ms ?? void 0
|
|
1936
|
+
})
|
|
1937
|
+
});
|
|
1938
|
+
const durationMs = Date.now() - startTime;
|
|
1939
|
+
if (response.ok) {
|
|
1940
|
+
if (row.interval_ms) {
|
|
1941
|
+
const nextTimeSec = row.scheduled_time + Math.floor(row.interval_ms / 1e3);
|
|
1942
|
+
db.prepare(`UPDATE alarm_entries SET scheduled_time = ? WHERE id = ? AND alarm_name = ?`).run(nextTimeSec, row.id, row.alarm_name);
|
|
1943
|
+
} else {
|
|
1944
|
+
db.prepare(`UPDATE alarm_entries SET status = 'fired' WHERE id = ? AND alarm_name = ?`).run(row.id, row.alarm_name);
|
|
1945
|
+
}
|
|
1946
|
+
db.prepare(`INSERT INTO alarm_executions (alarm_id, alarm_name, scheduled_time, status, duration_ms, created_at)
|
|
1947
|
+
VALUES (?, ?, ?, 'completed', ?, ?)`).run(row.id, row.alarm_name, row.scheduled_time, durationMs, now);
|
|
1948
|
+
} else {
|
|
1949
|
+
db.prepare(`INSERT INTO alarm_executions (alarm_id, alarm_name, scheduled_time, status, error, duration_ms, created_at)
|
|
1950
|
+
VALUES (?, ?, ?, 'failed', ?, ?, ?)`).run(row.id, row.alarm_name, row.scheduled_time, `HTTP ${String(response.status)}`, durationMs, now);
|
|
1951
|
+
db.prepare(`UPDATE alarm_entries SET scheduled_time = ? WHERE id = ? AND alarm_name = ?`).run(now + 5, row.id, row.alarm_name);
|
|
1952
|
+
}
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
const durationMs = Date.now() - startTime;
|
|
1955
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1956
|
+
db.prepare(`INSERT INTO alarm_executions (alarm_id, alarm_name, scheduled_time, status, error, duration_ms, created_at)
|
|
1957
|
+
VALUES (?, ?, ?, 'failed', ?, ?, ?)`).run(row.id, row.alarm_name, row.scheduled_time, errorMessage, durationMs, now);
|
|
1958
|
+
db.prepare(`UPDATE alarm_entries SET scheduled_time = ? WHERE id = ? AND alarm_name = ?`).run(now + 5, row.id, row.alarm_name);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return {
|
|
1963
|
+
start() {
|
|
1964
|
+
if (interval) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
interval = setInterval(() => {
|
|
1968
|
+
processAlarms().catch(() => {
|
|
1969
|
+
});
|
|
1970
|
+
}, 1e3);
|
|
1971
|
+
},
|
|
1972
|
+
stop() {
|
|
1973
|
+
if (interval) {
|
|
1974
|
+
clearInterval(interval);
|
|
1975
|
+
interval = null;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
var init_alarm_service = __esm({
|
|
1981
|
+
"../emulator/dist/services/alarm-service.js"() {
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
function parseCronField(field, min, max) {
|
|
1985
|
+
const values = /* @__PURE__ */ new Set();
|
|
1986
|
+
for (const part of field.split(",")) {
|
|
1987
|
+
const trimmed = part.trim();
|
|
1988
|
+
if (trimmed === "*") {
|
|
1989
|
+
for (let i = min; i <= max; i++) {
|
|
1990
|
+
values.add(i);
|
|
1991
|
+
}
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const stepMatch = trimmed.match(/^(.+)\/(\d+)$/);
|
|
1995
|
+
if (stepMatch) {
|
|
1996
|
+
const step = parseInt(stepMatch[2], 10);
|
|
1997
|
+
const base = stepMatch[1];
|
|
1998
|
+
let start = min;
|
|
1999
|
+
let end = max;
|
|
2000
|
+
if (base !== "*") {
|
|
2001
|
+
const rangeMatch2 = base.match(/^(\d+)-(\d+)$/);
|
|
2002
|
+
if (rangeMatch2) {
|
|
2003
|
+
start = parseInt(rangeMatch2[1], 10);
|
|
2004
|
+
end = parseInt(rangeMatch2[2], 10);
|
|
2005
|
+
} else {
|
|
2006
|
+
start = parseInt(base, 10);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (start < min || start > max || end < min || end > max) {
|
|
2010
|
+
throw new Error(`Cron field range ${start}-${end} is out of bounds (${min}-${max})`);
|
|
2011
|
+
}
|
|
2012
|
+
for (let i = start; i <= end; i += step) {
|
|
2013
|
+
values.add(i);
|
|
2014
|
+
}
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
const rangeMatch = trimmed.match(/^(\d+)-(\d+)$/);
|
|
2018
|
+
if (rangeMatch) {
|
|
2019
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
2020
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
2021
|
+
if (start < min || start > max || end < min || end > max) {
|
|
2022
|
+
throw new Error(`Cron field range ${start}-${end} is out of bounds (${min}-${max})`);
|
|
2023
|
+
}
|
|
2024
|
+
for (let i = start; i <= end; i++) {
|
|
2025
|
+
values.add(i);
|
|
2026
|
+
}
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
const num = parseInt(trimmed, 10);
|
|
2030
|
+
if (isNaN(num)) {
|
|
2031
|
+
throw new Error(`Invalid cron field value: '${trimmed}' (expected number, range, step, or wildcard)`);
|
|
2032
|
+
}
|
|
2033
|
+
if (num < min || num > max) {
|
|
2034
|
+
throw new Error(`Cron field value ${num} is out of range (${min}-${max})`);
|
|
2035
|
+
}
|
|
2036
|
+
values.add(num);
|
|
2037
|
+
}
|
|
2038
|
+
return { values };
|
|
2039
|
+
}
|
|
2040
|
+
function parseCronExpression(expression) {
|
|
2041
|
+
const parts = expression.trim().split(/\s+/);
|
|
2042
|
+
if (parts.length !== 5) {
|
|
2043
|
+
throw new Error(`Invalid cron expression: ${expression} (expected 5 fields)`);
|
|
2044
|
+
}
|
|
2045
|
+
return [
|
|
2046
|
+
parseCronField(parts[0], 0, 59),
|
|
2047
|
+
parseCronField(parts[1], 0, 23),
|
|
2048
|
+
parseCronField(parts[2], 1, 31),
|
|
2049
|
+
parseCronField(parts[3], 1, 12),
|
|
2050
|
+
parseCronField(parts[4], 0, 6)
|
|
2051
|
+
];
|
|
2052
|
+
}
|
|
2053
|
+
function cronMatchesDate(fields, date) {
|
|
2054
|
+
const minute = date.getMinutes();
|
|
2055
|
+
const hour = date.getHours();
|
|
2056
|
+
const dayOfMonth = date.getDate();
|
|
2057
|
+
const month = date.getMonth() + 1;
|
|
2058
|
+
const dayOfWeek = date.getDay();
|
|
2059
|
+
return fields[0].values.has(minute) && fields[1].values.has(hour) && fields[2].values.has(dayOfMonth) && fields[3].values.has(month) && fields[4].values.has(dayOfWeek);
|
|
2060
|
+
}
|
|
2061
|
+
function createCronScheduler(db, cronBindings, workerUrl) {
|
|
2062
|
+
let interval = null;
|
|
2063
|
+
const parsedCrons = /* @__PURE__ */ new Map();
|
|
2064
|
+
let lastCheckedMinute = -1;
|
|
2065
|
+
for (const [name, expression] of Object.entries(cronBindings)) {
|
|
2066
|
+
try {
|
|
2067
|
+
const fields = parseCronExpression(expression);
|
|
2068
|
+
parsedCrons.set(name, { expression, fields });
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
error(`Failed to parse cron expression for '${name}': ${err instanceof Error ? err.message : String(err)}`);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
async function checkCrons() {
|
|
2074
|
+
const now = /* @__PURE__ */ new Date();
|
|
2075
|
+
const currentMinute = Math.floor(now.getTime() / 6e4);
|
|
2076
|
+
if (currentMinute === lastCheckedMinute) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
lastCheckedMinute = currentMinute;
|
|
2080
|
+
for (const [name, cron] of parsedCrons) {
|
|
2081
|
+
if (cronMatchesDate(cron.fields, now)) {
|
|
2082
|
+
await triggerScheduledHandler(db, name, cron.expression, now, workerUrl);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return {
|
|
2087
|
+
start() {
|
|
2088
|
+
if (interval) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
log(`Cron scheduler started with ${parsedCrons.size} trigger(s)`);
|
|
2092
|
+
for (const [name, cron] of parsedCrons) {
|
|
2093
|
+
log(` ${name}: ${cron.expression}`);
|
|
2094
|
+
}
|
|
2095
|
+
interval = setInterval(() => {
|
|
2096
|
+
checkCrons().catch((err) => {
|
|
2097
|
+
error(`Cron check error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2098
|
+
});
|
|
2099
|
+
}, 15e3);
|
|
2100
|
+
checkCrons().catch(() => {
|
|
2101
|
+
});
|
|
2102
|
+
},
|
|
2103
|
+
stop() {
|
|
2104
|
+
if (interval) {
|
|
2105
|
+
clearInterval(interval);
|
|
2106
|
+
interval = null;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
async function triggerScheduledHandler(db, cronName, cronExpression, triggerTime, workerUrl) {
|
|
2112
|
+
const executionId = randomUUID();
|
|
2113
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2114
|
+
db.prepare(`INSERT INTO cron_executions (id, cron_name, cron_expression, status, triggered_at)
|
|
2115
|
+
VALUES (?, ?, ?, 'running', ?)`).run(executionId, cronName, cronExpression, now);
|
|
2116
|
+
log(`[cron] Triggering '${cronName}' (${cronExpression}) execution=${executionId}`);
|
|
2117
|
+
try {
|
|
2118
|
+
const response = await fetch(workerUrl, {
|
|
2119
|
+
method: "POST",
|
|
2120
|
+
headers: {
|
|
2121
|
+
"Content-Type": "application/json",
|
|
2122
|
+
"X-Ploy-Cron-Trigger": "true",
|
|
2123
|
+
"X-Ploy-Cron-Name": cronName,
|
|
2124
|
+
"X-Ploy-Cron-Expression": cronExpression,
|
|
2125
|
+
"X-Ploy-Cron-Execution-Id": executionId,
|
|
2126
|
+
"X-Ploy-Cron-Scheduled-Time": String(triggerTime.getTime())
|
|
2127
|
+
},
|
|
2128
|
+
body: JSON.stringify({
|
|
2129
|
+
cron: cronExpression,
|
|
2130
|
+
scheduledTime: triggerTime.getTime()
|
|
2131
|
+
})
|
|
2132
|
+
});
|
|
2133
|
+
const completedAt = Math.floor(Date.now() / 1e3);
|
|
2134
|
+
const durationMs = Date.now() - triggerTime.getTime();
|
|
2135
|
+
if (response.ok) {
|
|
2136
|
+
db.prepare(`UPDATE cron_executions SET status = 'completed', completed_at = ?, duration_ms = ? WHERE id = ?`).run(completedAt, durationMs, executionId);
|
|
2137
|
+
log(`[cron] '${cronName}' completed successfully (${durationMs}ms)`);
|
|
2138
|
+
} else {
|
|
2139
|
+
const errorText = await response.text();
|
|
2140
|
+
db.prepare(`UPDATE cron_executions SET status = 'failed', completed_at = ?, duration_ms = ?, error = ? WHERE id = ?`).run(completedAt, durationMs, errorText, executionId);
|
|
2141
|
+
error(`[cron] '${cronName}' failed: ${errorText}`);
|
|
2142
|
+
}
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
const completedAt = Math.floor(Date.now() / 1e3);
|
|
2145
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2146
|
+
db.prepare(`UPDATE cron_executions SET status = 'failed', completed_at = ?, error = ? WHERE id = ?`).run(completedAt, errorMessage, executionId);
|
|
2147
|
+
error(`[cron] '${cronName}' error: ${errorMessage}`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
var init_cron_service = __esm({
|
|
2151
|
+
"../emulator/dist/services/cron-service.js"() {
|
|
2152
|
+
init_logger();
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
1696
2155
|
function getProjectHash(projectDir) {
|
|
1697
2156
|
return createHash("sha256").update(projectDir).digest("hex").slice(0, 12);
|
|
1698
2157
|
}
|
|
@@ -2035,6 +2494,8 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2035
2494
|
state: config.state,
|
|
2036
2495
|
fs: config.fs,
|
|
2037
2496
|
workflow: config.workflow,
|
|
2497
|
+
cron: config.cron,
|
|
2498
|
+
alarm: config.alarm,
|
|
2038
2499
|
auth: config.auth
|
|
2039
2500
|
});
|
|
2040
2501
|
});
|
|
@@ -2559,6 +3020,123 @@ function createDashboardRoutes(app, dbManager2, config) {
|
|
|
2559
3020
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2560
3021
|
}
|
|
2561
3022
|
});
|
|
3023
|
+
app.get("/api/cron/:binding/executions", (c) => {
|
|
3024
|
+
const binding = c.req.param("binding");
|
|
3025
|
+
const cronExpression = config.cron?.[binding];
|
|
3026
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
3027
|
+
if (!cronExpression) {
|
|
3028
|
+
return c.json({ error: "Cron binding not found" }, 404);
|
|
3029
|
+
}
|
|
3030
|
+
try {
|
|
3031
|
+
const db = dbManager2.emulatorDb;
|
|
3032
|
+
const executions = db.prepare(`SELECT id, cron_name, cron_expression, status, error, duration_ms, triggered_at, completed_at, created_at
|
|
3033
|
+
FROM cron_executions
|
|
3034
|
+
WHERE cron_name = ?
|
|
3035
|
+
ORDER BY triggered_at DESC
|
|
3036
|
+
LIMIT ?`).all(binding, limit);
|
|
3037
|
+
return c.json({
|
|
3038
|
+
cronExpression,
|
|
3039
|
+
executions: executions.map((e) => ({
|
|
3040
|
+
id: e.id,
|
|
3041
|
+
status: e.status.toUpperCase(),
|
|
3042
|
+
error: e.error,
|
|
3043
|
+
durationMs: e.duration_ms,
|
|
3044
|
+
triggeredAt: new Date(e.triggered_at * 1e3).toISOString(),
|
|
3045
|
+
completedAt: e.completed_at ? new Date(e.completed_at * 1e3).toISOString() : null,
|
|
3046
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
3047
|
+
}))
|
|
3048
|
+
});
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
app.get("/api/alarm/:binding/entries", (c) => {
|
|
3054
|
+
const binding = c.req.param("binding");
|
|
3055
|
+
const alarmName = config.alarm?.[binding];
|
|
3056
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
3057
|
+
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
3058
|
+
if (!alarmName) {
|
|
3059
|
+
return c.json({ error: "Alarm binding not found" }, 404);
|
|
3060
|
+
}
|
|
3061
|
+
try {
|
|
3062
|
+
const db = dbManager2.emulatorDb;
|
|
3063
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM alarm_entries WHERE alarm_name = ?`).get(alarmName).count;
|
|
3064
|
+
const entries = db.prepare(`SELECT id, scheduled_time, payload, interval_ms, status, created_at
|
|
3065
|
+
FROM alarm_entries
|
|
3066
|
+
WHERE alarm_name = ?
|
|
3067
|
+
ORDER BY scheduled_time DESC
|
|
3068
|
+
LIMIT ? OFFSET ?`).all(alarmName, limit, offset);
|
|
3069
|
+
return c.json({
|
|
3070
|
+
entries: entries.map((e) => ({
|
|
3071
|
+
id: e.id,
|
|
3072
|
+
scheduledTime: new Date(e.scheduled_time * 1e3).toISOString(),
|
|
3073
|
+
payload: e.payload ? JSON.parse(e.payload) : null,
|
|
3074
|
+
intervalMs: e.interval_ms,
|
|
3075
|
+
status: e.status.toUpperCase(),
|
|
3076
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
3077
|
+
})),
|
|
3078
|
+
total,
|
|
3079
|
+
limit,
|
|
3080
|
+
offset
|
|
3081
|
+
});
|
|
3082
|
+
} catch (err) {
|
|
3083
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
3084
|
+
}
|
|
3085
|
+
});
|
|
3086
|
+
app.get("/api/alarm/:binding/metrics", (c) => {
|
|
3087
|
+
const binding = c.req.param("binding");
|
|
3088
|
+
const alarmName = config.alarm?.[binding];
|
|
3089
|
+
if (!alarmName) {
|
|
3090
|
+
return c.json({ error: "Alarm binding not found" }, 404);
|
|
3091
|
+
}
|
|
3092
|
+
try {
|
|
3093
|
+
const db = dbManager2.emulatorDb;
|
|
3094
|
+
const metrics = { pending: 0, fired: 0 };
|
|
3095
|
+
const statusCounts = db.prepare(`SELECT status, COUNT(*) as count
|
|
3096
|
+
FROM alarm_entries
|
|
3097
|
+
WHERE alarm_name = ?
|
|
3098
|
+
GROUP BY status`).all(alarmName);
|
|
3099
|
+
for (const row of statusCounts) {
|
|
3100
|
+
if (row.status === "pending") {
|
|
3101
|
+
metrics.pending = row.count;
|
|
3102
|
+
} else if (row.status === "fired") {
|
|
3103
|
+
metrics.fired = row.count;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return c.json({ metrics });
|
|
3107
|
+
} catch (err) {
|
|
3108
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
3109
|
+
}
|
|
3110
|
+
});
|
|
3111
|
+
app.get("/api/alarm/:binding/executions", (c) => {
|
|
3112
|
+
const binding = c.req.param("binding");
|
|
3113
|
+
const alarmName = config.alarm?.[binding];
|
|
3114
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10);
|
|
3115
|
+
if (!alarmName) {
|
|
3116
|
+
return c.json({ error: "Alarm binding not found" }, 404);
|
|
3117
|
+
}
|
|
3118
|
+
try {
|
|
3119
|
+
const db = dbManager2.emulatorDb;
|
|
3120
|
+
const executions = db.prepare(`SELECT id, alarm_id, scheduled_time, status, error, duration_ms, created_at
|
|
3121
|
+
FROM alarm_executions
|
|
3122
|
+
WHERE alarm_name = ?
|
|
3123
|
+
ORDER BY created_at DESC
|
|
3124
|
+
LIMIT ?`).all(alarmName, limit);
|
|
3125
|
+
return c.json({
|
|
3126
|
+
executions: executions.map((e) => ({
|
|
3127
|
+
id: e.id,
|
|
3128
|
+
alarmId: e.alarm_id,
|
|
3129
|
+
scheduledTime: new Date(e.scheduled_time * 1e3).toISOString(),
|
|
3130
|
+
status: e.status.toUpperCase(),
|
|
3131
|
+
error: e.error,
|
|
3132
|
+
durationMs: e.duration_ms,
|
|
3133
|
+
createdAt: new Date(e.created_at * 1e3).toISOString()
|
|
3134
|
+
}))
|
|
3135
|
+
});
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
2562
3140
|
if (hasDashboard) {
|
|
2563
3141
|
app.get("/assets/*", (c) => {
|
|
2564
3142
|
const path = c.req.path;
|
|
@@ -3269,6 +3847,12 @@ async function startMockServer(dbManager2, config, options = {}) {
|
|
|
3269
3847
|
app.post("/fs/delete", fsHandlers.deleteHandler);
|
|
3270
3848
|
app.post("/fs/list", fsHandlers.listHandler);
|
|
3271
3849
|
}
|
|
3850
|
+
if (config.alarm) {
|
|
3851
|
+
const alarmHandlers = createAlarmHandlers(dbManager2.emulatorDb);
|
|
3852
|
+
app.post("/alarm/set", alarmHandlers.setHandler);
|
|
3853
|
+
app.post("/alarm/get", alarmHandlers.getHandler);
|
|
3854
|
+
app.post("/alarm/delete", alarmHandlers.deleteHandler);
|
|
3855
|
+
}
|
|
3272
3856
|
if (config.auth) {
|
|
3273
3857
|
const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
|
|
3274
3858
|
app.post("/auth/signup", authHandlers.signupHandler);
|
|
@@ -3301,6 +3885,7 @@ var DEFAULT_MOCK_SERVER_PORT;
|
|
|
3301
3885
|
var init_mock_server = __esm({
|
|
3302
3886
|
"../emulator/dist/services/mock-server.js"() {
|
|
3303
3887
|
init_paths();
|
|
3888
|
+
init_alarm_service();
|
|
3304
3889
|
init_auth_service();
|
|
3305
3890
|
init_cache_service();
|
|
3306
3891
|
init_dashboard_routes();
|
|
@@ -3463,6 +4048,52 @@ CREATE TABLE IF NOT EXISTS fs_entries (
|
|
|
3463
4048
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
3464
4049
|
PRIMARY KEY (fs_name, key)
|
|
3465
4050
|
);
|
|
4051
|
+
|
|
4052
|
+
-- Cron executions table
|
|
4053
|
+
CREATE TABLE IF NOT EXISTS cron_executions (
|
|
4054
|
+
id TEXT PRIMARY KEY,
|
|
4055
|
+
cron_name TEXT NOT NULL,
|
|
4056
|
+
cron_expression TEXT NOT NULL,
|
|
4057
|
+
status TEXT DEFAULT 'running',
|
|
4058
|
+
error TEXT,
|
|
4059
|
+
duration_ms INTEGER,
|
|
4060
|
+
triggered_at INTEGER NOT NULL,
|
|
4061
|
+
completed_at INTEGER,
|
|
4062
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
4063
|
+
);
|
|
4064
|
+
|
|
4065
|
+
CREATE INDEX IF NOT EXISTS idx_cron_executions_name
|
|
4066
|
+
ON cron_executions(cron_name, triggered_at);
|
|
4067
|
+
|
|
4068
|
+
-- Alarm entries table (durable timers, optionally recurring)
|
|
4069
|
+
CREATE TABLE IF NOT EXISTS alarm_entries (
|
|
4070
|
+
id TEXT NOT NULL,
|
|
4071
|
+
alarm_name TEXT NOT NULL,
|
|
4072
|
+
scheduled_time INTEGER NOT NULL,
|
|
4073
|
+
payload TEXT,
|
|
4074
|
+
interval_ms INTEGER,
|
|
4075
|
+
status TEXT DEFAULT 'pending',
|
|
4076
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
4077
|
+
PRIMARY KEY (alarm_name, id)
|
|
4078
|
+
);
|
|
4079
|
+
|
|
4080
|
+
CREATE INDEX IF NOT EXISTS idx_alarm_entries_status_time
|
|
4081
|
+
ON alarm_entries(status, scheduled_time);
|
|
4082
|
+
|
|
4083
|
+
-- Alarm execution log
|
|
4084
|
+
CREATE TABLE IF NOT EXISTS alarm_executions (
|
|
4085
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4086
|
+
alarm_id TEXT NOT NULL,
|
|
4087
|
+
alarm_name TEXT NOT NULL,
|
|
4088
|
+
scheduled_time INTEGER NOT NULL,
|
|
4089
|
+
status TEXT DEFAULT 'pending',
|
|
4090
|
+
error TEXT,
|
|
4091
|
+
duration_ms INTEGER,
|
|
4092
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
4093
|
+
);
|
|
4094
|
+
|
|
4095
|
+
CREATE INDEX IF NOT EXISTS idx_alarm_executions_name
|
|
4096
|
+
ON alarm_executions(alarm_name, created_at);
|
|
3466
4097
|
`;
|
|
3467
4098
|
}
|
|
3468
4099
|
});
|
|
@@ -3479,6 +4110,8 @@ var init_emulator = __esm({
|
|
|
3479
4110
|
init_env();
|
|
3480
4111
|
init_ploy_config2();
|
|
3481
4112
|
init_workerd_config();
|
|
4113
|
+
init_alarm_service();
|
|
4114
|
+
init_cron_service();
|
|
3482
4115
|
init_mock_server();
|
|
3483
4116
|
init_queue_service();
|
|
3484
4117
|
init_logger();
|
|
@@ -3494,6 +4127,8 @@ var init_emulator = __esm({
|
|
|
3494
4127
|
workerdProcess = null;
|
|
3495
4128
|
fileWatcher = null;
|
|
3496
4129
|
queueProcessor = null;
|
|
4130
|
+
cronScheduler = null;
|
|
4131
|
+
alarmProcessor = null;
|
|
3497
4132
|
resolvedEnvVars = {};
|
|
3498
4133
|
constructor(options = {}) {
|
|
3499
4134
|
const port = options.port ?? 8787;
|
|
@@ -3561,6 +4196,18 @@ var init_emulator = __esm({
|
|
|
3561
4196
|
this.queueProcessor.start();
|
|
3562
4197
|
debug("Queue processor started", this.options.verbose);
|
|
3563
4198
|
}
|
|
4199
|
+
if (this.config.cron) {
|
|
4200
|
+
const workerUrl2 = `http://${this.options.host}:${String(this.options.port)}`;
|
|
4201
|
+
this.cronScheduler = createCronScheduler(this.dbManager.emulatorDb, this.config.cron, workerUrl2);
|
|
4202
|
+
this.cronScheduler.start();
|
|
4203
|
+
debug("Cron scheduler started", this.options.verbose);
|
|
4204
|
+
}
|
|
4205
|
+
if (this.config.alarm) {
|
|
4206
|
+
const workerUrl2 = `http://${this.options.host}:${String(this.options.port)}`;
|
|
4207
|
+
this.alarmProcessor = createAlarmProcessor(this.dbManager.emulatorDb, workerUrl2);
|
|
4208
|
+
this.alarmProcessor.start();
|
|
4209
|
+
debug("Alarm processor started", this.options.verbose);
|
|
4210
|
+
}
|
|
3564
4211
|
success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
|
|
3565
4212
|
log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
|
|
3566
4213
|
if (Object.keys(this.resolvedEnvVars).length > 0) {
|
|
@@ -3581,6 +4228,14 @@ var init_emulator = __esm({
|
|
|
3581
4228
|
if (this.config.workflow) {
|
|
3582
4229
|
log(` Workflow bindings: ${Object.keys(this.config.workflow).join(", ")}`);
|
|
3583
4230
|
}
|
|
4231
|
+
if (this.config.cron) {
|
|
4232
|
+
for (const [name, expr] of Object.entries(this.config.cron)) {
|
|
4233
|
+
log(` Cron: ${name} = ${expr}`);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
if (this.config.alarm) {
|
|
4237
|
+
log(` Alarm bindings: ${Object.keys(this.config.alarm).join(", ")}`);
|
|
4238
|
+
}
|
|
3584
4239
|
this.setupSignalHandlers();
|
|
3585
4240
|
} catch (err) {
|
|
3586
4241
|
error(`Failed to start emulator: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3705,6 +4360,14 @@ var init_emulator = __esm({
|
|
|
3705
4360
|
process.on("SIGTERM", handler);
|
|
3706
4361
|
}
|
|
3707
4362
|
async stop() {
|
|
4363
|
+
if (this.cronScheduler) {
|
|
4364
|
+
this.cronScheduler.stop();
|
|
4365
|
+
this.cronScheduler = null;
|
|
4366
|
+
}
|
|
4367
|
+
if (this.alarmProcessor) {
|
|
4368
|
+
this.alarmProcessor.stop();
|
|
4369
|
+
this.alarmProcessor = null;
|
|
4370
|
+
}
|
|
3708
4371
|
if (this.queueProcessor) {
|
|
3709
4372
|
this.queueProcessor.stop();
|
|
3710
4373
|
this.queueProcessor = null;
|
|
@@ -5600,6 +6263,24 @@ ${varProps}
|
|
|
5600
6263
|
properties.push(` ${bindingName}: WorkflowBinding;`);
|
|
5601
6264
|
}
|
|
5602
6265
|
}
|
|
6266
|
+
if (config.fs) {
|
|
6267
|
+
const fsKeys = Object.keys(config.fs);
|
|
6268
|
+
if (fsKeys.length > 0) {
|
|
6269
|
+
imports.push("FileStorageBinding");
|
|
6270
|
+
for (const bindingName of fsKeys) {
|
|
6271
|
+
properties.push(` ${bindingName}: FileStorageBinding;`);
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
if (config.alarm) {
|
|
6276
|
+
const alarmKeys = Object.keys(config.alarm);
|
|
6277
|
+
if (alarmKeys.length > 0) {
|
|
6278
|
+
imports.push("AlarmBinding");
|
|
6279
|
+
for (const bindingName of alarmKeys) {
|
|
6280
|
+
properties.push(` ${bindingName}: AlarmBinding;`);
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
5603
6284
|
const lines = [
|
|
5604
6285
|
"// This file is auto-generated by `ploy types`. Do not edit manually.",
|
|
5605
6286
|
""
|
|
@@ -5641,7 +6322,7 @@ async function typesCommand(options = {}) {
|
|
|
5641
6322
|
console.error("Error: ploy.yaml not found in current directory");
|
|
5642
6323
|
process.exit(1);
|
|
5643
6324
|
}
|
|
5644
|
-
const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow;
|
|
6325
|
+
const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow || config.fs || config.alarm || config.cron;
|
|
5645
6326
|
if (!hasBindings2) {
|
|
5646
6327
|
console.log("No bindings found in ploy.yaml. Generating empty Env.");
|
|
5647
6328
|
}
|