From 8a3750360de39fc8857d15d4b10963751deb663f Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:26:26 -0700 Subject: [PATCH 01/29] feat: Add Supabase Durable Object with stats endpoint and cache mechanism --- src/config.ts | 2 +- src/durable-objects/supabase-do.ts | 117 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/durable-objects/supabase-do.ts diff --git a/src/config.ts b/src/config.ts index 200808c..d3b75ab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ export const APP_CONFIG = { // supported services for API caching // each entry is a durable object that handles requests - SUPPORTED_SERVICES: ['/hiro-api'], + SUPPORTED_SERVICES: ['/hiro-api', '/supabase'], // VALUES BELOW CAN BE OVERRIDDEN BY DURABLE OBJECTS // default cache TTL used for KV CACHE_TTL: 900, // 15 minutes diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts new file mode 100644 index 0000000..41a5e68 --- /dev/null +++ b/src/durable-objects/supabase-do.ts @@ -0,0 +1,117 @@ +import { DurableObject } from 'cloudflare:workers'; +import { createClient } from '@supabase/supabase-js'; +import { Env } from '../../worker-configuration'; +import { APP_CONFIG } from '../config'; + +/** + * Durable Object class for Supabase queries + */ +export class SupabaseDO extends DurableObject { + private readonly CACHE_TTL: number = APP_CONFIG.CACHE_TTL; + private readonly ALARM_INTERVAL_MS = 60000; // 1 minute + private readonly BASE_PATH: string = '/supabase'; + private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); + private readonly SUPPORTED_PATHS: string[] = ['/stats']; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx = ctx; + this.env = env; + + // Set up alarm to run at configured interval + ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + + async alarm(): Promise { + const startTime = Date.now(); + try { + // TODO: Implement Supabase query and cache update + console.log('Updating Supabase stats cache...'); + + const endTime = Date.now(); + console.log(`supabase-do: alarm executed in ${endTime - startTime}ms`); + } catch (error) { + console.error(`Alarm execution failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + // Schedule next alarm + this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + + // Schedule next alarm if one isn't set + const currentAlarm = await this.ctx.storage.getAlarm(); + if (currentAlarm === null) { + this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + + // Handle requests that don't match the base path + if (!path.startsWith(this.BASE_PATH)) { + return new Response( + JSON.stringify({ + error: `Unrecognized path passed to SupabaseDO: ${path}`, + }), + { + status: 404, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // Parse requested endpoint from base path + const endpoint = path.replace(this.BASE_PATH, ''); + + // Handle root route + if (endpoint === '' || endpoint === '/') { + return new Response( + JSON.stringify({ + message: `Welcome to the Supabase cache! Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, + }), + { + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // Handle /stats endpoint + if (endpoint === '/stats') { + const cacheKey = `${this.CACHE_PREFIX}_stats`; + const cached = await this.env.AIBTCDEV_CACHE_KV.get(cacheKey); + + if (cached) { + return new Response(cached, { + headers: { 'Content-Type': 'application/json' }, + }); + } + + // TODO: Implement actual Supabase query here + const mockData = { + timestamp: new Date().toISOString(), + message: 'Stats endpoint placeholder - implement Supabase query', + }; + + const data = JSON.stringify(mockData); + await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { + expirationTtl: this.CACHE_TTL + }); + + return new Response(data, { + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Return 404 for any other endpoint + return new Response( + JSON.stringify({ + error: `Unrecognized endpoint: ${endpoint}. Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, + }), + { + status: 404, + headers: { 'Content-Type': 'application/json' }, + } + ); + } +} From c0f42b0ce69eef28f065f9237c2f5ad567f02460 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:26:57 -0700 Subject: [PATCH 02/29] feat: Add Supabase Durable Object binding and update migrations --- wrangler.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 058d665..a128ca4 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -18,11 +18,15 @@ enabled = true name = "HIRO_API_DO" class_name = "HiroApiDO" +[[durable_objects.bindings]] +name = "SUPABASE_DO" +class_name = "SupabaseDO" + # Durable Object migrations. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations [[migrations]] tag = "v1" -new_classes = ["HiroApiDO"] +new_classes = ["HiroApiDO", "SupabaseDO"] # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces From 481470f086396e8bb45f1fa60a0e6e556deb80c5 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 13:27:17 -0700 Subject: [PATCH 03/29] feat: install supabase sdk --- package-lock.json | 159 +++++++++++++++++++++++++++++++++++++++------- package.json | 3 + 2 files changed, 138 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cea1a7..0178ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "aibtcdev-api-cache", "version": "0.0.1", + "dependencies": { + "@supabase/supabase-js": "^2.46.1" + }, "devDependencies": { "@cloudflare/workers-types": "^4.20241106.0", "typescript": "^5.5.2", @@ -178,16 +181,104 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.65.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz", + "integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.3.tgz", + "integrity": "sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz", + "integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.7.tgz", + "integrity": "sha512-OLI0hiSAqQSqRpGMTUwoIWo51eUivSYlaNBgxsXZE7PSoWh12wPRdVt0psUMaUzEonSB85K21wGc7W5jHnT6uA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.46.1.tgz", + "integrity": "sha512-HiBpd8stf7M6+tlr+/82L8b2QmCjAD8ex9YdSAKU+whB/SHXXJdus1dGlqiH9Umy9ePUuxaYmVkGd9BcvBnNvg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.65.1", + "@supabase/functions-js": "2.4.3", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.16.3", + "@supabase/realtime-js": "2.10.7", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.8" } }, + "node_modules/@types/phoenix": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz", + "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1083,28 +1174,6 @@ "node": ">=14.0" } }, - "node_modules/miniflare/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/miniflare/node_modules/youch": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", @@ -1284,6 +1353,12 @@ "node": ">=0.10.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -1302,7 +1377,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unenv": { @@ -1347,6 +1421,22 @@ "dev": true, "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/workerd": { "version": "1.20241106.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241106.1.tgz", @@ -1499,6 +1589,27 @@ } } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xxhash-wasm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.0.2.tgz", diff --git a/package.json b/package.json index bc10095..67acbcf 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "@cloudflare/workers-types": "^4.20241106.0", "typescript": "^5.5.2", "wrangler": "^3.60.3" + }, + "dependencies": { + "@supabase/supabase-js": "^2.46.1" } } From 79059ae0cff169a67efcd6c1802a7d22e463fcd4 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:33:59 -0700 Subject: [PATCH 04/29] feat: Implement Supabase stats query with reusable function and interface --- src/durable-objects/supabase-do.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 41a5e68..606f987 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -1,8 +1,16 @@ import { DurableObject } from 'cloudflare:workers'; -import { createClient } from '@supabase/supabase-js'; +import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { Env } from '../../worker-configuration'; import { APP_CONFIG } from '../config'; +interface StatsResponse { + total_jobs: number; + main_chat_jobs: number; + individual_crew_jobs: number; + top_profile_stacks_addresses: string[]; + top_crew_names: string[]; +} + /** * Durable Object class for Supabase queries */ @@ -12,16 +20,27 @@ export class SupabaseDO extends DurableObject { private readonly BASE_PATH: string = '/supabase'; private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); private readonly SUPPORTED_PATHS: string[] = ['/stats']; + private supabase: SupabaseClient; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.ctx = ctx; this.env = env; + + // Initialize Supabase client + this.supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY); // Set up alarm to run at configured interval ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); } + private async fetchStats(): Promise { + const { data, error } = await this.supabase.rpc('get_usage_stats'); + + if (error) throw error; + return data[0] as StatsResponse; + } + async alarm(): Promise { const startTime = Date.now(); try { From e7f33b8913d5b5fb26a1013e1b5732c2946003c1 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:34:05 -0700 Subject: [PATCH 05/29] refactor: Replace mock data with actual Supabase stats fetching --- src/durable-objects/supabase-do.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 606f987..fb852ab 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -106,13 +106,11 @@ export class SupabaseDO extends DurableObject { }); } - // TODO: Implement actual Supabase query here - const mockData = { + const stats = await this.fetchStats(); + const data = JSON.stringify({ timestamp: new Date().toISOString(), - message: 'Stats endpoint placeholder - implement Supabase query', - }; - - const data = JSON.stringify(mockData); + ...stats + }); await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { expirationTtl: this.CACHE_TTL }); From 84bb2e71d5f9426c05beaef02d0c7362a701d0f7 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:36:11 -0700 Subject: [PATCH 06/29] fix: Improve Supabase stats fetching and caching with error handling --- src/durable-objects/supabase-do.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index fb852ab..6eb644e 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -35,18 +35,38 @@ export class SupabaseDO extends DurableObject { } private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc('get_usage_stats'); + const { data, error } = await this.supabase + .rpc('get_usage_stats') + .returns(); - if (error) throw error; - return data[0] as StatsResponse; + if (error) { + console.error('Error fetching stats:', error); + throw new Error(`Failed to fetch stats: ${error.message}`); + } + + if (!data || data.length === 0) { + throw new Error('No stats data returned from database'); + } + + return data[0]; } async alarm(): Promise { const startTime = Date.now(); try { - // TODO: Implement Supabase query and cache update console.log('Updating Supabase stats cache...'); + const stats = await this.fetchStats(); + const data = JSON.stringify({ + timestamp: new Date().toISOString(), + ...stats + }); + + const cacheKey = `${this.CACHE_PREFIX}_stats`; + await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { + expirationTtl: this.CACHE_TTL + }); + const endTime = Date.now(); console.log(`supabase-do: alarm executed in ${endTime - startTime}ms`); } catch (error) { From 7d392b7657d133193c7c59aac3f06771e5984c31 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:37:23 -0700 Subject: [PATCH 07/29] refactor: Move Supabase configuration from env to APP_CONFIG --- src/config.ts | 5 +++++ src/durable-objects/supabase-do.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index d3b75ab..944d3e0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,4 +12,9 @@ export const APP_CONFIG = { RETRY_DELAY: 1000, // multiplied by retry attempt number // how often to warm the cache, should be shorter than the cache TTL ALARM_INTERVAL_MS: 300000, // 5 minutes + // Supabase configuration + SUPABASE: { + URL: 'https://your-project.supabase.co', + ANON_KEY: 'your-anon-key' + } }; diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 6eb644e..4bdf436 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -28,7 +28,7 @@ export class SupabaseDO extends DurableObject { this.env = env; // Initialize Supabase client - this.supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY); + this.supabase = createClient(APP_CONFIG.SUPABASE.URL, APP_CONFIG.SUPABASE.ANON_KEY); // Set up alarm to run at configured interval ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); From d38fec093cf57b8ba5db8cb29ce0b89d64395737 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:38:32 -0700 Subject: [PATCH 08/29] refactor: Update Supabase config keys and client initialization --- src/config.ts | 9 ++++----- src/durable-objects/supabase-do.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index 944d3e0..71c345a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,9 +12,8 @@ export const APP_CONFIG = { RETRY_DELAY: 1000, // multiplied by retry attempt number // how often to warm the cache, should be shorter than the cache TTL ALARM_INTERVAL_MS: 300000, // 5 minutes - // Supabase configuration - SUPABASE: { - URL: 'https://your-project.supabase.co', - ANON_KEY: 'your-anon-key' - } + // Supabase URL for API requests + SUPABASE_URL: 'https://your-project.supabase.co', + // Supabase service role key for authenticated requests + SUPABASE_SERVICE_KEY: 'your-service-key' }; diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 4bdf436..118dcb6 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -28,7 +28,7 @@ export class SupabaseDO extends DurableObject { this.env = env; // Initialize Supabase client - this.supabase = createClient(APP_CONFIG.SUPABASE.URL, APP_CONFIG.SUPABASE.ANON_KEY); + this.supabase = createClient(APP_CONFIG.SUPABASE_URL, APP_CONFIG.SUPABASE_SERVICE_KEY); // Set up alarm to run at configured interval ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); From f01ad2be3184370a4410902082b39ea0678fe5ed Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 13:40:20 -0700 Subject: [PATCH 09/29] fix: intervene and actually fix the formatting --- src/config.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 71c345a..d753294 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,8 +12,7 @@ export const APP_CONFIG = { RETRY_DELAY: 1000, // multiplied by retry attempt number // how often to warm the cache, should be shorter than the cache TTL ALARM_INTERVAL_MS: 300000, // 5 minutes - // Supabase URL for API requests - SUPABASE_URL: 'https://your-project.supabase.co', - // Supabase service role key for authenticated requests - SUPABASE_SERVICE_KEY: 'your-service-key' + // supabase access settings + SUPABASE_URL: 'https://your-project.supabase.co', // supabase project URL + SUPABASE_SERVICE_KEY: 'your-service-key', // supabase service key }; From fab6b30cdad3181664472d0d5757e3d45b522d1e Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 13:44:42 -0700 Subject: [PATCH 10/29] refactor: Update Supabase config to use environment variables --- src/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index d753294..468158b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,7 +12,7 @@ export const APP_CONFIG = { RETRY_DELAY: 1000, // multiplied by retry attempt number // how often to warm the cache, should be shorter than the cache TTL ALARM_INTERVAL_MS: 300000, // 5 minutes - // supabase access settings - SUPABASE_URL: 'https://your-project.supabase.co', // supabase project URL - SUPABASE_SERVICE_KEY: 'your-service-key', // supabase service key + // supabase access settings - pulled from environment variables + SUPABASE_URL: env.SUPABASE_URL || '', // supabase project URL + SUPABASE_SERVICE_KEY: env.SUPABASE_SERVICE_KEY || '', // supabase service key }; From 96776cc48001d62fe013643da17dad14ba008d08 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 13:50:00 -0700 Subject: [PATCH 11/29] chore: update types with cf tool It removed the HIRO_API_KEY string, need to check if that has an impact. --- worker-configuration.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index fcea45e..52f163f 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,7 +1,7 @@ // Generated by Wrangler by running `wrangler types` -export interface Env { +interface Env { AIBTCDEV_CACHE_KV: KVNamespace; - HIRO_API_DO: DurableObjectNamespace /* HiroApiDO */; - HIRO_API_KEY: string; + HIRO_API_DO: DurableObjectNamespace; + SUPABASE_DO: DurableObjectNamespace /* SupabaseDO */; } From 501fb763ca6f66c8113af3a26cc68bd26504e01c Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 14:03:49 -0700 Subject: [PATCH 12/29] fix: add route to call supabase do, fix types and vars --- package.json | 1 + src/index.ts | 16 +++++++++++++++- worker-configuration.d.ts | 5 ++++- wrangler.toml | 5 +++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 67acbcf..53c6df7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "aibtcdev-api-cache", "version": "0.0.1", + "type": "module", "private": true, "scripts": { "deploy": "wrangler deploy", diff --git a/src/index.ts b/src/index.ts index 3e7ded1..44ca92c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ import { Env } from '../worker-configuration'; import { APP_CONFIG } from './config'; import { HiroApiDO } from './durable-objects/hiro-api-do'; -export { HiroApiDO }; +import { SupabaseDO } from './durable-objects/supabase-do'; + +// export the Durable Object classes we're using +export { HiroApiDO, SupabaseDO }; const supportedServices = APP_CONFIG.SUPPORTED_SERVICES; @@ -40,6 +43,17 @@ export default { return await stub.fetch(request); } + if (path.startsWith('/supabase')) { + // Create a DurableObjectId for our instance + let id: DurableObjectId = env.SUPABASE_DO.idFromName('supabase-do'); + + // Get the stub for communication + let stub = env.SUPABASE_DO.get(id); + + // Forward the request to the Durable Object + return await stub.fetch(request); + } + // Return 404 for any other path return new Response( JSON.stringify({ diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 52f163f..192d175 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -2,6 +2,9 @@ interface Env { AIBTCDEV_CACHE_KV: KVNamespace; + HIRO_API_KEY: string; + SUPABASE_URL: string; + SUPABASE_SERVICE_KEY: string; HIRO_API_DO: DurableObjectNamespace; - SUPABASE_DO: DurableObjectNamespace /* SupabaseDO */; + SUPABASE_DO: DurableObjectNamespace; } diff --git a/wrangler.toml b/wrangler.toml index a128ca4..6b088a8 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -11,6 +11,11 @@ routes = [{ pattern = "cache.aibtc.dev", custom_domain = true }] [observability] enabled = true +[vars] +HIRO_API_KEY = "available on the Hiro platform https://platform.hiro.so" +SUPABASE_URL = "from Supabase project dashboard" +SUPABASE_SERVICE_KEY = "from Supabase project dashboard" + # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects From 9e3976ea8d2f532415a3fdb104667976ef6e842c Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 14:06:01 -0700 Subject: [PATCH 13/29] chore: formatting and export config --- src/durable-objects/supabase-do.ts | 280 ++++++++++++++--------------- src/index.ts | 22 +-- worker-configuration.d.ts | 6 +- 3 files changed, 148 insertions(+), 160 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 118dcb6..4384eaf 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -4,151 +4,149 @@ import { Env } from '../../worker-configuration'; import { APP_CONFIG } from '../config'; interface StatsResponse { - total_jobs: number; - main_chat_jobs: number; - individual_crew_jobs: number; - top_profile_stacks_addresses: string[]; - top_crew_names: string[]; + total_jobs: number; + main_chat_jobs: number; + individual_crew_jobs: number; + top_profile_stacks_addresses: string[]; + top_crew_names: string[]; } /** * Durable Object class for Supabase queries */ export class SupabaseDO extends DurableObject { - private readonly CACHE_TTL: number = APP_CONFIG.CACHE_TTL; - private readonly ALARM_INTERVAL_MS = 60000; // 1 minute - private readonly BASE_PATH: string = '/supabase'; - private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); - private readonly SUPPORTED_PATHS: string[] = ['/stats']; - private supabase: SupabaseClient; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - this.ctx = ctx; - this.env = env; - - // Initialize Supabase client - this.supabase = createClient(APP_CONFIG.SUPABASE_URL, APP_CONFIG.SUPABASE_SERVICE_KEY); - - // Set up alarm to run at configured interval - ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); - } - - private async fetchStats(): Promise { - const { data, error } = await this.supabase - .rpc('get_usage_stats') - .returns(); - - if (error) { - console.error('Error fetching stats:', error); - throw new Error(`Failed to fetch stats: ${error.message}`); - } - - if (!data || data.length === 0) { - throw new Error('No stats data returned from database'); - } - - return data[0]; - } - - async alarm(): Promise { - const startTime = Date.now(); - try { - console.log('Updating Supabase stats cache...'); - - const stats = await this.fetchStats(); - const data = JSON.stringify({ - timestamp: new Date().toISOString(), - ...stats - }); - - const cacheKey = `${this.CACHE_PREFIX}_stats`; - await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { - expirationTtl: this.CACHE_TTL - }); - - const endTime = Date.now(); - console.log(`supabase-do: alarm executed in ${endTime - startTime}ms`); - } catch (error) { - console.error(`Alarm execution failed: ${error instanceof Error ? error.message : String(error)}`); - } finally { - // Schedule next alarm - this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); - } - } - - async fetch(request: Request): Promise { - const url = new URL(request.url); - const path = url.pathname; - - // Schedule next alarm if one isn't set - const currentAlarm = await this.ctx.storage.getAlarm(); - if (currentAlarm === null) { - this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); - } - - // Handle requests that don't match the base path - if (!path.startsWith(this.BASE_PATH)) { - return new Response( - JSON.stringify({ - error: `Unrecognized path passed to SupabaseDO: ${path}`, - }), - { - status: 404, - headers: { 'Content-Type': 'application/json' }, - } - ); - } - - // Parse requested endpoint from base path - const endpoint = path.replace(this.BASE_PATH, ''); - - // Handle root route - if (endpoint === '' || endpoint === '/') { - return new Response( - JSON.stringify({ - message: `Welcome to the Supabase cache! Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, - }), - { - headers: { 'Content-Type': 'application/json' }, - } - ); - } - - // Handle /stats endpoint - if (endpoint === '/stats') { - const cacheKey = `${this.CACHE_PREFIX}_stats`; - const cached = await this.env.AIBTCDEV_CACHE_KV.get(cacheKey); - - if (cached) { - return new Response(cached, { - headers: { 'Content-Type': 'application/json' }, - }); - } - - const stats = await this.fetchStats(); - const data = JSON.stringify({ - timestamp: new Date().toISOString(), - ...stats - }); - await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { - expirationTtl: this.CACHE_TTL - }); - - return new Response(data, { - headers: { 'Content-Type': 'application/json' }, - }); - } - - // Return 404 for any other endpoint - return new Response( - JSON.stringify({ - error: `Unrecognized endpoint: ${endpoint}. Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, - }), - { - status: 404, - headers: { 'Content-Type': 'application/json' }, - } - ); - } + private readonly CACHE_TTL: number = APP_CONFIG.CACHE_TTL; + private readonly ALARM_INTERVAL_MS = 60000; // 1 minute + private readonly BASE_PATH: string = '/supabase'; + private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); + private readonly SUPPORTED_PATHS: string[] = ['/stats']; + private supabase: SupabaseClient; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx = ctx; + this.env = env; + + // Initialize Supabase client + this.supabase = createClient(APP_CONFIG.SUPABASE_URL, APP_CONFIG.SUPABASE_SERVICE_KEY); + + // Set up alarm to run at configured interval + ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + + private async fetchStats(): Promise { + const { data, error } = await this.supabase.rpc('get_usage_stats').returns(); + + if (error) { + console.error('Error fetching stats:', error); + throw new Error(`Failed to fetch stats: ${error.message}`); + } + + if (!data || data.length === 0) { + throw new Error('No stats data returned from database'); + } + + return data[0]; + } + + async alarm(): Promise { + const startTime = Date.now(); + try { + console.log('Updating Supabase stats cache...'); + + const stats = await this.fetchStats(); + const data = JSON.stringify({ + timestamp: new Date().toISOString(), + ...stats, + }); + + const cacheKey = `${this.CACHE_PREFIX}_stats`; + await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { + expirationTtl: this.CACHE_TTL, + }); + + const endTime = Date.now(); + console.log(`supabase-do: alarm executed in ${endTime - startTime}ms`); + } catch (error) { + console.error(`Alarm execution failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + // Schedule next alarm + this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + + // Schedule next alarm if one isn't set + const currentAlarm = await this.ctx.storage.getAlarm(); + if (currentAlarm === null) { + this.ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); + } + + // Handle requests that don't match the base path + if (!path.startsWith(this.BASE_PATH)) { + return new Response( + JSON.stringify({ + error: `Unrecognized path passed to SupabaseDO: ${path}`, + }), + { + status: 404, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // Parse requested endpoint from base path + const endpoint = path.replace(this.BASE_PATH, ''); + + // Handle root route + if (endpoint === '' || endpoint === '/') { + return new Response( + JSON.stringify({ + message: `Welcome to the Supabase cache! Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, + }), + { + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // Handle /stats endpoint + if (endpoint === '/stats') { + const cacheKey = `${this.CACHE_PREFIX}_stats`; + const cached = await this.env.AIBTCDEV_CACHE_KV.get(cacheKey); + + if (cached) { + return new Response(cached, { + headers: { 'Content-Type': 'application/json' }, + }); + } + + const stats = await this.fetchStats(); + const data = JSON.stringify({ + timestamp: new Date().toISOString(), + ...stats, + }); + await this.env.AIBTCDEV_CACHE_KV.put(cacheKey, data, { + expirationTtl: this.CACHE_TTL, + }); + + return new Response(data, { + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Return 404 for any other endpoint + return new Response( + JSON.stringify({ + error: `Unrecognized endpoint: ${endpoint}. Supported endpoints: ${this.SUPPORTED_PATHS.join(', ')}`, + }), + { + status: 404, + headers: { 'Content-Type': 'application/json' }, + } + ); + } } diff --git a/src/index.ts b/src/index.ts index 44ca92c..0bfc732 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,25 +33,15 @@ export default { } if (path.startsWith('/hiro-api')) { - // Create a DurableObjectId for our instance - let id: DurableObjectId = env.HIRO_API_DO.idFromName('hiro-api-do'); - - // Get the stub for communication - let stub = env.HIRO_API_DO.get(id); - - // Forward the request to the Durable Object - return await stub.fetch(request); + const id: DurableObjectId = env.HIRO_API_DO.idFromName('hiro-api-do'); // create the instance + const stub = env.HIRO_API_DO.get(id); // get the stub for communication + return await stub.fetch(request); // forward the request to the Durable Object } if (path.startsWith('/supabase')) { - // Create a DurableObjectId for our instance - let id: DurableObjectId = env.SUPABASE_DO.idFromName('supabase-do'); - - // Get the stub for communication - let stub = env.SUPABASE_DO.get(id); - - // Forward the request to the Durable Object - return await stub.fetch(request); + let id: DurableObjectId = env.SUPABASE_DO.idFromName('supabase-do'); // create the instance + let stub = env.SUPABASE_DO.get(id); // get the stub for communication + return await stub.fetch(request); // forward the request to the Durable Object } // Return 404 for any other path diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 192d175..c9c0556 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,10 +1,10 @@ // Generated by Wrangler by running `wrangler types` -interface Env { +export interface Env { AIBTCDEV_CACHE_KV: KVNamespace; HIRO_API_KEY: string; SUPABASE_URL: string; SUPABASE_SERVICE_KEY: string; - HIRO_API_DO: DurableObjectNamespace; - SUPABASE_DO: DurableObjectNamespace; + HIRO_API_DO: DurableObjectNamespace; + SUPABASE_DO: DurableObjectNamespace; } From 8165ba6f37e55ab06af4f815d00a64127361c94c Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 14:08:28 -0700 Subject: [PATCH 14/29] fix: intervene its an object not an array --- src/durable-objects/supabase-do.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 4384eaf..b68ed36 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -42,11 +42,11 @@ export class SupabaseDO extends DurableObject { throw new Error(`Failed to fetch stats: ${error.message}`); } - if (!data || data.length === 0) { + if (!data) { throw new Error('No stats data returned from database'); } - return data[0]; + return data; } async alarm(): Promise { From 6e95a5245666ee3d500e7a20ca5dea82f593532d Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 14:54:27 -0700 Subject: [PATCH 15/29] fix: add function name from supabase dashboard --- src/durable-objects/supabase-do.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index b68ed36..847aa8e 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -35,7 +35,7 @@ export class SupabaseDO extends DurableObject { } private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc('get_usage_stats').returns(); + const { data, error } = await this.supabase.rpc('get_stats').returns(); if (error) { console.error('Error fetching stats:', error); From dd33bf4306a85bf3f645c4bff3f439a54328e21f Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 15:00:44 -0700 Subject: [PATCH 16/29] refactor: Update Supabase config to use process.env and add Env import --- src/config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 468158b..aa67b9c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,5 @@ +import { Env } from '../worker-configuration'; + export const APP_CONFIG = { // supported services for API caching // each entry is a durable object that handles requests @@ -13,6 +15,6 @@ export const APP_CONFIG = { // how often to warm the cache, should be shorter than the cache TTL ALARM_INTERVAL_MS: 300000, // 5 minutes // supabase access settings - pulled from environment variables - SUPABASE_URL: env.SUPABASE_URL || '', // supabase project URL - SUPABASE_SERVICE_KEY: env.SUPABASE_SERVICE_KEY || '', // supabase service key + SUPABASE_URL: process.env.SUPABASE_URL!, // supabase project URL + SUPABASE_SERVICE_KEY: process.env.SUPABASE_SERVICE_KEY!, // supabase service key }; From d9244638ba12439e46285ef0d4075c9a4a6e68d9 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:00:46 -0700 Subject: [PATCH 17/29] refactor: Migrate configuration to singleton AppConfig pattern --- src/config.ts | 60 +++++++++++++++++++++--------- src/durable-objects/supabase-do.ts | 13 +++++-- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/config.ts b/src/config.ts index aa67b9c..ab2f3eb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,20 +1,44 @@ import { Env } from '../worker-configuration'; -export const APP_CONFIG = { - // supported services for API caching - // each entry is a durable object that handles requests - SUPPORTED_SERVICES: ['/hiro-api', '/supabase'], - // VALUES BELOW CAN BE OVERRIDDEN BY DURABLE OBJECTS - // default cache TTL used for KV - CACHE_TTL: 900, // 15 minutes - // default rate limiting settings - MAX_REQUESTS_PER_INTERVAL: 30, // no more than 30 requests - INTERVAL_MS: 15000, // in a span of 15 seconds - MAX_RETRIES: 3, // max retries for failed fetches - RETRY_DELAY: 1000, // multiplied by retry attempt number - // how often to warm the cache, should be shorter than the cache TTL - ALARM_INTERVAL_MS: 300000, // 5 minutes - // supabase access settings - pulled from environment variables - SUPABASE_URL: process.env.SUPABASE_URL!, // supabase project URL - SUPABASE_SERVICE_KEY: process.env.SUPABASE_SERVICE_KEY!, // supabase service key -}; +export class AppConfig { + private static instance: AppConfig; + private env: Env | null = null; + + private constructor() {} + + public static getInstance(): AppConfig { + if (!AppConfig.instance) { + AppConfig.instance = new AppConfig(); + } + return AppConfig.instance; + } + + public initialize(env: Env) { + this.env = env; + } + + public getConfig() { + if (!this.env) { + throw new Error('AppConfig not initialized with environment variables'); + } + + return { + // supported services for API caching + // each entry is a durable object that handles requests + SUPPORTED_SERVICES: ['/hiro-api', '/supabase'], + // VALUES BELOW CAN BE OVERRIDDEN BY DURABLE OBJECTS + // default cache TTL used for KV + CACHE_TTL: 900, // 15 minutes + // default rate limiting settings + MAX_REQUESTS_PER_INTERVAL: 30, // no more than 30 requests + INTERVAL_MS: 15000, // in a span of 15 seconds + MAX_RETRIES: 3, // max retries for failed fetches + RETRY_DELAY: 1000, // multiplied by retry attempt number + // how often to warm the cache, should be shorter than the cache TTL + ALARM_INTERVAL_MS: 300000, // 5 minutes + // environment variables + SUPABASE_URL: this.env.SUPABASE_URL, + SUPABASE_SERVICE_KEY: this.env.SUPABASE_SERVICE_KEY, + }; + } +} diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 847aa8e..f595cd6 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -1,7 +1,7 @@ import { DurableObject } from 'cloudflare:workers'; import { createClient, SupabaseClient } from '@supabase/supabase-js'; import { Env } from '../../worker-configuration'; -import { APP_CONFIG } from '../config'; +import { AppConfig } from '../config'; interface StatsResponse { total_jobs: number; @@ -15,7 +15,7 @@ interface StatsResponse { * Durable Object class for Supabase queries */ export class SupabaseDO extends DurableObject { - private readonly CACHE_TTL: number = APP_CONFIG.CACHE_TTL; + private readonly CACHE_TTL: number; private readonly ALARM_INTERVAL_MS = 60000; // 1 minute private readonly BASE_PATH: string = '/supabase'; private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); @@ -27,8 +27,15 @@ export class SupabaseDO extends DurableObject { this.ctx = ctx; this.env = env; + // Initialize AppConfig + const appConfig = AppConfig.getInstance(); + appConfig.initialize(env); + const config = appConfig.getConfig(); + + this.CACHE_TTL = config.CACHE_TTL; + // Initialize Supabase client - this.supabase = createClient(APP_CONFIG.SUPABASE_URL, APP_CONFIG.SUPABASE_SERVICE_KEY); + this.supabase = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_KEY); // Set up alarm to run at configured interval ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); From 6c29927254284608bb95b92049a72480030009cc Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:01:21 -0700 Subject: [PATCH 18/29] refactor: Update HiroApiDO to use AppConfig singleton pattern --- src/durable-objects/hiro-api-do.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/durable-objects/hiro-api-do.ts b/src/durable-objects/hiro-api-do.ts index a25fbd2..0f5ced4 100644 --- a/src/durable-objects/hiro-api-do.ts +++ b/src/durable-objects/hiro-api-do.ts @@ -1,6 +1,6 @@ import { DurableObject } from 'cloudflare:workers'; import { Env } from '../../worker-configuration'; -import { APP_CONFIG } from '../config'; +import { AppConfig } from '../config'; import { RateLimitedFetcher } from '../rate-limiter'; /** @@ -8,12 +8,12 @@ import { RateLimitedFetcher } from '../rate-limiter'; */ export class HiroApiDO extends DurableObject { // can override values here for all endpoints - private readonly CACHE_TTL: number = APP_CONFIG.CACHE_TTL; - private readonly MAX_REQUESTS_PER_MINUTE = APP_CONFIG.MAX_REQUESTS_PER_INTERVAL; - private readonly INTERVAL_MS = APP_CONFIG.INTERVAL_MS; - private readonly MAX_RETRIES = APP_CONFIG.MAX_RETRIES; - private readonly RETRY_DELAY = APP_CONFIG.RETRY_DELAY; - private readonly ALARM_INTERVAL_MS = APP_CONFIG.ALARM_INTERVAL_MS; + private readonly CACHE_TTL: number; + private readonly MAX_REQUESTS_PER_MINUTE: number; + private readonly INTERVAL_MS: number; + private readonly MAX_RETRIES: number; + private readonly RETRY_DELAY: number; + private readonly ALARM_INTERVAL_MS: number; // settings specific to this Durable Object private readonly BASE_API_URL: string = 'https://api.hiro.so'; private readonly BASE_PATH: string = '/hiro-api'; @@ -49,6 +49,20 @@ export class HiroApiDO extends DurableObject { super(ctx, env); this.ctx = ctx; this.env = env; + + // Initialize AppConfig + const appConfig = AppConfig.getInstance(); + appConfig.initialize(env); + const config = appConfig.getConfig(); + + // Set configuration values + this.CACHE_TTL = config.CACHE_TTL; + this.MAX_REQUESTS_PER_MINUTE = config.MAX_REQUESTS_PER_INTERVAL; + this.INTERVAL_MS = config.INTERVAL_MS; + this.MAX_RETRIES = config.MAX_RETRIES; + this.RETRY_DELAY = config.RETRY_DELAY; + this.ALARM_INTERVAL_MS = config.ALARM_INTERVAL_MS; + this.fetcher = new RateLimitedFetcher( this.env, this.BASE_API_URL, From 52d2e3fe16456a085db512c12bc83c2bff88fe2e Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:02:10 -0700 Subject: [PATCH 19/29] refactor: Update index to use AppConfig singleton and initialize with env --- src/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0bfc732..8ade469 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,15 @@ import { Env } from '../worker-configuration'; -import { APP_CONFIG } from './config'; +import { AppConfig } from './config'; import { HiroApiDO } from './durable-objects/hiro-api-do'; import { SupabaseDO } from './durable-objects/supabase-do'; // export the Durable Object classes we're using export { HiroApiDO, SupabaseDO }; -const supportedServices = APP_CONFIG.SUPPORTED_SERVICES; +// Initialize AppConfig +const appConfig = AppConfig.getInstance(); +const config = appConfig.getConfig(); +const supportedServices = config.SUPPORTED_SERVICES; export default { /** @@ -17,7 +20,9 @@ export default { * @param ctx - The execution context of the Worker * @returns The response to be sent back to the client */ - async fetch(request, env, ctx): Promise { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + // Initialize config with environment + AppConfig.getInstance().initialize(env); const url = new URL(request.url); const path = url.pathname; From 97e673bfeb5d2354f1ca99d1a830b2eafa830460 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:05:01 -0700 Subject: [PATCH 20/29] fix: Correctly initialize AppConfig and access config after environment setup --- src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8ade469..f1ef271 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,9 @@ import { SupabaseDO } from './durable-objects/supabase-do'; // export the Durable Object classes we're using export { HiroApiDO, SupabaseDO }; -// Initialize AppConfig +// Initialize AppConfig with environment in the global scope const appConfig = AppConfig.getInstance(); -const config = appConfig.getConfig(); -const supportedServices = config.SUPPORTED_SERVICES; +let supportedServices: string[] = []; export default { /** @@ -23,6 +22,8 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // Initialize config with environment AppConfig.getInstance().initialize(env); + const config = appConfig.getConfig(); + supportedServices = config.SUPPORTED_SERVICES; const url = new URL(request.url); const path = url.pathname; From 36d8f77d01fe05686571a94d0a6e1884210742e3 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:12:15 -0700 Subject: [PATCH 21/29] refactor: Improve AppConfig initialization and singleton pattern --- src/config.ts | 21 +++++++++------------ src/durable-objects/hiro-api-do.ts | 6 ++---- src/durable-objects/supabase-do.ts | 6 ++---- src/index.ts | 11 ++++------- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/config.ts b/src/config.ts index ab2f3eb..6c1b03e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,25 +2,22 @@ import { Env } from '../worker-configuration'; export class AppConfig { private static instance: AppConfig; - private env: Env | null = null; + private env: Env; - private constructor() {} + private constructor(env: Env) { + this.env = env; + } - public static getInstance(): AppConfig { - if (!AppConfig.instance) { - AppConfig.instance = new AppConfig(); + public static getInstance(env?: Env): AppConfig { + if (!AppConfig.instance && env) { + AppConfig.instance = new AppConfig(env); + } else if (!AppConfig.instance) { + throw new Error('AppConfig must be initialized with environment variables first'); } return AppConfig.instance; } - public initialize(env: Env) { - this.env = env; - } - public getConfig() { - if (!this.env) { - throw new Error('AppConfig not initialized with environment variables'); - } return { // supported services for API caching diff --git a/src/durable-objects/hiro-api-do.ts b/src/durable-objects/hiro-api-do.ts index 0f5ced4..112df75 100644 --- a/src/durable-objects/hiro-api-do.ts +++ b/src/durable-objects/hiro-api-do.ts @@ -50,10 +50,8 @@ export class HiroApiDO extends DurableObject { this.ctx = ctx; this.env = env; - // Initialize AppConfig - const appConfig = AppConfig.getInstance(); - appConfig.initialize(env); - const config = appConfig.getConfig(); + // Initialize AppConfig with environment + const config = AppConfig.getInstance(env).getConfig(); // Set configuration values this.CACHE_TTL = config.CACHE_TTL; diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index f595cd6..a42b25a 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -27,10 +27,8 @@ export class SupabaseDO extends DurableObject { this.ctx = ctx; this.env = env; - // Initialize AppConfig - const appConfig = AppConfig.getInstance(); - appConfig.initialize(env); - const config = appConfig.getConfig(); + // Initialize AppConfig with environment + const config = AppConfig.getInstance(env).getConfig(); this.CACHE_TTL = config.CACHE_TTL; diff --git a/src/index.ts b/src/index.ts index f1ef271..97a207b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,11 +6,9 @@ import { SupabaseDO } from './durable-objects/supabase-do'; // export the Durable Object classes we're using export { HiroApiDO, SupabaseDO }; -// Initialize AppConfig with environment in the global scope -const appConfig = AppConfig.getInstance(); -let supportedServices: string[] = []; - export default { + // Initialize these at the top level when needed + supportedServices: [] as string[], /** * This is the standard fetch handler for a Cloudflare Worker * @@ -21,9 +19,8 @@ export default { */ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // Initialize config with environment - AppConfig.getInstance().initialize(env); - const config = appConfig.getConfig(); - supportedServices = config.SUPPORTED_SERVICES; + const config = AppConfig.getInstance(env).getConfig(); + this.supportedServices = config.SUPPORTED_SERVICES; const url = new URL(request.url); const path = url.pathname; From be95c98d046273ec1ce4fd11d991cb670cb75552 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:12:50 -0700 Subject: [PATCH 22/29] fix: Correct supportedServices reference in index.ts --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 97a207b..e6044e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ export default { if (path === '/') { return new Response( JSON.stringify({ - message: `Welcome to the aibtcdev-api-cache! Supported services: ${supportedServices.join(', ')}`, + message: `Welcome to the aibtcdev-api-cache! Supported services: ${this.supportedServices.join(', ')}`, }), { headers: { 'Content-Type': 'application/json' }, @@ -50,7 +50,7 @@ export default { // Return 404 for any other path return new Response( JSON.stringify({ - error: `Invalid path: ${path}. Supported services: ${supportedServices.join(', ')}`, + error: `Invalid path: ${path}. Supported services: ${this.supportedServices.join(', ')}`, }), { status: 404, From e967a139a08a7e3531a6fa6c87d4f13e8405a190 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 15:18:10 -0700 Subject: [PATCH 23/29] fix: small correction in format --- src/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index e6044e1..fd3957b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,6 @@ import { SupabaseDO } from './durable-objects/supabase-do'; export { HiroApiDO, SupabaseDO }; export default { - // Initialize these at the top level when needed - supportedServices: [] as string[], /** * This is the standard fetch handler for a Cloudflare Worker * @@ -20,14 +18,13 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // Initialize config with environment const config = AppConfig.getInstance(env).getConfig(); - this.supportedServices = config.SUPPORTED_SERVICES; const url = new URL(request.url); const path = url.pathname; if (path === '/') { return new Response( JSON.stringify({ - message: `Welcome to the aibtcdev-api-cache! Supported services: ${this.supportedServices.join(', ')}`, + message: `Welcome to the aibtcdev-api-cache! Supported services: ${config.SUPPORTED_SERVICES.join(', ')}`, }), { headers: { 'Content-Type': 'application/json' }, @@ -50,7 +47,7 @@ export default { // Return 404 for any other path return new Response( JSON.stringify({ - error: `Invalid path: ${path}. Supported services: ${this.supportedServices.join(', ')}`, + error: `Invalid path: ${path}. Supported services: ${config.SUPPORTED_SERVICES.join(', ')}`, }), { status: 404, From 97aec4fa8b9b3bfa7df7f6d8e2a39d5e7c0f1049 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:19:38 -0700 Subject: [PATCH 24/29] fix: Update Supabase RPC call to properly handle stats retrieval --- src/durable-objects/supabase-do.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index a42b25a..1165b5b 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -40,7 +40,9 @@ export class SupabaseDO extends DurableObject { } private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc('get_stats').returns(); + const { data, error } = await this.supabase.rpc('get_stats', {}, { + count: 'exact' + }); if (error) { console.error('Error fetching stats:', error); @@ -51,7 +53,7 @@ export class SupabaseDO extends DurableObject { throw new Error('No stats data returned from database'); } - return data; + return data as StatsResponse; } async alarm(): Promise { From 3c5e75e825fdd4a83327cc94c88423b4da367cb2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Tue, 12 Nov 2024 15:31:01 -0700 Subject: [PATCH 25/29] fix: add error handling for stats --- src/durable-objects/supabase-do.ts | 40 ++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 1165b5b..b12491b 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -29,9 +29,9 @@ export class SupabaseDO extends DurableObject { // Initialize AppConfig with environment const config = AppConfig.getInstance(env).getConfig(); - + this.CACHE_TTL = config.CACHE_TTL; - + // Initialize Supabase client this.supabase = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_KEY); @@ -39,18 +39,24 @@ export class SupabaseDO extends DurableObject { ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); } - private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc('get_stats', {}, { - count: 'exact' - }); + private async fetchStats(): Promise { + const { data, error } = await this.supabase.rpc( + 'get_stats', + {}, + { + count: 'exact', + get: true, + } + ); if (error) { console.error('Error fetching stats:', error); - throw new Error(`Failed to fetch stats: ${error.message}`); + return undefined; } if (!data) { - throw new Error('No stats data returned from database'); + console.error('No stats data returned from database'); + return undefined; } return data as StatsResponse; @@ -62,6 +68,10 @@ export class SupabaseDO extends DurableObject { console.log('Updating Supabase stats cache...'); const stats = await this.fetchStats(); + if (!stats) { + console.error('Failed to fetch stats from Supabase'); + return; + } const data = JSON.stringify({ timestamp: new Date().toISOString(), ...stats, @@ -132,6 +142,20 @@ export class SupabaseDO extends DurableObject { } const stats = await this.fetchStats(); + // verify that stats were fetched + if (!stats) { + return new Response( + JSON.stringify({ + error: 'Failed to fetch stats from Supabase', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + // format the data, store it, and return it const data = JSON.stringify({ timestamp: new Date().toISOString(), ...stats, From 5e0bb7f32d04522a554d85ecb47f127c2aebfa3a Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:32:20 -0700 Subject: [PATCH 26/29] fix: Refactor Supabase client initialization with direct env variables --- src/durable-objects/supabase-do.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index b12491b..b1cd491 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -29,11 +29,16 @@ export class SupabaseDO extends DurableObject { // Initialize AppConfig with environment const config = AppConfig.getInstance(env).getConfig(); - + + // Set configuration values this.CACHE_TTL = config.CACHE_TTL; - - // Initialize Supabase client - this.supabase = createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_KEY); + + // Initialize Supabase client with config values + this.supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_KEY, { + auth: { + persistSession: false + } + }); // Set up alarm to run at configured interval ctx.storage.setAlarm(Date.now() + this.ALARM_INTERVAL_MS); From 12dc87191b104cef4717bd293e79f0e12b113c0b Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:33:57 -0700 Subject: [PATCH 27/29] fix: Update Supabase RPC call options to resolve stats fetching error --- src/durable-objects/supabase-do.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index b1cd491..b54b75a 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -45,14 +45,10 @@ export class SupabaseDO extends DurableObject { } private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc( - 'get_stats', - {}, - { - count: 'exact', - get: true, - } - ); + const { data, error } = await this.supabase.rpc('get_stats', {}, { + head: false, + count: null + }); if (error) { console.error('Error fetching stats:', error); From 4489e0c9a4a4fddabcbdc6c67ca56aa75b070204 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:37:11 -0700 Subject: [PATCH 28/29] refactor: Improve Supabase RPC call with explicit typing and error handling --- src/durable-objects/supabase-do.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index b54b75a..260dd03 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -45,22 +45,27 @@ export class SupabaseDO extends DurableObject { } private async fetchStats(): Promise { - const { data, error } = await this.supabase.rpc('get_stats', {}, { - head: false, - count: null - }); + try { + const { data, error } = await this.supabase + .rpc('get_stats') + .returns() + .single(); + + if (error) { + console.error('Error fetching stats:', error); + return undefined; + } - if (error) { - console.error('Error fetching stats:', error); - return undefined; - } + if (!data) { + console.error('No stats data returned from database'); + return undefined; + } - if (!data) { - console.error('No stats data returned from database'); + return data; + } catch (err) { + console.error('Exception in fetchStats:', err); return undefined; } - - return data as StatsResponse; } async alarm(): Promise { From 37c580b2483bc76b579e0d12b62af772e428f7bf Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Tue, 12 Nov 2024 15:37:34 -0700 Subject: [PATCH 29/29] fix: Update Supabase RPC call to handle result data correctly --- src/durable-objects/supabase-do.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/durable-objects/supabase-do.ts b/src/durable-objects/supabase-do.ts index 260dd03..e51b83d 100644 --- a/src/durable-objects/supabase-do.ts +++ b/src/durable-objects/supabase-do.ts @@ -47,9 +47,11 @@ export class SupabaseDO extends DurableObject { private async fetchStats(): Promise { try { const { data, error } = await this.supabase - .rpc('get_stats') - .returns() - .single(); + .rpc('get_stats', undefined, { + count: 'exact' + }) + .select('*') + .maybeSingle(); if (error) { console.error('Error fetching stats:', error);