@positronic/cloudflare 0.0.3 → 0.0.5

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 (52) hide show
  1. package/dist/src/api.js +1270 -0
  2. package/dist/src/brain-runner-do.js +654 -0
  3. package/dist/src/dev-server.js +1357 -0
  4. package/{src/index.ts → dist/src/index.js} +1 -6
  5. package/dist/src/manifest.js +278 -0
  6. package/dist/src/monitor-do.js +408 -0
  7. package/{src/node-index.ts → dist/src/node-index.js} +3 -7
  8. package/dist/src/r2-loader.js +207 -0
  9. package/dist/src/schedule-do.js +705 -0
  10. package/dist/src/sqlite-adapter.js +69 -0
  11. package/dist/types/api.d.ts +21 -0
  12. package/dist/types/api.d.ts.map +1 -0
  13. package/dist/types/brain-runner-do.d.ts +25 -0
  14. package/dist/types/brain-runner-do.d.ts.map +1 -0
  15. package/dist/types/dev-server.d.ts +45 -0
  16. package/dist/types/dev-server.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +7 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/manifest.d.ts +11 -0
  20. package/dist/types/manifest.d.ts.map +1 -0
  21. package/dist/types/monitor-do.d.ts +16 -0
  22. package/dist/types/monitor-do.d.ts.map +1 -0
  23. package/dist/types/node-index.d.ts +10 -0
  24. package/dist/types/node-index.d.ts.map +1 -0
  25. package/dist/types/r2-loader.d.ts +10 -0
  26. package/dist/types/r2-loader.d.ts.map +1 -0
  27. package/dist/types/schedule-do.d.ts +47 -0
  28. package/dist/types/schedule-do.d.ts.map +1 -0
  29. package/dist/types/sqlite-adapter.d.ts +10 -0
  30. package/dist/types/sqlite-adapter.d.ts.map +1 -0
  31. package/package.json +5 -1
  32. package/src/api.ts +0 -579
  33. package/src/brain-runner-do.ts +0 -309
  34. package/src/dev-server.ts +0 -776
  35. package/src/manifest.ts +0 -69
  36. package/src/monitor-do.ts +0 -268
  37. package/src/r2-loader.ts +0 -27
  38. package/src/schedule-do.ts +0 -377
  39. package/src/sqlite-adapter.ts +0 -50
  40. package/test-project/package-lock.json +0 -3010
  41. package/test-project/package.json +0 -21
  42. package/test-project/src/index.ts +0 -70
  43. package/test-project/src/runner.ts +0 -24
  44. package/test-project/tests/api.test.ts +0 -1005
  45. package/test-project/tests/r2loader.test.ts +0 -73
  46. package/test-project/tests/resources-api.test.ts +0 -671
  47. package/test-project/tests/spec.test.ts +0 -135
  48. package/test-project/tests/tsconfig.json +0 -7
  49. package/test-project/tsconfig.json +0 -20
  50. package/test-project/vitest.config.ts +0 -12
  51. package/test-project/wrangler.jsonc +0 -53
  52. package/tsconfig.json +0 -11
@@ -1,377 +0,0 @@
1
- import { DurableObject } from 'cloudflare:workers';
2
- import { v4 as uuidv4 } from 'uuid';
3
- import { parseCronExpression, type Cron } from 'cron-schedule';
4
- import { BRAIN_EVENTS, type BrainEvent } from '@positronic/core';
5
- import type { BrainRunnerDO } from './brain-runner-do.js';
6
-
7
- export interface Env {
8
- BRAIN_RUNNER_DO: DurableObjectNamespace<BrainRunnerDO>;
9
- IS_TEST?: string;
10
- NODE_ENV?: string;
11
- }
12
-
13
- interface Schedule {
14
- id: string;
15
- brainName: string;
16
- cronExpression: string;
17
- enabled: boolean;
18
- createdAt: number;
19
- nextRunAt?: number;
20
- }
21
-
22
- interface ScheduledRun {
23
- id: number;
24
- scheduleId: string;
25
- brainRunId?: string;
26
- status: 'triggered' | 'failed' | 'complete';
27
- ranAt: number;
28
- completedAt?: number;
29
- error?: string;
30
- }
31
-
32
- const ALARM_INTERVAL = 60 * 1000;
33
-
34
- export class ScheduleDO extends DurableObject<Env> {
35
- private readonly storage: SqlStorage;
36
-
37
- constructor(state: DurableObjectState, env: Env) {
38
- super(state, env);
39
- this.storage = state.storage.sql;
40
-
41
- // Initialize database schema
42
- this.storage.exec(`
43
- CREATE TABLE IF NOT EXISTS schedules (
44
- id TEXT PRIMARY KEY,
45
- brain_name TEXT NOT NULL,
46
- cron_expression TEXT NOT NULL,
47
- enabled INTEGER NOT NULL DEFAULT 1,
48
- created_at INTEGER NOT NULL,
49
- next_run_at INTEGER
50
- );
51
-
52
- CREATE INDEX IF NOT EXISTS idx_schedules_brain
53
- ON schedules(brain_name);
54
-
55
- CREATE INDEX IF NOT EXISTS idx_schedules_enabled
56
- ON schedules(enabled);
57
-
58
- CREATE TABLE IF NOT EXISTS scheduled_runs (
59
- id INTEGER PRIMARY KEY AUTOINCREMENT,
60
- schedule_id TEXT NOT NULL,
61
- brain_run_id TEXT UNIQUE,
62
- status TEXT NOT NULL CHECK(status IN ('triggered', 'failed', 'complete')),
63
- ran_at INTEGER NOT NULL,
64
- completed_at INTEGER,
65
- error TEXT,
66
- FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE CASCADE
67
- );
68
-
69
- CREATE INDEX IF NOT EXISTS idx_runs_schedule
70
- ON scheduled_runs(schedule_id, ran_at DESC);
71
- `);
72
- }
73
-
74
- async createSchedule(
75
- brainName: string,
76
- cronExpression: string
77
- ): Promise<Schedule> {
78
- const id = uuidv4();
79
- const createdAt = Date.now();
80
- if (this.env.IS_TEST !== 'true') {
81
- const alarm = await this.ctx.storage.getAlarm();
82
- if (!alarm) {
83
- await this.ctx.storage.setAlarm(Date.now() + ALARM_INTERVAL);
84
- }
85
- }
86
- // Note: Cron expression is validated at the API level before calling this method
87
- // Calculate next run time
88
- const cron = parseCronExpression(cronExpression);
89
- const nextRunAt = this.calculateNextRunTime(cron, createdAt);
90
-
91
- this.storage.exec(
92
- `INSERT INTO schedules (id, brain_name, cron_expression, enabled, created_at, next_run_at)
93
- VALUES (?, ?, ?, 1, ?, ?)`,
94
- id,
95
- brainName,
96
- cronExpression,
97
- createdAt,
98
- nextRunAt
99
- );
100
-
101
- return {
102
- id,
103
- brainName,
104
- cronExpression,
105
- enabled: true,
106
- createdAt,
107
- nextRunAt,
108
- };
109
- }
110
-
111
- async getSchedule(scheduleId: string): Promise<Schedule | null> {
112
- const results = this.storage
113
- .exec(
114
- `SELECT id, brain_name, cron_expression, enabled, created_at, next_run_at
115
- FROM schedules WHERE id = ?`,
116
- scheduleId
117
- )
118
- .toArray();
119
-
120
- if (results.length === 0) {
121
- return null;
122
- }
123
-
124
- const result = results[0];
125
-
126
- return {
127
- id: result.id as string,
128
- brainName: result.brain_name as string,
129
- cronExpression: result.cron_expression as string,
130
- enabled: result.enabled === 1,
131
- createdAt: result.created_at as number,
132
- nextRunAt: result.next_run_at as number | undefined,
133
- };
134
- }
135
-
136
- async deleteSchedule(scheduleId: string): Promise<boolean> {
137
- // Check if schedule exists first
138
- const existing = await this.getSchedule(scheduleId);
139
- if (!existing) {
140
- return false;
141
- }
142
-
143
- this.storage.exec(`DELETE FROM schedules WHERE id = ?`, scheduleId);
144
-
145
- return true;
146
- }
147
-
148
- async listSchedules(): Promise<{ schedules: Schedule[]; count: number }> {
149
- if (this.env.NODE_ENV === 'development') {
150
- console.log('[ScheduleDO] Checking alarm');
151
- const alarm = await this.ctx.storage.getAlarm();
152
- if (alarm) {
153
- console.log('[ScheduleDO] Deleting alarm');
154
- await this.ctx.storage.deleteAlarm();
155
- console.log('[ScheduleDO] Running alarm handler');
156
- await this.alarm();
157
- }
158
- }
159
-
160
- const schedules = this.storage
161
- .exec(
162
- `SELECT id, brain_name, cron_expression, enabled, created_at, next_run_at
163
- FROM schedules
164
- ORDER BY created_at DESC`
165
- )
166
- .toArray()
167
- .map((row) => ({
168
- id: row.id as string,
169
- brainName: row.brain_name as string,
170
- cronExpression: row.cron_expression as string,
171
- enabled: row.enabled === 1,
172
- createdAt: row.created_at as number,
173
- nextRunAt: row.next_run_at as number | undefined,
174
- }));
175
-
176
- return {
177
- schedules,
178
- count: schedules.length,
179
- };
180
- }
181
-
182
- async getAllRuns(
183
- scheduleId?: string,
184
- limit: number = 100
185
- ): Promise<{ runs: ScheduledRun[]; count: number }> {
186
- let query = `
187
- SELECT id, schedule_id, brain_run_id, status, ran_at, completed_at, error
188
- FROM scheduled_runs
189
- `;
190
- const params: any[] = [];
191
-
192
- if (scheduleId) {
193
- query += ` WHERE schedule_id = ?`;
194
- params.push(scheduleId);
195
- }
196
-
197
- query += ` ORDER BY ran_at DESC LIMIT ?`;
198
- params.push(limit);
199
-
200
- const runs = this.storage
201
- .exec(query, ...params)
202
- .toArray()
203
- .map((row) => ({
204
- id: row.id as number,
205
- scheduleId: row.schedule_id as string,
206
- brainRunId: row.brain_run_id as string | undefined,
207
- status: row.status as 'triggered' | 'failed' | 'complete',
208
- ranAt: row.ran_at as number,
209
- completedAt: row.completed_at as number | undefined,
210
- error: row.error as string | undefined,
211
- }));
212
-
213
- // Get total count
214
- let countQuery = `SELECT COUNT(*) as count FROM scheduled_runs`;
215
- const countParams: any[] = [];
216
-
217
- if (scheduleId) {
218
- countQuery += ` WHERE schedule_id = ?`;
219
- countParams.push(scheduleId);
220
- }
221
-
222
- const countResult = this.storage.exec(countQuery, ...countParams).one();
223
- const count = (countResult?.count as number) || 0;
224
-
225
- return { runs, count };
226
- }
227
-
228
- // Handle the alarm trigger - runs every minute in a perpetual cycle
229
- async alarm(): Promise<void> {
230
- try {
231
- // This alarm runs every minute to check for schedules that need to be executed.
232
- // Since cron expressions have minute-level granularity at most (e.g., * * * * *),
233
- // checking every minute ensures we never miss a scheduled run.
234
-
235
- // Get all enabled schedules that are due
236
- const now = Date.now();
237
-
238
- const dueSchedules = this.storage
239
- .exec(
240
- `SELECT id, brain_name, cron_expression
241
- FROM schedules
242
- WHERE enabled = 1 AND next_run_at <= ?`,
243
- now
244
- )
245
- .toArray();
246
-
247
- // Process each due schedule
248
- for (const schedule of dueSchedules) {
249
- const scheduleId = schedule.id as string;
250
- const brainName = schedule.brain_name as string;
251
- const cronExpression = schedule.cron_expression as string;
252
-
253
- try {
254
- // Trigger the brain run
255
- const brainRunId = await this.triggerBrainRun(brainName);
256
-
257
- // Record successful run
258
- this.storage.exec(
259
- `INSERT INTO scheduled_runs (schedule_id, brain_run_id, status, ran_at)
260
- VALUES (?, ?, 'triggered', ?)`,
261
- scheduleId,
262
- brainRunId,
263
- now
264
- );
265
- } catch (error) {
266
- // Record failed run
267
- const errorMessage =
268
- error instanceof Error ? error.message : 'Unknown error';
269
- this.storage.exec(
270
- `INSERT INTO scheduled_runs (schedule_id, status, ran_at, error)
271
- VALUES (?, 'failed', ?, ?)`,
272
- scheduleId,
273
- now,
274
- errorMessage
275
- );
276
-
277
- console.error(
278
- `[ScheduleDO] Failed to trigger brain ${brainName}:`,
279
- error
280
- );
281
- }
282
-
283
- // Calculate and update next run time
284
- const cron = parseCronExpression(cronExpression);
285
- const nextRunAt = this.calculateNextRunTime(cron, now);
286
-
287
- this.storage.exec(
288
- `UPDATE schedules SET next_run_at = ? WHERE id = ?`,
289
- nextRunAt,
290
- scheduleId
291
- );
292
- }
293
- } finally {
294
- // Always schedule the next alarm for 1 minute from now
295
- // This creates a perpetual cycle that checks for due schedules every minute
296
- // The finally block ensures this happens even if there's an error above
297
- // Skip in test environment to avoid isolated storage issues
298
- if (this.env.IS_TEST !== 'true') {
299
- await this.ctx.storage.setAlarm(Date.now() + ALARM_INTERVAL);
300
- }
301
- }
302
- }
303
-
304
- private async triggerBrainRun(brainName: string): Promise<string> {
305
- const brainRunId = uuidv4();
306
- const namespace = this.env.BRAIN_RUNNER_DO;
307
- const doId = namespace.idFromName(brainRunId);
308
- const stub = namespace.get(doId);
309
- console.log(
310
- `[ScheduleDO] Triggering brain run ${brainName} with id ${brainRunId}`
311
- );
312
- await stub.start(brainName, brainRunId);
313
-
314
- return brainRunId;
315
- }
316
-
317
- // Called by ScheduleAdapter when brain events occur
318
- async handleBrainEvent(event: BrainEvent<any>): Promise<void> {
319
- // We only care about completion events for scheduled runs
320
- if (
321
- event.type !== BRAIN_EVENTS.COMPLETE &&
322
- event.type !== BRAIN_EVENTS.ERROR
323
- ) {
324
- return;
325
- }
326
-
327
- // Check if this brain run was triggered by a schedule
328
- const result = this.storage
329
- .exec(
330
- `SELECT id FROM scheduled_runs WHERE brain_run_id = ?`,
331
- event.brainRunId
332
- )
333
- .toArray();
334
-
335
- if (result.length === 0) {
336
- // This brain run wasn't triggered by a schedule, ignore it
337
- return;
338
- }
339
-
340
- const scheduledRun = result[0];
341
-
342
- const completedAt = Date.now();
343
- const status = event.type === BRAIN_EVENTS.COMPLETE ? 'complete' : 'failed';
344
- const error =
345
- event.type === BRAIN_EVENTS.ERROR
346
- ? event.error
347
- ? JSON.stringify(event.error)
348
- : 'Unknown error'
349
- : null;
350
-
351
- // Update the scheduled run record
352
- this.storage.exec(
353
- `UPDATE scheduled_runs
354
- SET status = ?, completed_at = ?, error = ?
355
- WHERE brain_run_id = ?`,
356
- status,
357
- completedAt,
358
- error,
359
- event.brainRunId
360
- );
361
- }
362
-
363
- private isValidCronExpression(expression: string): boolean {
364
- try {
365
- // Try to parse the expression - if it throws, it's invalid
366
- parseCronExpression(expression);
367
- return true;
368
- } catch (error) {
369
- return false;
370
- }
371
- }
372
-
373
- private calculateNextRunTime(cron: Cron, afterTime: number): number {
374
- const nextDate = cron.getNextDate(new Date(afterTime));
375
- return nextDate.getTime();
376
- }
377
- }
@@ -1,50 +0,0 @@
1
- import type { Adapter, BrainEvent } from '@positronic/core';
2
- import type { SqlStorage } from '@cloudflare/workers-types';
3
-
4
- // Define the new schema with a single events table
5
- const initSQL = `
6
- CREATE TABLE IF NOT EXISTS brain_events (
7
- event_id INTEGER PRIMARY KEY AUTOINCREMENT,
8
- event_type TEXT NOT NULL,
9
- serialized_event TEXT NOT NULL CHECK(json_valid(serialized_event)),
10
- timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
11
- );
12
- `;
13
-
14
- export class BrainRunSQLiteAdapter implements Adapter {
15
- private sql: SqlStorage;
16
- private schemaInitialized = false; // Track schema initialization
17
-
18
- constructor(sql: SqlStorage) {
19
- this.sql = sql;
20
- }
21
-
22
- private initializeSchema() {
23
- if (!this.schemaInitialized) {
24
- this.sql.exec(initSQL);
25
- this.schemaInitialized = true;
26
- }
27
- }
28
-
29
- public dispatch(event: BrainEvent) {
30
- try {
31
- this.initializeSchema();
32
-
33
- const insertSql = `
34
- INSERT INTO brain_events (
35
- event_type,
36
- serialized_event
37
- ) VALUES (?, ?);`;
38
-
39
- this.sql.exec(insertSql, event.type, JSON.stringify(event));
40
- } catch (e) {
41
- console.error(
42
- '[SQL_ADAPTER] Error handling brain event:',
43
- e,
44
- 'Event data:',
45
- JSON.stringify(event)
46
- );
47
- throw e;
48
- }
49
- }
50
- }