@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/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,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.serialize();
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 = new Database(join(dataDir, "emulator.db"));
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 = new Database(join(dataDir, "db", `${bindingName}.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.0",
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"