lopata 0.0.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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,91 @@
1
+ import type { HandlerContext, OkResponse } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+
4
+ export interface ScheduledTrigger {
5
+ expression: string;
6
+ description: string;
7
+ workerName: string | null;
8
+ }
9
+
10
+ export const handlers = {
11
+ "scheduled.listTriggers"(_input: {}, ctx: HandlerContext): ScheduledTrigger[] {
12
+ const triggers: ScheduledTrigger[] = [];
13
+
14
+ if (ctx.registry) {
15
+ for (const [name, mgr] of ctx.registry.listManagers()) {
16
+ for (const cron of mgr.config.triggers?.crons ?? []) {
17
+ triggers.push({ expression: cron, description: cronToHuman(cron), workerName: name });
18
+ }
19
+ }
20
+ } else if (ctx.config) {
21
+ for (const cron of ctx.config.triggers?.crons ?? []) {
22
+ triggers.push({ expression: cron, description: cronToHuman(cron), workerName: null });
23
+ }
24
+ }
25
+
26
+ return triggers;
27
+ },
28
+
29
+ async "scheduled.trigger"({ cron, workerName }: { cron: string; workerName?: string | null }, ctx: HandlerContext): Promise<OkResponse> {
30
+ let gen;
31
+ if (workerName && ctx.registry) {
32
+ const mgr = ctx.registry.listManagers().get(workerName);
33
+ gen = mgr?.active;
34
+ } else {
35
+ gen = ctx.manager?.active;
36
+ }
37
+ if (!gen) throw new Error("No active generation");
38
+ const res = await gen.callScheduled(cron);
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ throw new Error(text || `Scheduled handler failed with status ${res.status}`);
42
+ }
43
+ return { ok: true };
44
+ },
45
+ };
46
+
47
+ const SPECIAL_DESCRIPTIONS: Record<string, string> = {
48
+ "@daily": "Every day at midnight",
49
+ "@midnight": "Every day at midnight",
50
+ "@hourly": "Every hour",
51
+ "@weekly": "Every week (Sunday midnight)",
52
+ "@monthly": "First day of every month",
53
+ "@yearly": "First day of every year",
54
+ "@annually": "First day of every year",
55
+ };
56
+
57
+ function cronToHuman(expression: string): string {
58
+ const trimmed = expression.trim();
59
+ const special = SPECIAL_DESCRIPTIONS[trimmed.toLowerCase()];
60
+ if (special) return special;
61
+
62
+ const parts = trimmed.split(/\s+/);
63
+ if (parts.length !== 5) return expression;
64
+
65
+ const [minute, hour, dom, month, dow] = parts;
66
+ const segments: string[] = [];
67
+
68
+ if (minute === "*" && hour === "*" && dom === "*" && month === "*" && dow === "*") {
69
+ return "Every minute";
70
+ }
71
+
72
+ // Detect common patterns
73
+ if (minute!.startsWith("*/")) {
74
+ return `Every ${minute!.slice(2)} minutes`;
75
+ }
76
+ if (hour!.startsWith("*/") && minute === "0") {
77
+ return `Every ${hour!.slice(2)} hours`;
78
+ }
79
+
80
+ if (hour !== "*" && minute !== "*") {
81
+ segments.push(`At ${hour!.padStart(2, "0")}:${minute!.padStart(2, "0")}`);
82
+ } else if (minute !== "*") {
83
+ segments.push(`At minute ${minute}`);
84
+ }
85
+
86
+ if (dow !== "*") segments.push(`on day-of-week ${dow}`);
87
+ if (dom !== "*") segments.push(`on day ${dom}`);
88
+ if (month !== "*") segments.push(`in month ${month}`);
89
+
90
+ return segments.join(" ") || expression;
91
+ }
@@ -0,0 +1,64 @@
1
+ import { getTraceStore } from "../../../tracing/store";
2
+ import type { TraceSummary, TraceDetail } from "../../../tracing/types";
3
+
4
+ export const handlers = {
5
+ "traces.list"(input: { limit?: number; cursor?: string }): { items: TraceSummary[]; cursor: string | null } {
6
+ if (input.limit !== undefined && (typeof input.limit !== "number" || input.limit < 1)) {
7
+ throw new Error("limit must be a positive number");
8
+ }
9
+ if (input.cursor !== undefined && typeof input.cursor !== "string") {
10
+ throw new Error("cursor must be a string");
11
+ }
12
+ const store = getTraceStore();
13
+ return store.listTraces({ limit: input.limit ?? 50, cursor: input.cursor });
14
+ },
15
+
16
+ "traces.getTrace"(input: { traceId: string }): TraceDetail {
17
+ if (!input.traceId || typeof input.traceId !== "string") {
18
+ throw new Error("traceId is required and must be a string");
19
+ }
20
+ const store = getTraceStore();
21
+ return store.getTrace(input.traceId);
22
+ },
23
+
24
+ "traces.search"(input: { query: string; limit?: number }): { items: TraceSummary[]; cursor: string | null } {
25
+ if (!input.query || typeof input.query !== "string") {
26
+ throw new Error("query is required and must be a string");
27
+ }
28
+ if (input.limit !== undefined && (typeof input.limit !== "number" || input.limit < 1)) {
29
+ throw new Error("limit must be a positive number");
30
+ }
31
+ const store = getTraceStore();
32
+ return store.searchTraces(input.query, input.limit ?? 50);
33
+ },
34
+
35
+ "traces.listSpans"(input: { limit?: number; cursor?: string }): { items: Array<{ spanId: string; traceId: string; name: string; status: string; durationMs: number | null; startTime: number; workerName: string | null }>; cursor: string | null } {
36
+ if (input.limit !== undefined && (typeof input.limit !== "number" || input.limit < 1)) {
37
+ throw new Error("limit must be a positive number");
38
+ }
39
+ const store = getTraceStore();
40
+ return store.listAllSpans({ limit: input.limit ?? 50, cursor: input.cursor });
41
+ },
42
+
43
+ "traces.listLogs"(input: { limit?: number; cursor?: string }): { items: Array<{ id: number; spanId: string; traceId: string; timestamp: number; name: string; level: string | null; message: string | null }>; cursor: string | null } {
44
+ if (input.limit !== undefined && (typeof input.limit !== "number" || input.limit < 1)) {
45
+ throw new Error("limit must be a positive number");
46
+ }
47
+ const store = getTraceStore();
48
+ return store.listAllLogs({ limit: input.limit ?? 50, cursor: input.cursor });
49
+ },
50
+
51
+ "traces.errors"(input: { traceId: string }) {
52
+ if (!input.traceId || typeof input.traceId !== "string") {
53
+ throw new Error("traceId is required and must be a string");
54
+ }
55
+ const store = getTraceStore();
56
+ return store.getErrorsForTrace(input.traceId);
57
+ },
58
+
59
+ "traces.clear"(_input: {}): { ok: true } {
60
+ const store = getTraceStore();
61
+ store.clearTraces();
62
+ return { ok: true };
63
+ },
64
+ };
@@ -0,0 +1,65 @@
1
+ import type { HandlerContext, WorkerInfo, WorkerBinding } from "../types";
2
+ import type { WranglerConfig } from "../../../config";
3
+
4
+ function extractBindings(config: WranglerConfig): WorkerBinding[] {
5
+ const bindings: WorkerBinding[] = [];
6
+
7
+ for (const ns of config.kv_namespaces ?? []) {
8
+ bindings.push({ type: "kv", name: ns.binding, target: ns.id, href: `#/kv/${encodeURIComponent(ns.id)}` });
9
+ }
10
+ for (const b of config.r2_buckets ?? []) {
11
+ bindings.push({ type: "r2", name: b.binding, target: b.bucket_name, href: `#/r2/${encodeURIComponent(b.bucket_name)}` });
12
+ }
13
+ for (const db of config.d1_databases ?? []) {
14
+ bindings.push({ type: "d1", name: db.binding, target: db.database_name, href: `#/d1/${encodeURIComponent(db.database_name)}` });
15
+ }
16
+ for (const b of config.durable_objects?.bindings ?? []) {
17
+ bindings.push({ type: "do", name: b.name, target: b.class_name, href: `#/do/${encodeURIComponent(b.class_name)}` });
18
+ }
19
+ for (const p of config.queues?.producers ?? []) {
20
+ bindings.push({ type: "queue", name: p.binding, target: p.queue, href: `#/queue/${encodeURIComponent(p.queue)}` });
21
+ }
22
+ for (const w of config.workflows ?? []) {
23
+ bindings.push({ type: "workflow", name: w.binding, target: w.class_name, href: `#/workflows/${encodeURIComponent(w.binding)}` });
24
+ }
25
+ for (const c of config.containers ?? []) {
26
+ bindings.push({ type: "container", name: c.name ?? c.class_name, target: c.image, href: `#/containers/${encodeURIComponent(c.class_name)}` });
27
+ }
28
+ for (const s of config.services ?? []) {
29
+ const target = s.entrypoint ? `${s.service}#${s.entrypoint}` : s.service;
30
+ bindings.push({ type: "service", name: s.binding, target, href: null });
31
+ }
32
+ if (config.images) {
33
+ bindings.push({ type: "images", name: config.images.binding, target: "", href: null });
34
+ }
35
+
36
+ return bindings;
37
+ }
38
+
39
+ export const handlers = {
40
+ "workers.list"(_input: {}, ctx: HandlerContext): WorkerInfo[] {
41
+ if (ctx.registry) {
42
+ const workers: WorkerInfo[] = [];
43
+ let isFirst = true;
44
+ for (const [name, mgr] of ctx.registry.listManagers()) {
45
+ workers.push({
46
+ name,
47
+ isMain: isFirst,
48
+ bindings: extractBindings(mgr.config),
49
+ });
50
+ isFirst = false;
51
+ }
52
+ return workers;
53
+ }
54
+
55
+ if (ctx.config) {
56
+ return [{
57
+ name: ctx.config.name || "main",
58
+ isMain: true,
59
+ bindings: extractBindings(ctx.config),
60
+ }];
61
+ }
62
+
63
+ return [];
64
+ },
65
+ };
@@ -0,0 +1,171 @@
1
+ import type { HandlerContext, WorkflowSummary, WorkflowInstance, WorkflowDetail, OkResponse } from "../types";
2
+ import { getAllConfigs } from "../types";
3
+ import { getDatabase } from "../../../db";
4
+ import type { SQLQueryBindings } from "bun:sqlite";
5
+ import type { SqliteWorkflowBinding } from "../../../bindings/workflow";
6
+ import { getWaitingEventTypes, isInstanceSleeping } from "../../../bindings/workflow";
7
+
8
+ function getWorkflowBinding(ctx: HandlerContext, name: string): SqliteWorkflowBinding {
9
+ if (ctx.registry) {
10
+ for (const manager of ctx.registry.listManagers().values()) {
11
+ const gen = manager.active;
12
+ if (!gen) continue;
13
+ const entry = gen.registry.workflows.find(w => w.bindingName === name);
14
+ if (entry) return entry.binding;
15
+ }
16
+ }
17
+ if (ctx.manager?.active) {
18
+ const entry = ctx.manager.active.registry.workflows.find(w => w.bindingName === name);
19
+ if (entry) return entry.binding;
20
+ }
21
+ throw new Error(`Workflow binding "${name}" not found`);
22
+ }
23
+
24
+ export const handlers = {
25
+ "workflows.list"(_input: {}, ctx: HandlerContext): WorkflowSummary[] {
26
+ const db = getDatabase();
27
+ const rows = db.query<{ workflow_name: string; status: string; count: number }, []>(
28
+ "SELECT workflow_name, status, COUNT(*) as count FROM workflow_instances GROUP BY workflow_name, status ORDER BY workflow_name"
29
+ ).all();
30
+
31
+ const grouped = new Map<string, { total: number; byStatus: Record<string, number> }>();
32
+ for (const row of rows) {
33
+ let entry = grouped.get(row.workflow_name);
34
+ if (!entry) {
35
+ entry = { total: 0, byStatus: {} };
36
+ grouped.set(row.workflow_name, entry);
37
+ }
38
+ entry.total += row.count;
39
+ entry.byStatus[row.status] = row.count;
40
+ }
41
+
42
+ for (const config of getAllConfigs(ctx)) {
43
+ for (const w of config.workflows ?? []) {
44
+ if (!grouped.has(w.binding)) {
45
+ grouped.set(w.binding, { total: 0, byStatus: {} });
46
+ }
47
+ }
48
+ }
49
+
50
+ return Array.from(grouped.entries())
51
+ .sort((a, b) => a[0].localeCompare(b[0]))
52
+ .map(([name, data]) => ({ name, ...data }));
53
+ },
54
+
55
+ "workflows.listInstances"({ name, status }: { name: string; status?: string }): WorkflowInstance[] {
56
+ const db = getDatabase();
57
+ let query = "SELECT id, status, params, output, error, created_at, updated_at FROM workflow_instances WHERE workflow_name = ?";
58
+ const params: SQLQueryBindings[] = [name];
59
+
60
+ if (status) { query += " AND status = ?"; params.push(status); }
61
+ query += " ORDER BY created_at DESC LIMIT 100";
62
+
63
+ return db.prepare(query).all(...params) as WorkflowInstance[];
64
+ },
65
+
66
+ "workflows.getInstance"({ id }: { name: string; id: string }): WorkflowDetail {
67
+ const db = getDatabase();
68
+ const instance = db.query<Record<string, unknown>, [string]>(
69
+ "SELECT * FROM workflow_instances WHERE id = ?"
70
+ ).get(id);
71
+ if (!instance) throw new Error("Workflow instance not found");
72
+
73
+ const steps = db.query<{ step_name: string; output: string | null; completed_at: number }, [string]>(
74
+ "SELECT step_name, output, completed_at FROM workflow_steps WHERE instance_id = ? ORDER BY completed_at"
75
+ ).all(id);
76
+
77
+ const stepAttempts = db.query<{ step_name: string; failed_attempts: number; last_error: string | null; last_error_name: string | null; last_error_id: string | null; updated_at: number | null }, [string]>(
78
+ "SELECT step_name, failed_attempts, last_error, last_error_name, last_error_id, updated_at FROM workflow_step_attempts WHERE instance_id = ? ORDER BY updated_at DESC"
79
+ ).all(id);
80
+
81
+ const events = db.query<{ id: number; event_type: string; payload: string | null; created_at: number }, [string]>(
82
+ "SELECT id, event_type, payload, created_at FROM workflow_events WHERE instance_id = ? ORDER BY created_at"
83
+ ).all(id);
84
+
85
+ // Compute active sleep: check if the instance is currently sleeping (in-memory)
86
+ let activeSleep: WorkflowDetail["activeSleep"] = null;
87
+ if (instance.status === "running" && isInstanceSleeping(id)) {
88
+ // Find the latest sleep/sleepUntil step to get the "until" time
89
+ for (let i = steps.length - 1; i >= 0; i--) {
90
+ const s = steps[i]!;
91
+ if ((s.step_name.startsWith("sleep:") || s.step_name.startsWith("sleepUntil:")) && s.output) {
92
+ try {
93
+ const parsed = JSON.parse(s.output) as { until: number | string };
94
+ const until = typeof parsed.until === "string" ? new Date(parsed.until).getTime() : parsed.until;
95
+ if (until > Date.now()) {
96
+ activeSleep = { stepName: s.step_name, until };
97
+ break;
98
+ }
99
+ } catch {}
100
+ }
101
+ }
102
+ }
103
+
104
+ // Compute waiting event types from in-memory registry
105
+ const waitingForEvents = instance.status === "waiting" ? getWaitingEventTypes(id) : [];
106
+
107
+ return { ...instance, steps, stepAttempts, events, activeSleep, waitingForEvents } as WorkflowDetail;
108
+ },
109
+
110
+ async "workflows.terminate"({ name, id }: { name: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
111
+ const binding = getWorkflowBinding(ctx, name);
112
+ const instance = await binding.get(id);
113
+ await instance.terminate();
114
+ return { ok: true };
115
+ },
116
+
117
+ async "workflows.create"({ name, params }: { name: string; params: string }, ctx: HandlerContext): Promise<{ ok: true; id: string }> {
118
+ const binding = getWorkflowBinding(ctx, name);
119
+ const parsed = JSON.parse(params);
120
+ const instance = await binding.create({ params: parsed });
121
+ return { ok: true, id: instance.id };
122
+ },
123
+
124
+ async "workflows.pause"({ name, id }: { name: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
125
+ const binding = getWorkflowBinding(ctx, name);
126
+ const instance = await binding.get(id);
127
+ await instance.pause();
128
+ return { ok: true };
129
+ },
130
+
131
+ async "workflows.resume"({ name, id }: { name: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
132
+ const binding = getWorkflowBinding(ctx, name);
133
+ const instance = await binding.get(id);
134
+ await instance.resume();
135
+ return { ok: true };
136
+ },
137
+
138
+ async "workflows.restart"({ name, id, fromStep }: { name: string; id: string; fromStep?: string }, ctx: HandlerContext): Promise<OkResponse> {
139
+ const binding = getWorkflowBinding(ctx, name);
140
+ const instance = await binding.get(id);
141
+ await instance.restart(fromStep ? { fromStep } : undefined);
142
+ return { ok: true };
143
+ },
144
+
145
+ async "workflows.skipSleep"({ name, id }: { name: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
146
+ const binding = getWorkflowBinding(ctx, name);
147
+ const instance = await binding.get(id);
148
+ await instance.skipSleep();
149
+ return { ok: true };
150
+ },
151
+
152
+ async "workflows.sendEvent"({ name, id, type, payload }: { name: string; id: string; type: string; payload?: string }, ctx: HandlerContext): Promise<OkResponse> {
153
+ const binding = getWorkflowBinding(ctx, name);
154
+ const instance = await binding.get(id);
155
+ const parsed = payload ? JSON.parse(payload) : undefined;
156
+ await instance.sendEvent({ type, payload: parsed });
157
+ return { ok: true };
158
+ },
159
+
160
+ async "workflows.duplicate"({ name, id }: { name: string; id: string }, ctx: HandlerContext): Promise<{ ok: true; id: string }> {
161
+ const binding = getWorkflowBinding(ctx, name);
162
+ const db = getDatabase();
163
+ const row = db.query<{ params: string | null }, [string]>(
164
+ "SELECT params FROM workflow_instances WHERE id = ?"
165
+ ).get(id);
166
+ if (!row) throw new Error("Workflow instance not found");
167
+ const params = row.params !== null ? JSON.parse(row.params) : {};
168
+ const newInstance = await binding.create({ params });
169
+ return { ok: true, id: newInstance.id };
170
+ },
171
+ };
@@ -0,0 +1,132 @@
1
+ import { useState, useEffect, useRef } from "preact/hooks";
2
+ import { rpc } from "./client";
3
+ import type { Procedures } from "./server";
4
+ import type { Paginated } from "./types";
5
+
6
+ type EmptyObject = Record<string, never>;
7
+
8
+ // ─── useQuery ────────────────────────────────────────────────────────
9
+
10
+ interface QueryResult<T> {
11
+ data: T | null;
12
+ isLoading: boolean;
13
+ error: Error | null;
14
+ refetch: () => void;
15
+ }
16
+
17
+ export function useQuery<K extends keyof Procedures>(
18
+ procedure: K,
19
+ ...args: Procedures[K]["input"] extends EmptyObject ? [] : [Procedures[K]["input"]]
20
+ ): QueryResult<Procedures[K]["output"]> {
21
+ type Output = Procedures[K]["output"];
22
+ const [state, setState] = useState<{ data: Output | null; isLoading: boolean; error: Error | null }>({
23
+ data: null, isLoading: true, error: null,
24
+ });
25
+ const input = args[0];
26
+ const key = JSON.stringify(input ?? {});
27
+ const genRef = useRef(0);
28
+
29
+ const doFetch = () => {
30
+ const gen = ++genRef.current;
31
+ setState(s => ({ ...s, isLoading: true, error: null }));
32
+ (rpc as Function)(procedure, input)
33
+ .then((data: Output) => {
34
+ if (genRef.current === gen) setState({ data, isLoading: false, error: null });
35
+ })
36
+ .catch((err: unknown) => {
37
+ if (genRef.current === gen) setState({ data: null, isLoading: false, error: toError(err) });
38
+ });
39
+ };
40
+
41
+ useEffect(() => { doFetch(); }, [procedure, key]);
42
+
43
+ return { ...state, refetch: doFetch };
44
+ }
45
+
46
+ // ─── usePaginatedQuery ───────────────────────────────────────────────
47
+
48
+ type PaginatedProcedures = {
49
+ [K in keyof Procedures]: Procedures[K]["output"] extends Paginated<any> ? K : never;
50
+ }[keyof Procedures];
51
+
52
+ type PaginatedItem<K extends PaginatedProcedures> =
53
+ Procedures[K]["output"] extends Paginated<infer T> ? T : never;
54
+
55
+ interface PaginatedQueryResult<T> {
56
+ items: T[];
57
+ isLoading: boolean;
58
+ hasMore: boolean;
59
+ loadMore: () => void;
60
+ refetch: () => void;
61
+ }
62
+
63
+ export function usePaginatedQuery<K extends PaginatedProcedures>(
64
+ procedure: K,
65
+ input: Omit<Procedures[K]["input"], "cursor">,
66
+ ): PaginatedQueryResult<PaginatedItem<K>> {
67
+ type Item = PaginatedItem<K>;
68
+ const [items, setItems] = useState<Item[]>([]);
69
+ const [isLoading, setIsLoading] = useState(true);
70
+ const cursorRef = useRef<string | null>(null);
71
+ const [hasMore, setHasMore] = useState(false);
72
+ const key = JSON.stringify(input);
73
+
74
+ const load = (reset: boolean) => {
75
+ setIsLoading(true);
76
+ const fullInput = { ...input, cursor: reset ? "" : (cursorRef.current ?? "") };
77
+ (rpc as Function)(procedure, fullInput).then((data: Paginated<Item>) => {
78
+ setItems(prev => reset ? data.items : [...prev, ...data.items]);
79
+ cursorRef.current = data.cursor;
80
+ setHasMore(data.cursor !== null);
81
+ setIsLoading(false);
82
+ });
83
+ };
84
+
85
+ useEffect(() => {
86
+ cursorRef.current = null;
87
+ load(true);
88
+ }, [procedure, key]);
89
+
90
+ return { items, isLoading, hasMore, loadMore: () => load(false), refetch: () => load(true) };
91
+ }
92
+
93
+ // ─── useMutation ─────────────────────────────────────────────────────
94
+
95
+ interface MutationResult<Input, Output> {
96
+ mutate: (...args: Input extends EmptyObject ? [] : [Input]) => Promise<Output | undefined>;
97
+ data: Output | null;
98
+ isLoading: boolean;
99
+ error: Error | null;
100
+ reset: () => void;
101
+ }
102
+
103
+ export function useMutation<K extends keyof Procedures>(
104
+ procedure: K,
105
+ ): MutationResult<Procedures[K]["input"], Procedures[K]["output"]> {
106
+ type Output = Procedures[K]["output"];
107
+ const [state, setState] = useState<{ data: Output | null; isLoading: boolean; error: Error | null }>({
108
+ data: null, isLoading: false, error: null,
109
+ });
110
+
111
+ const mutate = async (...args: any[]): Promise<Output | undefined> => {
112
+ setState({ data: null, isLoading: true, error: null });
113
+ try {
114
+ const result = await (rpc as Function)(procedure, args[0]);
115
+ setState({ data: result, isLoading: false, error: null });
116
+ return result;
117
+ } catch (err) {
118
+ setState({ data: null, isLoading: false, error: toError(err) });
119
+ return undefined;
120
+ }
121
+ };
122
+
123
+ const reset = () => setState({ data: null, isLoading: false, error: null });
124
+
125
+ return { ...state, mutate: mutate as MutationResult<Procedures[K]["input"], Output>["mutate"], reset };
126
+ }
127
+
128
+ // ─── Helpers ─────────────────────────────────────────────────────────
129
+
130
+ function toError(err: unknown): Error {
131
+ return err instanceof Error ? err : new Error(String(err));
132
+ }
@@ -0,0 +1,70 @@
1
+ import type { HandlerContext } from "./types";
2
+ import { handlers as overview } from "./handlers/overview";
3
+ import { handlers as kv } from "./handlers/kv";
4
+ import { handlers as r2 } from "./handlers/r2";
5
+ import { handlers as queue } from "./handlers/queue";
6
+ import { handlers as durableObjects } from "./handlers/do";
7
+ import { handlers as workflows } from "./handlers/workflows";
8
+ import { handlers as d1 } from "./handlers/d1";
9
+ import { handlers as cache } from "./handlers/cache";
10
+ import { handlers as generations } from "./handlers/generations";
11
+ import { handlers as workers } from "./handlers/workers";
12
+ import { handlers as containers } from "./handlers/containers";
13
+ import { handlers as traces } from "./handlers/traces";
14
+ import { handlers as config } from "./handlers/config";
15
+ import { handlers as errors } from "./handlers/errors";
16
+ import { handlers as scheduled } from "./handlers/scheduled";
17
+ import { handlers as email } from "./handlers/email";
18
+ import { handlers as ai } from "./handlers/ai";
19
+ import { handlers as analyticsEngine } from "./handlers/analytics-engine";
20
+
21
+ const allHandlers = {
22
+ ...overview,
23
+ ...kv,
24
+ ...r2,
25
+ ...queue,
26
+ ...durableObjects,
27
+ ...workflows,
28
+ ...d1,
29
+ ...cache,
30
+ ...generations,
31
+ ...workers,
32
+ ...containers,
33
+ ...traces,
34
+ ...config,
35
+ ...errors,
36
+ ...scheduled,
37
+ ...email,
38
+ ...ai,
39
+ ...analyticsEngine,
40
+ };
41
+
42
+ export type Procedures = {
43
+ [K in keyof typeof allHandlers]: {
44
+ input: Parameters<(typeof allHandlers)[K]>[0];
45
+ output: Awaited<ReturnType<(typeof allHandlers)[K]>>;
46
+ };
47
+ };
48
+
49
+ function json(data: unknown, status = 200): Response {
50
+ return new Response(JSON.stringify(data), {
51
+ status,
52
+ headers: { "Content-Type": "application/json" },
53
+ });
54
+ }
55
+
56
+ export async function dispatch(request: Request, ctx: HandlerContext): Promise<Response> {
57
+ try {
58
+ const body = await request.json() as { procedure: string; input: unknown };
59
+ if (!body.procedure || typeof body.procedure !== "string") {
60
+ return json({ error: "procedure must be a string" }, 400);
61
+ }
62
+ const handler = allHandlers[body.procedure as keyof typeof allHandlers];
63
+ if (!handler) return json({ error: `Unknown procedure: ${body.procedure}` }, 404);
64
+ const result = await (handler as Function)(body.input ?? {}, ctx);
65
+ return json(result);
66
+ } catch (err) {
67
+ console.error("[bunflare dashboard] RPC error:", err);
68
+ return json({ error: String(err) }, 500);
69
+ }
70
+ }