@meetploy/cli 1.17.1 → 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/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 Database from 'better-sqlite3';
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,7 +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.alarm, "alarm", configFile);
222
+ validateBindings(config.timer, "timer", configFile);
223
223
  if (config.ai !== void 0 && typeof config.ai !== "boolean") {
224
224
  throw new Error(`'ai' in ${configFile} must be a boolean`);
225
225
  }
@@ -295,7 +295,7 @@ function readAndValidatePloyConfigSync(projectDir, configPath, validationOptions
295
295
  return validatePloyConfig(config, configFile, validationOptions);
296
296
  }
297
297
  function hasBindings(config) {
298
- return !!(config.env ?? config.db ?? config.queue ?? config.cache ?? config.state ?? config.fs ?? config.workflow ?? config.cron ?? config.alarm ?? 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);
299
299
  }
300
300
  function parseDotEnv(content) {
301
301
  const result = {};
@@ -398,72 +398,6 @@ var init_cli = __esm({
398
398
  }
399
399
  });
400
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
-
467
401
  // ../emulator/dist/runtime/cache-runtime.js
468
402
  var CACHE_RUNTIME_CODE;
469
403
  var init_cache_runtime = __esm({
@@ -1087,6 +1021,72 @@ export function initializeState(stateName: string, serviceUrl: string): StateBin
1087
1021
  }
1088
1022
  });
1089
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
+
1090
1090
  // ../emulator/dist/runtime/workflow-runtime.js
1091
1091
  var WORKFLOW_RUNTIME_CODE;
1092
1092
  var init_workflow_runtime = __esm({
@@ -1337,10 +1337,10 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
1337
1337
  bindings.push(` ${bindingName}: initializeWorkflow("${workflowName}", "${mockServiceUrl}"),`);
1338
1338
  }
1339
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}"),`);
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
1344
  }
1345
1345
  }
1346
1346
  imports.push('import userWorker from "__ploy_user_worker__";');
@@ -1402,18 +1402,18 @@ function generateWrapperCode(config, mockServiceUrl, envVars) {
1402
1402
  headers: { "Content-Type": "application/json" }
1403
1403
  });
1404
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) {
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
1412
  const body = await request.json().catch(() => ({}));
1413
1413
  try {
1414
- await userWorker.alarm({
1415
- id: alarmId,
1416
- scheduledTime: parseInt(alarmTime || "0", 10),
1414
+ await userWorker.timer({
1415
+ id: timerId,
1416
+ scheduledTime: parseInt(timerTime || "0", 10),
1417
1417
  payload: body.payload,
1418
1418
  intervalMs: body.intervalMs
1419
1419
  }, injectedEnv, ctx);
@@ -1472,7 +1472,7 @@ export default {
1472
1472
  async fetch(request, env, ctx) {
1473
1473
  const injectedEnv = { ...env, ...ployBindings };${envVarsCode}
1474
1474
  ${cronHandlerCode}
1475
- ${alarmHandlerCode}
1475
+ ${timerHandlerCode}
1476
1476
  ${workflowHandlerCode}
1477
1477
  ${queueHandlerCode}
1478
1478
 
@@ -1520,8 +1520,8 @@ function createRuntimePlugin(_config) {
1520
1520
  path: "__ploy_workflow_runtime__",
1521
1521
  namespace: "ploy-runtime"
1522
1522
  }));
1523
- build2.onResolve({ filter: /^__ploy_alarm_runtime__$/ }, () => ({
1524
- path: "__ploy_alarm_runtime__",
1523
+ build2.onResolve({ filter: /^__ploy_timer_runtime__$/ }, () => ({
1524
+ path: "__ploy_timer_runtime__",
1525
1525
  namespace: "ploy-runtime"
1526
1526
  }));
1527
1527
  build2.onLoad({ filter: /^__ploy_db_runtime__$/, namespace: "ploy-runtime" }, () => ({
@@ -1548,8 +1548,8 @@ function createRuntimePlugin(_config) {
1548
1548
  contents: WORKFLOW_RUNTIME_CODE,
1549
1549
  loader: "ts"
1550
1550
  }));
1551
- build2.onLoad({ filter: /^__ploy_alarm_runtime__$/, namespace: "ploy-runtime" }, () => ({
1552
- contents: ALARM_RUNTIME_CODE,
1551
+ build2.onLoad({ filter: /^__ploy_timer_runtime__$/, namespace: "ploy-runtime" }, () => ({
1552
+ contents: TIMER_RUNTIME_CODE,
1553
1553
  loader: "ts"
1554
1554
  }));
1555
1555
  }
@@ -1588,12 +1588,12 @@ async function bundleWorker(options) {
1588
1588
  var NODE_BUILTINS;
1589
1589
  var init_bundler = __esm({
1590
1590
  "../emulator/dist/bundler/bundler.js"() {
1591
- init_alarm_runtime();
1592
1591
  init_cache_runtime();
1593
1592
  init_db_runtime();
1594
1593
  init_fs_runtime();
1595
1594
  init_queue_runtime();
1596
1595
  init_state_runtime();
1596
+ init_timer_runtime();
1597
1597
  init_workflow_runtime();
1598
1598
  NODE_BUILTINS = [
1599
1599
  "assert",
@@ -1852,135 +1852,6 @@ var init_workerd_config = __esm({
1852
1852
  "../emulator/dist/config/workerd-config.js"() {
1853
1853
  }
1854
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
1855
  function parseCronField(field, min, max) {
1985
1856
  const values = /* @__PURE__ */ new Set();
1986
1857
  for (const part of field.split(",")) {
@@ -2495,7 +2366,7 @@ function createDashboardRoutes(app, dbManager2, config) {
2495
2366
  fs: config.fs,
2496
2367
  workflow: config.workflow,
2497
2368
  cron: config.cron,
2498
- alarm: config.alarm,
2369
+ timer: config.timer,
2499
2370
  auth: config.auth
2500
2371
  });
2501
2372
  });
@@ -3050,22 +2921,22 @@ function createDashboardRoutes(app, dbManager2, config) {
3050
2921
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
3051
2922
  }
3052
2923
  });
3053
- app.get("/api/alarm/:binding/entries", (c) => {
2924
+ app.get("/api/timer/:binding/entries", (c) => {
3054
2925
  const binding = c.req.param("binding");
3055
- const alarmName = config.alarm?.[binding];
2926
+ const timerName = config.timer?.[binding];
3056
2927
  const limit = parseInt(c.req.query("limit") ?? "20", 10);
3057
2928
  const offset = parseInt(c.req.query("offset") ?? "0", 10);
3058
- if (!alarmName) {
3059
- return c.json({ error: "Alarm binding not found" }, 404);
2929
+ if (!timerName) {
2930
+ return c.json({ error: "Timer binding not found" }, 404);
3060
2931
  }
3061
2932
  try {
3062
2933
  const db = dbManager2.emulatorDb;
3063
- const total = db.prepare(`SELECT COUNT(*) as count FROM alarm_entries WHERE alarm_name = ?`).get(alarmName).count;
2934
+ const total = db.prepare(`SELECT COUNT(*) as count FROM timer_entries WHERE timer_name = ?`).get(timerName).count;
3064
2935
  const entries = db.prepare(`SELECT id, scheduled_time, payload, interval_ms, status, created_at
3065
- FROM alarm_entries
3066
- WHERE alarm_name = ?
2936
+ FROM timer_entries
2937
+ WHERE timer_name = ?
3067
2938
  ORDER BY scheduled_time DESC
3068
- LIMIT ? OFFSET ?`).all(alarmName, limit, offset);
2939
+ LIMIT ? OFFSET ?`).all(timerName, limit, offset);
3069
2940
  return c.json({
3070
2941
  entries: entries.map((e) => ({
3071
2942
  id: e.id,
@@ -3083,19 +2954,19 @@ function createDashboardRoutes(app, dbManager2, config) {
3083
2954
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
3084
2955
  }
3085
2956
  });
3086
- app.get("/api/alarm/:binding/metrics", (c) => {
2957
+ app.get("/api/timer/:binding/metrics", (c) => {
3087
2958
  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);
2959
+ const timerName = config.timer?.[binding];
2960
+ if (!timerName) {
2961
+ return c.json({ error: "Timer binding not found" }, 404);
3091
2962
  }
3092
2963
  try {
3093
2964
  const db = dbManager2.emulatorDb;
3094
2965
  const metrics = { pending: 0, fired: 0 };
3095
2966
  const statusCounts = db.prepare(`SELECT status, COUNT(*) as count
3096
- FROM alarm_entries
3097
- WHERE alarm_name = ?
3098
- GROUP BY status`).all(alarmName);
2967
+ FROM timer_entries
2968
+ WHERE timer_name = ?
2969
+ GROUP BY status`).all(timerName);
3099
2970
  for (const row of statusCounts) {
3100
2971
  if (row.status === "pending") {
3101
2972
  metrics.pending = row.count;
@@ -3108,24 +2979,24 @@ function createDashboardRoutes(app, dbManager2, config) {
3108
2979
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
3109
2980
  }
3110
2981
  });
3111
- app.get("/api/alarm/:binding/executions", (c) => {
2982
+ app.get("/api/timer/:binding/executions", (c) => {
3112
2983
  const binding = c.req.param("binding");
3113
- const alarmName = config.alarm?.[binding];
2984
+ const timerName = config.timer?.[binding];
3114
2985
  const limit = parseInt(c.req.query("limit") ?? "20", 10);
3115
- if (!alarmName) {
3116
- return c.json({ error: "Alarm binding not found" }, 404);
2986
+ if (!timerName) {
2987
+ return c.json({ error: "Timer binding not found" }, 404);
3117
2988
  }
3118
2989
  try {
3119
2990
  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 = ?
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 = ?
3123
2994
  ORDER BY created_at DESC
3124
- LIMIT ?`).all(alarmName, limit);
2995
+ LIMIT ?`).all(timerName, limit);
3125
2996
  return c.json({
3126
2997
  executions: executions.map((e) => ({
3127
2998
  id: e.id,
3128
- alarmId: e.alarm_id,
2999
+ timerId: e.timer_id,
3129
3000
  scheduledTime: new Date(e.scheduled_time * 1e3).toISOString(),
3130
3001
  status: e.status.toUpperCase(),
3131
3002
  error: e.error,
@@ -3269,7 +3140,7 @@ function createDbHandler(getDatabase) {
3269
3140
  return c.json(results);
3270
3141
  }
3271
3142
  if (method === "dump") {
3272
- const buffer = db.serialize();
3143
+ const buffer = await db.dumpToBuffer();
3273
3144
  return new Response(new Uint8Array(buffer), {
3274
3145
  headers: {
3275
3146
  "Content-Type": "application/octet-stream"
@@ -3625,6 +3496,135 @@ var init_state_service = __esm({
3625
3496
  "../emulator/dist/services/state-service.js"() {
3626
3497
  }
3627
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
+ });
3628
3628
  function createWorkflowHandlers(db, workerUrl) {
3629
3629
  const triggerHandler = async (c) => {
3630
3630
  try {
@@ -3847,11 +3847,11 @@ async function startMockServer(dbManager2, config, options = {}) {
3847
3847
  app.post("/fs/delete", fsHandlers.deleteHandler);
3848
3848
  app.post("/fs/list", fsHandlers.listHandler);
3849
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);
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
3855
  }
3856
3856
  if (config.auth) {
3857
3857
  const authHandlers = createAuthHandlers(dbManager2.emulatorDb);
@@ -3885,7 +3885,6 @@ var DEFAULT_MOCK_SERVER_PORT;
3885
3885
  var init_mock_server = __esm({
3886
3886
  "../emulator/dist/services/mock-server.js"() {
3887
3887
  init_paths();
3888
- init_alarm_service();
3889
3888
  init_auth_service();
3890
3889
  init_cache_service();
3891
3890
  init_dashboard_routes();
@@ -3893,20 +3892,82 @@ var init_mock_server = __esm({
3893
3892
  init_fs_service();
3894
3893
  init_queue_service();
3895
3894
  init_state_service();
3895
+ init_timer_service();
3896
3896
  init_workflow_service();
3897
3897
  DEFAULT_MOCK_SERVER_PORT = 4003;
3898
3898
  }
3899
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
+ });
3900
3961
  function initializeDatabases(projectDir) {
3901
3962
  const dataDir = ensureDataDir(projectDir);
3902
3963
  const d1Databases = /* @__PURE__ */ new Map();
3903
- const emulatorDb = new Database(join(dataDir, "emulator.db"));
3964
+ const emulatorDb = openDatabase(join(dataDir, "emulator.db"));
3904
3965
  emulatorDb.pragma("journal_mode = WAL");
3905
3966
  emulatorDb.exec(EMULATOR_SCHEMA);
3906
3967
  function getD1Database(bindingName) {
3907
3968
  let db = d1Databases.get(bindingName);
3908
3969
  if (!db) {
3909
- db = new Database(join(dataDir, "db", `${bindingName}.db`));
3970
+ db = openDatabase(join(dataDir, "db", `${bindingName}.db`));
3910
3971
  db.pragma("journal_mode = WAL");
3911
3972
  d1Databases.set(bindingName, db);
3912
3973
  }
@@ -3929,6 +3990,7 @@ var EMULATOR_SCHEMA;
3929
3990
  var init_sqlite = __esm({
3930
3991
  "../emulator/dist/utils/sqlite.js"() {
3931
3992
  init_paths();
3993
+ init_sqlite_db();
3932
3994
  EMULATOR_SCHEMA = `
3933
3995
  -- Queue messages table
3934
3996
  CREATE TABLE IF NOT EXISTS queue_messages (
@@ -4065,26 +4127,26 @@ CREATE TABLE IF NOT EXISTS cron_executions (
4065
4127
  CREATE INDEX IF NOT EXISTS idx_cron_executions_name
4066
4128
  ON cron_executions(cron_name, triggered_at);
4067
4129
 
4068
- -- Alarm entries table (durable timers, optionally recurring)
4069
- CREATE TABLE IF NOT EXISTS alarm_entries (
4130
+ -- Timer entries table (durable timers, optionally recurring)
4131
+ CREATE TABLE IF NOT EXISTS timer_entries (
4070
4132
  id TEXT NOT NULL,
4071
- alarm_name TEXT NOT NULL,
4133
+ timer_name TEXT NOT NULL,
4072
4134
  scheduled_time INTEGER NOT NULL,
4073
4135
  payload TEXT,
4074
4136
  interval_ms INTEGER,
4075
4137
  status TEXT DEFAULT 'pending',
4076
4138
  created_at INTEGER DEFAULT (strftime('%s', 'now')),
4077
- PRIMARY KEY (alarm_name, id)
4139
+ PRIMARY KEY (timer_name, id)
4078
4140
  );
4079
4141
 
4080
- CREATE INDEX IF NOT EXISTS idx_alarm_entries_status_time
4081
- ON alarm_entries(status, scheduled_time);
4142
+ CREATE INDEX IF NOT EXISTS idx_timer_entries_status_time
4143
+ ON timer_entries(status, scheduled_time);
4082
4144
 
4083
- -- Alarm execution log
4084
- CREATE TABLE IF NOT EXISTS alarm_executions (
4145
+ -- Timer execution log
4146
+ CREATE TABLE IF NOT EXISTS timer_executions (
4085
4147
  id INTEGER PRIMARY KEY AUTOINCREMENT,
4086
- alarm_id TEXT NOT NULL,
4087
- alarm_name TEXT NOT NULL,
4148
+ timer_id TEXT NOT NULL,
4149
+ timer_name TEXT NOT NULL,
4088
4150
  scheduled_time INTEGER NOT NULL,
4089
4151
  status TEXT DEFAULT 'pending',
4090
4152
  error TEXT,
@@ -4092,8 +4154,8 @@ CREATE TABLE IF NOT EXISTS alarm_executions (
4092
4154
  created_at INTEGER DEFAULT (strftime('%s', 'now'))
4093
4155
  );
4094
4156
 
4095
- CREATE INDEX IF NOT EXISTS idx_alarm_executions_name
4096
- ON alarm_executions(alarm_name, created_at);
4157
+ CREATE INDEX IF NOT EXISTS idx_timer_executions_name
4158
+ ON timer_executions(timer_name, created_at);
4097
4159
  `;
4098
4160
  }
4099
4161
  });
@@ -4110,10 +4172,10 @@ var init_emulator = __esm({
4110
4172
  init_env();
4111
4173
  init_ploy_config2();
4112
4174
  init_workerd_config();
4113
- init_alarm_service();
4114
4175
  init_cron_service();
4115
4176
  init_mock_server();
4116
4177
  init_queue_service();
4178
+ init_timer_service();
4117
4179
  init_logger();
4118
4180
  init_paths();
4119
4181
  init_sqlite();
@@ -4128,7 +4190,7 @@ var init_emulator = __esm({
4128
4190
  fileWatcher = null;
4129
4191
  queueProcessor = null;
4130
4192
  cronScheduler = null;
4131
- alarmProcessor = null;
4193
+ timerProcessor = null;
4132
4194
  resolvedEnvVars = {};
4133
4195
  constructor(options = {}) {
4134
4196
  const port = options.port ?? 8787;
@@ -4202,11 +4264,11 @@ var init_emulator = __esm({
4202
4264
  this.cronScheduler.start();
4203
4265
  debug("Cron scheduler started", this.options.verbose);
4204
4266
  }
4205
- if (this.config.alarm) {
4267
+ if (this.config.timer) {
4206
4268
  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);
4269
+ this.timerProcessor = createTimerProcessor(this.dbManager.emulatorDb, workerUrl2);
4270
+ this.timerProcessor.start();
4271
+ debug("Timer processor started", this.options.verbose);
4210
4272
  }
4211
4273
  success(`Emulator running at http://${this.options.host}:${String(this.options.port)}`);
4212
4274
  log(` Dashboard: http://${this.options.host}:${String(this.mockServer.port)}`);
@@ -4233,8 +4295,8 @@ var init_emulator = __esm({
4233
4295
  log(` Cron: ${name} = ${expr}`);
4234
4296
  }
4235
4297
  }
4236
- if (this.config.alarm) {
4237
- log(` Alarm bindings: ${Object.keys(this.config.alarm).join(", ")}`);
4298
+ if (this.config.timer) {
4299
+ log(` Timer bindings: ${Object.keys(this.config.timer).join(", ")}`);
4238
4300
  }
4239
4301
  this.setupSignalHandlers();
4240
4302
  } catch (err) {
@@ -4364,9 +4426,9 @@ var init_emulator = __esm({
4364
4426
  this.cronScheduler.stop();
4365
4427
  this.cronScheduler = null;
4366
4428
  }
4367
- if (this.alarmProcessor) {
4368
- this.alarmProcessor.stop();
4369
- this.alarmProcessor = null;
4429
+ if (this.timerProcessor) {
4430
+ this.timerProcessor.stop();
4431
+ this.timerProcessor = null;
4370
4432
  }
4371
4433
  if (this.queueProcessor) {
4372
4434
  this.queueProcessor.stop();
@@ -6272,12 +6334,12 @@ ${varProps}
6272
6334
  }
6273
6335
  }
6274
6336
  }
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;`);
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;`);
6281
6343
  }
6282
6344
  }
6283
6345
  }
@@ -6322,7 +6384,7 @@ async function typesCommand(options = {}) {
6322
6384
  console.error("Error: ploy.yaml not found in current directory");
6323
6385
  process.exit(1);
6324
6386
  }
6325
- const hasBindings2 = config.env || config.ai || config.db || config.queue || config.cache || config.state || config.workflow || config.fs || config.alarm || 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;
6326
6388
  if (!hasBindings2) {
6327
6389
  console.log("No bindings found in ploy.yaml. Generating empty Env.");
6328
6390
  }