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,267 @@
1
+ /**
2
+ * RPC stub utilities for wrapping RpcTarget instances and functions
3
+ * returned from DO/service binding RPC calls.
4
+ *
5
+ * On Cloudflare, when an RPC method returns an RpcTarget or function,
6
+ * CF wraps it in a stub proxy. This module provides equivalent local
7
+ * wrapping so code behaves consistently between dev and production.
8
+ */
9
+
10
+ import { warnInvalidRpcArgs, warnInvalidRpcReturn } from "../rpc-validate";
11
+
12
+ // Brand symbol shared across plugin.ts and vite-plugin/modules-plugin.ts
13
+ export const RPC_TARGET_BRAND = Symbol.for("bunflare.RpcTarget");
14
+
15
+ export function isRpcTarget(value: unknown): boolean {
16
+ return (
17
+ value !== null &&
18
+ typeof value === "object" &&
19
+ (value as Record<symbol, unknown>)[RPC_TARGET_BRAND] === true
20
+ );
21
+ }
22
+
23
+ // Properties that should NOT be proxied as RPC (JS internals, Promise protocol, etc.)
24
+ export const NON_RPC_PROPS = new Set<string | symbol>([
25
+ "then", "catch", "finally", // Promise/thenable protocol
26
+ "toJSON", "valueOf", "toString", // conversion
27
+ Symbol.toPrimitive, Symbol.toStringTag, Symbol.iterator, Symbol.asyncIterator,
28
+ ]);
29
+
30
+ // Cache to avoid wrapping the same target twice (handles `return this`)
31
+ const stubCache = new WeakMap<object, object>();
32
+
33
+ /**
34
+ * Wrap an RpcTarget instance in a Proxy that mimics CF stub behavior:
35
+ * - Method calls: validate args → call → wrap return value
36
+ * - Property access: thenable pattern, wraps returned RpcTarget/function values
37
+ * - Filters `_`-prefixed properties (returns undefined)
38
+ * - Symbol.dispose → no-op
39
+ * - dup() → new stub wrapping same target
40
+ */
41
+ export function createRpcStub(target: object): object {
42
+ const cached = stubCache.get(target);
43
+ if (cached) return cached;
44
+
45
+ const stub = new Proxy({} as Record<string, unknown>, {
46
+ get(_obj, prop: string | symbol) {
47
+ if (NON_RPC_PROPS.has(prop)) return undefined;
48
+
49
+ // Symbol.dispose — no-op for `using` keyword compatibility
50
+ if (prop === Symbol.dispose) {
51
+ return () => {};
52
+ }
53
+
54
+ if (typeof prop === "symbol") return undefined;
55
+
56
+ // Filter _-prefixed private members (CF hides these)
57
+ if (prop.startsWith("_")) return undefined;
58
+
59
+ // dup() — returns a new stub wrapping the same target
60
+ if (prop === "dup") {
61
+ return () => {
62
+ // Create a fresh stub (bypass cache)
63
+ const dup = createRpcStubUncached(target);
64
+ return dup;
65
+ };
66
+ }
67
+
68
+ const member = (target as Record<string, unknown>)[prop];
69
+
70
+ // If it's a function, return an rpcCallable with thenable for property access
71
+ if (typeof member === "function") {
72
+ const rpcCallable = (...args: unknown[]) => {
73
+ warnInvalidRpcArgs(args, prop);
74
+ const result = (member as Function).call(target, ...args);
75
+ return Promise.resolve(result).then((r) => wrapRpcReturnValue(r, prop));
76
+ };
77
+
78
+ // Thenable for `await stub.method` (returns the wrapped function itself)
79
+ rpcCallable.then = (
80
+ onFulfilled?: ((value: unknown) => unknown) | null,
81
+ onRejected?: ((reason: unknown) => unknown) | null,
82
+ ) => {
83
+ const wrapped = createRpcFunctionStub(member as Function, target);
84
+ return Promise.resolve(wrapped).then(onFulfilled, onRejected);
85
+ };
86
+
87
+ return rpcCallable;
88
+ }
89
+
90
+ // Non-function property: return thenable
91
+ const rpcCallable = (..._args: unknown[]) => {
92
+ return Promise.reject(new Error(`"${prop}" is not a method on the RPC target`));
93
+ };
94
+
95
+ rpcCallable.then = (
96
+ onFulfilled?: ((value: unknown) => unknown) | null,
97
+ onRejected?: ((reason: unknown) => unknown) | null,
98
+ ) => {
99
+ const wrapped = wrapRpcReturnValue(member, prop);
100
+ return Promise.resolve(wrapped).then(onFulfilled, onRejected);
101
+ };
102
+
103
+ return rpcCallable;
104
+ },
105
+ });
106
+
107
+ stubCache.set(target, stub);
108
+ return stub;
109
+ }
110
+
111
+ /** Create a stub without caching (used by dup()) */
112
+ function createRpcStubUncached(target: object): object {
113
+ return new Proxy({} as Record<string, unknown>, {
114
+ get(_obj, prop: string | symbol) {
115
+ if (NON_RPC_PROPS.has(prop)) return undefined;
116
+ if (prop === Symbol.dispose) return () => {};
117
+ if (typeof prop === "symbol") return undefined;
118
+ if (typeof prop === "string" && prop.startsWith("_")) return undefined;
119
+ if (prop === "dup") return () => createRpcStubUncached(target);
120
+
121
+ const member = (target as Record<string, unknown>)[prop];
122
+
123
+ if (typeof member === "function") {
124
+ const rpcCallable = (...args: unknown[]) => {
125
+ warnInvalidRpcArgs(args, prop as string);
126
+ const result = (member as Function).call(target, ...args);
127
+ return Promise.resolve(result).then((r) => wrapRpcReturnValue(r, prop as string));
128
+ };
129
+ rpcCallable.then = (
130
+ onFulfilled?: ((value: unknown) => unknown) | null,
131
+ onRejected?: ((reason: unknown) => unknown) | null,
132
+ ) => {
133
+ const wrapped = createRpcFunctionStub(member as Function, target);
134
+ return Promise.resolve(wrapped).then(onFulfilled, onRejected);
135
+ };
136
+ return rpcCallable;
137
+ }
138
+
139
+ const rpcCallable = (..._args: unknown[]) => {
140
+ return Promise.reject(new Error(`"${prop}" is not a method on the RPC target`));
141
+ };
142
+ rpcCallable.then = (
143
+ onFulfilled?: ((value: unknown) => unknown) | null,
144
+ onRejected?: ((reason: unknown) => unknown) | null,
145
+ ) => {
146
+ const wrapped = wrapRpcReturnValue(member, prop as string);
147
+ return Promise.resolve(wrapped).then(onFulfilled, onRejected);
148
+ };
149
+ return rpcCallable;
150
+ },
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Wrap a function in a callable stub with validation + Symbol.dispose + dup().
156
+ */
157
+ export function createRpcFunctionStub(fn: Function, thisArg?: object): Function {
158
+ const stub = (...args: unknown[]) => {
159
+ warnInvalidRpcArgs(args, fn.name || "<anonymous>");
160
+ const result = fn.call(thisArg, ...args);
161
+ return Promise.resolve(result).then((r) => wrapRpcReturnValue(r, fn.name || "<anonymous>"));
162
+ };
163
+
164
+ Object.defineProperty(stub, Symbol.dispose, {
165
+ value: () => {},
166
+ writable: false,
167
+ configurable: true,
168
+ });
169
+
170
+ Object.defineProperty(stub, "dup", {
171
+ value: () => createRpcFunctionStub(fn, thisArg),
172
+ writable: false,
173
+ configurable: true,
174
+ });
175
+
176
+ return stub;
177
+ }
178
+
179
+ /**
180
+ * Wrap a Promise in an RpcPromise proxy that supports promise pipelining.
181
+ *
182
+ * `stub.getChild().childMethod()` works without intermediate await:
183
+ * - then/catch/finally → delegate to underlying promise
184
+ * - Any other property → returns a pipelined callable
185
+ */
186
+ export function createRpcPromise(promise: Promise<unknown>): Promise<unknown> {
187
+ return new Proxy(promise, {
188
+ get(target, prop: string | symbol) {
189
+ // Promise protocol — delegate to the underlying promise
190
+ if (prop === "then" || prop === "catch" || prop === "finally") {
191
+ const method = target[prop as keyof Promise<unknown>] as Function;
192
+ return method.bind(target);
193
+ }
194
+
195
+ // Symbol.dispose — no-op
196
+ if (prop === Symbol.dispose) return () => {};
197
+
198
+ // dup() — new RpcPromise wrapping same promise
199
+ if (prop === "dup") return () => createRpcPromise(promise);
200
+
201
+ // NON_RPC_PROPS (excluding then/catch/finally already handled)
202
+ if (NON_RPC_PROPS.has(prop)) return undefined;
203
+
204
+ if (typeof prop === "symbol") return undefined;
205
+
206
+ // Filter _-prefixed
207
+ if (prop.startsWith("_")) return undefined;
208
+
209
+ // Promise pipelining: property access chains through the resolved value
210
+ const pipelined = (...args: unknown[]) => {
211
+ const chained = promise.then((resolved) => {
212
+ if (resolved === null || resolved === undefined) {
213
+ throw new Error(`Cannot access "${prop}" on ${String(resolved)}`);
214
+ }
215
+ const member = (resolved as Record<string, unknown>)[prop];
216
+ if (typeof member !== "function") {
217
+ throw new Error(`"${prop}" is not a method on the resolved value`);
218
+ }
219
+ return (member as Function).call(resolved, ...args);
220
+ }).then((r) => wrapRpcReturnValue(r, prop));
221
+ return createRpcPromise(chained);
222
+ };
223
+
224
+ // Make pipelined callable also thenable for property access
225
+ pipelined.then = (
226
+ onFulfilled?: ((value: unknown) => unknown) | null,
227
+ onRejected?: ((reason: unknown) => unknown) | null,
228
+ ) => {
229
+ const chained = promise.then((resolved) => {
230
+ if (resolved === null || resolved === undefined) {
231
+ return undefined;
232
+ }
233
+ const member = (resolved as Record<string, unknown>)[prop];
234
+ if (typeof member === "function") {
235
+ return createRpcFunctionStub(member as Function, resolved as object);
236
+ }
237
+ return wrapRpcReturnValue(member, prop);
238
+ });
239
+ return chained.then(onFulfilled, onRejected);
240
+ };
241
+
242
+ return pipelined;
243
+ },
244
+ }) as Promise<unknown>;
245
+ }
246
+
247
+ /**
248
+ * Inspect a return value and wrap it appropriately:
249
+ * - RpcTarget instance → createRpcStub()
250
+ * - Function → createRpcFunctionStub()
251
+ * - Otherwise → warn if invalid + pass through
252
+ */
253
+ export function wrapRpcReturnValue(value: unknown, context: string): unknown {
254
+ if (value === null || value === undefined) return value;
255
+
256
+ if (isRpcTarget(value)) {
257
+ return createRpcStub(value as object);
258
+ }
259
+
260
+ if (typeof value === "function") {
261
+ return createRpcFunctionStub(value as Function);
262
+ }
263
+
264
+ // Not an RpcTarget or function — validate and pass through
265
+ warnInvalidRpcReturn(value, context);
266
+ return value;
267
+ }
@@ -0,0 +1,172 @@
1
+ import { ExecutionContext } from "../execution-context";
2
+ import { startSpan, persistError } from "../tracing/span";
3
+
4
+ export interface ScheduledController {
5
+ readonly scheduledTime: number;
6
+ readonly cron: string;
7
+ readonly type: "scheduled";
8
+ noRetry(): void;
9
+ }
10
+
11
+ interface CronField {
12
+ type: "any" | "values";
13
+ values: number[];
14
+ }
15
+
16
+ interface ParsedCron {
17
+ expression: string;
18
+ minute: CronField;
19
+ hour: CronField;
20
+ dayOfMonth: CronField;
21
+ month: CronField;
22
+ dayOfWeek: CronField;
23
+ }
24
+
25
+ const DAY_NAMES: Record<string, number> = {
26
+ SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
27
+ };
28
+
29
+ const MONTH_NAMES: Record<string, number> = {
30
+ JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
31
+ JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,
32
+ };
33
+
34
+ const SPECIAL_CRONS: Record<string, string> = {
35
+ "@daily": "0 0 * * *",
36
+ "@midnight": "0 0 * * *",
37
+ "@hourly": "0 * * * *",
38
+ "@weekly": "0 0 * * 0",
39
+ "@monthly": "0 0 1 * *",
40
+ "@yearly": "0 0 1 1 *",
41
+ "@annually": "0 0 1 1 *",
42
+ };
43
+
44
+ function resolveToken(token: string, names: Record<string, number> | null): number {
45
+ if (names) {
46
+ const upper = token.toUpperCase();
47
+ if (upper in names) return names[upper]!;
48
+ }
49
+ return parseInt(token, 10);
50
+ }
51
+
52
+ function parseField(field: string, min: number, max: number, names: Record<string, number> | null = null): CronField {
53
+ if (field === "*") {
54
+ return { type: "any", values: [] };
55
+ }
56
+
57
+ const values: number[] = [];
58
+
59
+ for (const part of field.split(",")) {
60
+ const stepMatch = part.match(/^(\*|([a-zA-Z0-9]+)-([a-zA-Z0-9]+))\/(\d+)$/);
61
+ if (stepMatch) {
62
+ const step = parseInt(stepMatch[4]!, 10);
63
+ const start = stepMatch[1] === "*" ? min : resolveToken(stepMatch[2]!, names);
64
+ const end = stepMatch[1] === "*" ? max : resolveToken(stepMatch[3]!, names);
65
+ for (let i = start; i <= end; i += step) {
66
+ values.push(i);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ const rangeMatch = part.match(/^([a-zA-Z0-9]+)-([a-zA-Z0-9]+)$/);
72
+ if (rangeMatch) {
73
+ const start = resolveToken(rangeMatch[1]!, names);
74
+ const end = resolveToken(rangeMatch[2]!, names);
75
+ for (let i = start; i <= end; i++) {
76
+ values.push(i);
77
+ }
78
+ continue;
79
+ }
80
+
81
+ values.push(resolveToken(part, names));
82
+ }
83
+
84
+ return { type: "values", values };
85
+ }
86
+
87
+ export function parseCron(expression: string): ParsedCron {
88
+ const trimmed = expression.trim();
89
+
90
+ // Handle special cron strings
91
+ const special = SPECIAL_CRONS[trimmed.toLowerCase()];
92
+ if (special) {
93
+ const parsed = parseCron(special);
94
+ parsed.expression = trimmed;
95
+ return parsed;
96
+ }
97
+
98
+ const parts = trimmed.split(/\s+/);
99
+ if (parts.length !== 5) {
100
+ throw new Error(`Invalid cron expression: "${expression}" (expected 5 fields)`);
101
+ }
102
+
103
+ return {
104
+ expression: trimmed,
105
+ minute: parseField(parts[0]!, 0, 59),
106
+ hour: parseField(parts[1]!, 0, 23),
107
+ dayOfMonth: parseField(parts[2]!, 1, 31),
108
+ month: parseField(parts[3]!, 1, 12, MONTH_NAMES),
109
+ dayOfWeek: parseField(parts[4]!, 0, 6, DAY_NAMES),
110
+ };
111
+ }
112
+
113
+ function fieldMatches(field: CronField, value: number): boolean {
114
+ if (field.type === "any") return true;
115
+ return field.values.includes(value);
116
+ }
117
+
118
+ export function cronMatchesDate(cron: ParsedCron, date: Date): boolean {
119
+ return (
120
+ fieldMatches(cron.minute, date.getMinutes()) &&
121
+ fieldMatches(cron.hour, date.getHours()) &&
122
+ fieldMatches(cron.dayOfMonth, date.getDate()) &&
123
+ fieldMatches(cron.month, date.getMonth() + 1) &&
124
+ fieldMatches(cron.dayOfWeek, date.getDay())
125
+ );
126
+ }
127
+
128
+ export function createScheduledController(cron: string, scheduledTime: number): ScheduledController {
129
+ return {
130
+ scheduledTime,
131
+ cron,
132
+ type: "scheduled",
133
+ noRetry() {},
134
+ };
135
+ }
136
+
137
+ type ScheduledHandler = (controller: ScheduledController, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
138
+
139
+ export function startCronScheduler(
140
+ crons: string[],
141
+ handler: ScheduledHandler,
142
+ env: Record<string, unknown>,
143
+ workerName?: string,
144
+ ): NodeJS.Timer {
145
+ const parsed = crons.map(parseCron);
146
+
147
+ // Check every 60 seconds, aligned to the start of each minute
148
+ const interval = setInterval(() => {
149
+ const now = new Date();
150
+ for (const cron of parsed) {
151
+ if (cronMatchesDate(cron, now)) {
152
+ const controller = createScheduledController(cron.expression, now.getTime());
153
+ const ctx = new ExecutionContext();
154
+ console.log(`[bunflare] Cron triggered: ${cron.expression}`);
155
+ startSpan({
156
+ name: "scheduled",
157
+ kind: "server",
158
+ attributes: { cron: cron.expression },
159
+ workerName,
160
+ }, async () => {
161
+ await handler(controller, env, ctx);
162
+ await ctx._awaitAll();
163
+ }).catch((err) => {
164
+ console.error(`[bunflare] Scheduled handler error (${cron.expression}):`, err);
165
+ persistError(err, "scheduled", workerName);
166
+ });
167
+ }
168
+ }
169
+ }, 60_000);
170
+
171
+ return interval;
172
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Service Binding — worker-to-worker communication via HTTP fetch and RPC.
3
+ *
4
+ * The binding is a Proxy that supports:
5
+ * - `.fetch(request | url, init?)` — calls the target worker's fetch() handler
6
+ * - `.myMethod(args)` — RPC call to the target's entrypoint class method (always returns Promise)
7
+ * - `.myProperty` — RPC property access (returns thenable/Promise)
8
+ * - `.connect()` — stub for TCP socket (throws — not supported in dev)
9
+ */
10
+
11
+ import { ExecutionContext } from "../execution-context";
12
+ import { getActiveContext, runWithContext } from "../tracing/context";
13
+ import { warnInvalidRpcArgs } from "../rpc-validate";
14
+ import { NON_RPC_PROPS, wrapRpcReturnValue, createRpcFunctionStub } from "./rpc-stub";
15
+
16
+ type WorkerModule = Record<string, unknown>;
17
+
18
+ export interface ServiceBindingLimits {
19
+ /** Max subrequests per top-level request (CF default: 1000 for workers, 32 for service bindings) */
20
+ maxSubrequests?: number;
21
+ /** Max payload size in bytes for RPC arguments (CF default: 32 MiB) */
22
+ maxRpcPayloadSize?: number;
23
+ }
24
+
25
+ const SERVICE_BINDING_DEFAULTS: Required<ServiceBindingLimits> = {
26
+ maxSubrequests: 1000,
27
+ maxRpcPayloadSize: 32 * 1024 * 1024,
28
+ };
29
+
30
+ // Internal properties that should be forwarded to the ServiceBinding instance
31
+ const INTERNAL_PROPS = new Set(["_wire", "isWired", "_subrequestCount"]);
32
+
33
+ export class ServiceBinding {
34
+ private _resolver: (() => { workerModule: Record<string, unknown>; env: Record<string, unknown> }) | null = null;
35
+ private _entrypoint: string | undefined;
36
+ private _serviceName: string;
37
+ private _limits: Required<ServiceBindingLimits>;
38
+ _subrequestCount: number = 0;
39
+
40
+ constructor(serviceName: string, entrypoint?: string, limits?: ServiceBindingLimits) {
41
+ this._serviceName = serviceName;
42
+ this._entrypoint = entrypoint;
43
+ this._limits = { ...SERVICE_BINDING_DEFAULTS, ...limits };
44
+ }
45
+
46
+ _wire(resolverOrModule: (() => { workerModule: Record<string, unknown>; env: Record<string, unknown> }) | Record<string, unknown>, env?: Record<string, unknown>): void {
47
+ if (typeof resolverOrModule === "function" && env === undefined) {
48
+ // New API: resolver function
49
+ this._resolver = resolverOrModule as () => { workerModule: Record<string, unknown>; env: Record<string, unknown> };
50
+ } else {
51
+ // Legacy API: _wire(workerModule, env)
52
+ const workerModule = resolverOrModule as Record<string, unknown>;
53
+ const capturedEnv = env!;
54
+ this._resolver = () => ({ workerModule, env: capturedEnv });
55
+ }
56
+ }
57
+
58
+ get isWired(): boolean {
59
+ return this._resolver !== null;
60
+ }
61
+
62
+ private _resolve(): { workerModule: Record<string, unknown>; env: Record<string, unknown> } {
63
+ if (!this._resolver) {
64
+ throw new Error(`Service binding "${this._serviceName}" is not wired — target worker not loaded`);
65
+ }
66
+ return this._resolver();
67
+ }
68
+
69
+ private _checkSubrequestLimit(): void {
70
+ this._subrequestCount++;
71
+ if (this._subrequestCount > this._limits.maxSubrequests) {
72
+ throw new Error(
73
+ `Service binding "${this._serviceName}": subrequest limit exceeded (max ${this._limits.maxSubrequests})`
74
+ );
75
+ }
76
+ }
77
+
78
+ private _getTarget(ctx?: ExecutionContext): Record<string, unknown> {
79
+ const { workerModule, env } = this._resolve();
80
+ if (this._entrypoint) {
81
+ const cls = workerModule[this._entrypoint] as (new (...args: unknown[]) => Record<string, unknown>) | undefined;
82
+ if (!cls) {
83
+ throw new Error(`Entrypoint "${this._entrypoint}" not exported from worker module`);
84
+ }
85
+ return new cls(ctx ?? new ExecutionContext(), env);
86
+ }
87
+ // Default export: could be class-based or object-based
88
+ const def = workerModule.default;
89
+ if (typeof def === "function" && def.prototype && typeof def.prototype.fetch === "function") {
90
+ return new (def as new (ctx: ExecutionContext, env: unknown) => Record<string, unknown>)(ctx ?? new ExecutionContext(), env);
91
+ }
92
+ return def as Record<string, unknown>;
93
+ }
94
+
95
+ async fetch(input: Request | string | URL, init?: RequestInit): Promise<Response> {
96
+ this._checkSubrequestLimit();
97
+ const execCtx = new ExecutionContext();
98
+ const target = this._getTarget(execCtx);
99
+ if (!target?.fetch || typeof target.fetch !== "function") {
100
+ throw new Error(`Service binding "${this._serviceName}" target has no fetch() handler`);
101
+ }
102
+ const url = input instanceof URL ? input.toString() : input;
103
+ const request = typeof url === "string" ? new Request(url, init) : url;
104
+ // Class-based entrypoints receive (request) — env/ctx via constructor
105
+ // Object-based entrypoints receive (request, env, ctx)
106
+ const { workerModule, env } = this._resolve();
107
+ const def = workerModule.default;
108
+ const isClass = (this._entrypoint || (typeof def === "function" && def.prototype?.fetch));
109
+
110
+ // Propagate trace context to target worker so child spans link correctly
111
+ const parentCtx = getActiveContext();
112
+ const doCall = async () => {
113
+ const response = isClass
114
+ ? await (target.fetch as (r: Request) => Promise<Response>)(request)
115
+ : await (target.fetch as (r: Request, e: unknown, c: ExecutionContext) => Promise<Response>)(request, env, execCtx);
116
+ execCtx._awaitAll().catch(() => {});
117
+ return response;
118
+ };
119
+
120
+ if (parentCtx) {
121
+ return runWithContext(parentCtx, doCall);
122
+ }
123
+ return doCall();
124
+ }
125
+
126
+ connect(_address: string | { hostname: string; port: number }): never {
127
+ throw new Error(
128
+ `Service binding "${this._serviceName}": connect() (TCP sockets) is not supported in local dev mode`
129
+ );
130
+ }
131
+
132
+ toProxy(): Record<string, unknown> {
133
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
134
+ const self = this;
135
+ return new Proxy({} as Record<string, unknown>, {
136
+ get(_obj, prop: string | symbol) {
137
+ if (typeof prop === "symbol") {
138
+ if (NON_RPC_PROPS.has(prop)) return undefined;
139
+ return undefined;
140
+ }
141
+
142
+ if (prop === "fetch") {
143
+ return self.fetch.bind(self);
144
+ }
145
+ if (prop === "connect") {
146
+ return self.connect.bind(self);
147
+ }
148
+ if (INTERNAL_PROPS.has(prop)) {
149
+ const val = (self as unknown as Record<string, unknown>)[prop];
150
+ if (typeof val === "function") return val.bind(self);
151
+ return val;
152
+ }
153
+ // Non-RPC props should not trigger proxy behavior
154
+ if (NON_RPC_PROPS.has(prop)) {
155
+ return undefined;
156
+ }
157
+
158
+ // RPC: return a callable that also acts as a thenable for property access
159
+ // If called as a function → RPC method call (always returns Promise)
160
+ // If awaited → RPC property read (returns Promise of the property value)
161
+ const rpcCallable = (...args: unknown[]) => {
162
+ self._checkSubrequestLimit();
163
+ warnInvalidRpcArgs(args, prop);
164
+ const target = self._getTarget();
165
+ const member = target[prop];
166
+ if (typeof member !== "function") {
167
+ throw new Error(`Service binding "${self._serviceName}": "${prop}" is not a method on the target`);
168
+ }
169
+ // Propagate trace context so child spans link correctly
170
+ const parentCtx = getActiveContext();
171
+ const doCall = () => (member as (...a: unknown[]) => unknown).call(target, ...args);
172
+ const result = parentCtx
173
+ ? runWithContext(parentCtx, doCall)
174
+ : doCall();
175
+ // CF always wraps in Promise for async consistency
176
+ return Promise.resolve(result).then((r) => wrapRpcReturnValue(r, prop));
177
+ };
178
+
179
+ // Make it thenable for property access: `await binding.prop`
180
+ rpcCallable.then = (
181
+ onFulfilled?: ((value: unknown) => unknown) | null,
182
+ onRejected?: ((reason: unknown) => unknown) | null,
183
+ ) => {
184
+ self._checkSubrequestLimit();
185
+ const promise = new Promise<unknown>((resolve, reject) => {
186
+ try {
187
+ const target = self._getTarget();
188
+ const member = target[prop];
189
+ if (typeof member === "function") {
190
+ resolve(createRpcFunctionStub(member as Function, target));
191
+ } else {
192
+ resolve(wrapRpcReturnValue(member, prop));
193
+ }
194
+ } catch (e) {
195
+ reject(e);
196
+ }
197
+ });
198
+ return promise.then(onFulfilled, onRejected);
199
+ };
200
+
201
+ return rpcCallable;
202
+ },
203
+ });
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Create a service binding proxy.
209
+ */
210
+ export function createServiceBinding(
211
+ serviceName: string,
212
+ entrypoint?: string,
213
+ limits?: ServiceBindingLimits,
214
+ ): Record<string, unknown> {
215
+ const binding = new ServiceBinding(serviceName, entrypoint, limits);
216
+ return binding.toProxy();
217
+ }