stripe-experiment-sync 1.0.9 → 1.0.11

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.
@@ -2,11 +2,11 @@ import {
2
2
  StripeSync,
3
3
  createStripeWebSocketClient,
4
4
  runMigrations
5
- } from "./chunk-PQ2T7XTY.js";
5
+ } from "./chunk-LFXGDVMC.js";
6
6
  import {
7
7
  install,
8
8
  uninstall
9
- } from "./chunk-OLVA37VZ.js";
9
+ } from "./chunk-EVNKMOYA.js";
10
10
 
11
11
  // src/cli/config.ts
12
12
  import dotenv from "dotenv";
@@ -533,7 +533,8 @@ async function installCommand(options) {
533
533
  supabaseAccessToken: accessToken,
534
534
  supabaseProjectRef: projectRef,
535
535
  stripeKey,
536
- packageVersion: options.packageVersion
536
+ packageVersion: options.packageVersion,
537
+ workerIntervalSeconds: options.workerInterval
537
538
  });
538
539
  console.log(chalk3.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
539
540
  console.log(chalk3.cyan.bold(" Installation Complete!"));
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-RR5BGG4F.js";
3
+ } from "./chunk-XTL72K7K.js";
4
4
 
5
5
  // src/supabase/supabase.ts
6
6
  import { SupabaseManagementAPI } from "supabase-management-js";
7
7
 
8
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
9
- var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
8
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
9
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
10
10
 
11
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
11
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
12
12
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
13
13
 
14
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
15
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
14
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
15
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
16
16
 
17
17
  // src/supabase/edge-function-code.ts
18
18
  var setupFunctionCode = stripe_setup_default;
@@ -86,8 +86,29 @@ var SupabaseSetupClient = class {
86
86
  }
87
87
  /**
88
88
  * Setup pg_cron job to invoke worker function
89
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
89
90
  */
90
- async setupPgCronJob() {
91
+ async setupPgCronJob(intervalSeconds = 60) {
92
+ if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
93
+ throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
94
+ }
95
+ let schedule;
96
+ if (intervalSeconds < 60) {
97
+ schedule = `${intervalSeconds} seconds`;
98
+ } else if (intervalSeconds % 60 === 0) {
99
+ const minutes = intervalSeconds / 60;
100
+ if (minutes < 60) {
101
+ schedule = `*/${minutes} * * * *`;
102
+ } else {
103
+ throw new Error(
104
+ `Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
105
+ );
106
+ }
107
+ } else {
108
+ throw new Error(
109
+ `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
110
+ );
111
+ }
91
112
  const serviceRoleKey = await this.getServiceRoleKey();
92
113
  const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
93
114
  const sql = `
@@ -115,11 +136,11 @@ var SupabaseSetupClient = class {
115
136
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
116
137
  );
117
138
 
118
- -- Create job to invoke worker every 10 seconds
139
+ -- Create job to invoke worker at configured interval
119
140
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
120
141
  SELECT cron.schedule(
121
142
  'stripe-sync-worker',
122
- '10 seconds',
143
+ '${schedule}',
123
144
  $$
124
145
  SELECT net.http_post(
125
146
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
@@ -303,10 +324,12 @@ var SupabaseSetupClient = class {
303
324
  await this.deleteSecret("STRIPE_SECRET_KEY");
304
325
  try {
305
326
  await this.runSQL(`
306
- SELECT cron.unschedule('stripe-sync-worker')
307
- WHERE EXISTS (
308
- SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
309
- )
327
+ DO $$
328
+ BEGIN
329
+ IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
330
+ PERFORM cron.unschedule('stripe-sync-worker');
331
+ END IF;
332
+ END $$;
310
333
  `);
311
334
  } catch (err) {
312
335
  console.warn("Could not unschedule pg_cron job:", err);
@@ -322,15 +345,31 @@ var SupabaseSetupClient = class {
322
345
  try {
323
346
  await this.runSQL(`
324
347
  SELECT pg_terminate_backend(pid)
325
- FROM pg_stat_activity
326
- WHERE datname = current_database()
327
- AND pid != pg_backend_pid()
328
- AND query ILIKE '%stripe.%'
348
+ FROM pg_locks l
349
+ JOIN pg_class c ON l.relation = c.oid
350
+ JOIN pg_namespace n ON c.relnamespace = n.oid
351
+ WHERE n.nspname = 'stripe'
352
+ AND l.pid != pg_backend_pid()
329
353
  `);
330
354
  } catch (err) {
331
355
  console.warn("Could not terminate connections:", err);
332
356
  }
333
- await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
357
+ let dropAttempts = 0;
358
+ const maxAttempts = 3;
359
+ while (dropAttempts < maxAttempts) {
360
+ try {
361
+ await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
362
+ break;
363
+ } catch (err) {
364
+ dropAttempts++;
365
+ if (dropAttempts >= maxAttempts) {
366
+ throw new Error(
367
+ `Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
368
+ );
369
+ }
370
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
371
+ }
372
+ }
334
373
  } catch (error) {
335
374
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
336
375
  }
@@ -347,7 +386,7 @@ var SupabaseSetupClient = class {
347
386
  `from 'npm:stripe-experiment-sync@${version}'`
348
387
  );
349
388
  }
350
- async install(stripeKey, packageVersion) {
389
+ async install(stripeKey, packageVersion, workerIntervalSeconds) {
351
390
  const trimmedStripeKey = stripeKey.trim();
352
391
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
353
392
  throw new Error('Stripe key should start with "sk_" or "rk_"');
@@ -371,7 +410,7 @@ var SupabaseSetupClient = class {
371
410
  if (!setupResult.success) {
372
411
  throw new Error(`Setup failed: ${setupResult.error}`);
373
412
  }
374
- await this.setupPgCronJob();
413
+ await this.setupPgCronJob(workerIntervalSeconds);
375
414
  await this.updateInstallationComment(
376
415
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
377
416
  );
@@ -384,14 +423,20 @@ var SupabaseSetupClient = class {
384
423
  }
385
424
  };
386
425
  async function install(params) {
387
- const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
426
+ const {
427
+ supabaseAccessToken,
428
+ supabaseProjectRef,
429
+ stripeKey,
430
+ packageVersion,
431
+ workerIntervalSeconds
432
+ } = params;
388
433
  const client = new SupabaseSetupClient({
389
434
  accessToken: supabaseAccessToken,
390
435
  projectRef: supabaseProjectRef,
391
436
  projectBaseUrl: params.baseProjectUrl,
392
437
  managementApiBaseUrl: params.baseManagementApiUrl
393
438
  });
394
- await client.install(stripeKey, packageVersion);
439
+ await client.install(stripeKey, packageVersion, workerIntervalSeconds);
395
440
  }
396
441
  async function uninstall(params) {
397
442
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./chunk-RR5BGG4F.js";
3
+ } from "./chunk-XTL72K7K.js";
4
4
 
5
5
  // src/stripeSync.ts
6
6
  import Stripe2 from "stripe";
@@ -1,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "stripe-experiment-sync",
4
- version: "1.0.9",
4
+ version: "1.0.11",
5
5
  private: false,
6
6
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
7
7
  type: "module",
@@ -33,7 +33,7 @@ var import_commander = require("commander");
33
33
  // package.json
34
34
  var package_default = {
35
35
  name: "stripe-experiment-sync",
36
- version: "1.0.9",
36
+ version: "1.0.11",
37
37
  private: false,
38
38
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
39
39
  type: "module",
@@ -3630,14 +3630,14 @@ Creating ngrok tunnel for port ${port}...`));
3630
3630
  // src/supabase/supabase.ts
3631
3631
  var import_supabase_management_js = require("supabase-management-js");
3632
3632
 
3633
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3634
- var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
3633
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3634
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
3635
3635
 
3636
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3636
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3637
3637
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
3638
3638
 
3639
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3640
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3639
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3640
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3641
3641
 
3642
3642
  // src/supabase/edge-function-code.ts
3643
3643
  var setupFunctionCode = stripe_setup_default;
@@ -3711,8 +3711,29 @@ var SupabaseSetupClient = class {
3711
3711
  }
3712
3712
  /**
3713
3713
  * Setup pg_cron job to invoke worker function
3714
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
3714
3715
  */
3715
- async setupPgCronJob() {
3716
+ async setupPgCronJob(intervalSeconds = 60) {
3717
+ if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
3718
+ throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
3719
+ }
3720
+ let schedule;
3721
+ if (intervalSeconds < 60) {
3722
+ schedule = `${intervalSeconds} seconds`;
3723
+ } else if (intervalSeconds % 60 === 0) {
3724
+ const minutes = intervalSeconds / 60;
3725
+ if (minutes < 60) {
3726
+ schedule = `*/${minutes} * * * *`;
3727
+ } else {
3728
+ throw new Error(
3729
+ `Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
3730
+ );
3731
+ }
3732
+ } else {
3733
+ throw new Error(
3734
+ `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
3735
+ );
3736
+ }
3716
3737
  const serviceRoleKey = await this.getServiceRoleKey();
3717
3738
  const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
3718
3739
  const sql3 = `
@@ -3740,11 +3761,11 @@ var SupabaseSetupClient = class {
3740
3761
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
3741
3762
  );
3742
3763
 
3743
- -- Create job to invoke worker every 10 seconds
3764
+ -- Create job to invoke worker at configured interval
3744
3765
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
3745
3766
  SELECT cron.schedule(
3746
3767
  'stripe-sync-worker',
3747
- '10 seconds',
3768
+ '${schedule}',
3748
3769
  $$
3749
3770
  SELECT net.http_post(
3750
3771
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
@@ -3928,10 +3949,12 @@ var SupabaseSetupClient = class {
3928
3949
  await this.deleteSecret("STRIPE_SECRET_KEY");
3929
3950
  try {
3930
3951
  await this.runSQL(`
3931
- SELECT cron.unschedule('stripe-sync-worker')
3932
- WHERE EXISTS (
3933
- SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
3934
- )
3952
+ DO $$
3953
+ BEGIN
3954
+ IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
3955
+ PERFORM cron.unschedule('stripe-sync-worker');
3956
+ END IF;
3957
+ END $$;
3935
3958
  `);
3936
3959
  } catch (err) {
3937
3960
  console.warn("Could not unschedule pg_cron job:", err);
@@ -3947,15 +3970,31 @@ var SupabaseSetupClient = class {
3947
3970
  try {
3948
3971
  await this.runSQL(`
3949
3972
  SELECT pg_terminate_backend(pid)
3950
- FROM pg_stat_activity
3951
- WHERE datname = current_database()
3952
- AND pid != pg_backend_pid()
3953
- AND query ILIKE '%stripe.%'
3973
+ FROM pg_locks l
3974
+ JOIN pg_class c ON l.relation = c.oid
3975
+ JOIN pg_namespace n ON c.relnamespace = n.oid
3976
+ WHERE n.nspname = 'stripe'
3977
+ AND l.pid != pg_backend_pid()
3954
3978
  `);
3955
3979
  } catch (err) {
3956
3980
  console.warn("Could not terminate connections:", err);
3957
3981
  }
3958
- await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
3982
+ let dropAttempts = 0;
3983
+ const maxAttempts = 3;
3984
+ while (dropAttempts < maxAttempts) {
3985
+ try {
3986
+ await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
3987
+ break;
3988
+ } catch (err) {
3989
+ dropAttempts++;
3990
+ if (dropAttempts >= maxAttempts) {
3991
+ throw new Error(
3992
+ `Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
3993
+ );
3994
+ }
3995
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
3996
+ }
3997
+ }
3959
3998
  } catch (error) {
3960
3999
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
3961
4000
  }
@@ -3972,7 +4011,7 @@ var SupabaseSetupClient = class {
3972
4011
  `from 'npm:stripe-experiment-sync@${version}'`
3973
4012
  );
3974
4013
  }
3975
- async install(stripeKey, packageVersion) {
4014
+ async install(stripeKey, packageVersion, workerIntervalSeconds) {
3976
4015
  const trimmedStripeKey = stripeKey.trim();
3977
4016
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
3978
4017
  throw new Error('Stripe key should start with "sk_" or "rk_"');
@@ -3996,7 +4035,7 @@ var SupabaseSetupClient = class {
3996
4035
  if (!setupResult.success) {
3997
4036
  throw new Error(`Setup failed: ${setupResult.error}`);
3998
4037
  }
3999
- await this.setupPgCronJob();
4038
+ await this.setupPgCronJob(workerIntervalSeconds);
4000
4039
  await this.updateInstallationComment(
4001
4040
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
4002
4041
  );
@@ -4009,14 +4048,20 @@ var SupabaseSetupClient = class {
4009
4048
  }
4010
4049
  };
4011
4050
  async function install(params) {
4012
- const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
4051
+ const {
4052
+ supabaseAccessToken,
4053
+ supabaseProjectRef,
4054
+ stripeKey,
4055
+ packageVersion,
4056
+ workerIntervalSeconds
4057
+ } = params;
4013
4058
  const client = new SupabaseSetupClient({
4014
4059
  accessToken: supabaseAccessToken,
4015
4060
  projectRef: supabaseProjectRef,
4016
4061
  projectBaseUrl: params.baseProjectUrl,
4017
4062
  managementApiBaseUrl: params.baseManagementApiUrl
4018
4063
  });
4019
- await client.install(stripeKey, packageVersion);
4064
+ await client.install(stripeKey, packageVersion, workerIntervalSeconds);
4020
4065
  }
4021
4066
  async function uninstall(params) {
4022
4067
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -4467,7 +4512,8 @@ async function installCommand(options) {
4467
4512
  supabaseAccessToken: accessToken,
4468
4513
  supabaseProjectRef: projectRef,
4469
4514
  stripeKey,
4470
- packageVersion: options.packageVersion
4515
+ packageVersion: options.packageVersion,
4516
+ workerIntervalSeconds: options.workerInterval
4471
4517
  });
4472
4518
  console.log(import_chalk3.default.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4473
4519
  console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
@@ -4585,12 +4631,17 @@ var supabase = program.command("supabase").description("Supabase Edge Functions
4585
4631
  supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
4586
4632
  "--package-version <version>",
4587
4633
  "Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
4634
+ ).option(
4635
+ "--worker-interval <seconds>",
4636
+ "Worker interval in seconds (defaults to 60)",
4637
+ (val) => parseInt(val, 10)
4588
4638
  ).action(async (options) => {
4589
4639
  await installCommand({
4590
4640
  supabaseAccessToken: options.token,
4591
4641
  supabaseProjectRef: options.project,
4592
4642
  stripeKey: options.stripeKey,
4593
- packageVersion: options.packageVersion
4643
+ packageVersion: options.packageVersion,
4644
+ workerInterval: options.workerInterval
4594
4645
  });
4595
4646
  });
4596
4647
  supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
package/dist/cli/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  migrateCommand,
6
6
  syncCommand,
7
7
  uninstallCommand
8
- } from "../chunk-PWJLHHPY.js";
9
- import "../chunk-PQ2T7XTY.js";
10
- import "../chunk-OLVA37VZ.js";
8
+ } from "../chunk-4MOFTZ53.js";
9
+ import "../chunk-LFXGDVMC.js";
10
+ import "../chunk-EVNKMOYA.js";
11
11
  import {
12
12
  package_default
13
- } from "../chunk-RR5BGG4F.js";
13
+ } from "../chunk-XTL72K7K.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -41,12 +41,17 @@ var supabase = program.command("supabase").description("Supabase Edge Functions
41
41
  supabase.command("install").description("Install Stripe sync to Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").option(
42
42
  "--package-version <version>",
43
43
  "Package version to install (e.g., 1.0.8-beta.1, defaults to latest)"
44
+ ).option(
45
+ "--worker-interval <seconds>",
46
+ "Worker interval in seconds (defaults to 60)",
47
+ (val) => parseInt(val, 10)
44
48
  ).action(async (options) => {
45
49
  await installCommand({
46
50
  supabaseAccessToken: options.token,
47
51
  supabaseProjectRef: options.project,
48
52
  stripeKey: options.stripeKey,
49
- packageVersion: options.packageVersion
53
+ packageVersion: options.packageVersion,
54
+ workerInterval: options.workerInterval
50
55
  });
51
56
  });
52
57
  supabase.command("uninstall").description("Uninstall Stripe sync from Supabase Edge Functions").option("--token <token>", "Supabase access token (or SUPABASE_ACCESS_TOKEN env)").option("--project <ref>", "Supabase project ref (or SUPABASE_PROJECT_REF env)").option("--stripe-key <key>", "Stripe API key (or STRIPE_API_KEY env)").action(async (options) => {
package/dist/cli/lib.cjs CHANGED
@@ -105,7 +105,7 @@ async function loadConfig(options) {
105
105
  // package.json
106
106
  var package_default = {
107
107
  name: "stripe-experiment-sync",
108
- version: "1.0.9",
108
+ version: "1.0.11",
109
109
  private: false,
110
110
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
111
111
  type: "module",
@@ -3644,14 +3644,14 @@ Creating ngrok tunnel for port ${port}...`));
3644
3644
  // src/supabase/supabase.ts
3645
3645
  var import_supabase_management_js = require("supabase-management-js");
3646
3646
 
3647
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3648
- var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
3647
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
3648
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
3649
3649
 
3650
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3650
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
3651
3651
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
3652
3652
 
3653
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3654
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3653
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
3654
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
3655
3655
 
3656
3656
  // src/supabase/edge-function-code.ts
3657
3657
  var setupFunctionCode = stripe_setup_default;
@@ -3725,8 +3725,29 @@ var SupabaseSetupClient = class {
3725
3725
  }
3726
3726
  /**
3727
3727
  * Setup pg_cron job to invoke worker function
3728
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
3728
3729
  */
3729
- async setupPgCronJob() {
3730
+ async setupPgCronJob(intervalSeconds = 60) {
3731
+ if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
3732
+ throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
3733
+ }
3734
+ let schedule;
3735
+ if (intervalSeconds < 60) {
3736
+ schedule = `${intervalSeconds} seconds`;
3737
+ } else if (intervalSeconds % 60 === 0) {
3738
+ const minutes = intervalSeconds / 60;
3739
+ if (minutes < 60) {
3740
+ schedule = `*/${minutes} * * * *`;
3741
+ } else {
3742
+ throw new Error(
3743
+ `Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
3744
+ );
3745
+ }
3746
+ } else {
3747
+ throw new Error(
3748
+ `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
3749
+ );
3750
+ }
3730
3751
  const serviceRoleKey = await this.getServiceRoleKey();
3731
3752
  const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
3732
3753
  const sql3 = `
@@ -3754,11 +3775,11 @@ var SupabaseSetupClient = class {
3754
3775
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
3755
3776
  );
3756
3777
 
3757
- -- Create job to invoke worker every 10 seconds
3778
+ -- Create job to invoke worker at configured interval
3758
3779
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
3759
3780
  SELECT cron.schedule(
3760
3781
  'stripe-sync-worker',
3761
- '10 seconds',
3782
+ '${schedule}',
3762
3783
  $$
3763
3784
  SELECT net.http_post(
3764
3785
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
@@ -3942,10 +3963,12 @@ var SupabaseSetupClient = class {
3942
3963
  await this.deleteSecret("STRIPE_SECRET_KEY");
3943
3964
  try {
3944
3965
  await this.runSQL(`
3945
- SELECT cron.unschedule('stripe-sync-worker')
3946
- WHERE EXISTS (
3947
- SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
3948
- )
3966
+ DO $$
3967
+ BEGIN
3968
+ IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
3969
+ PERFORM cron.unschedule('stripe-sync-worker');
3970
+ END IF;
3971
+ END $$;
3949
3972
  `);
3950
3973
  } catch (err) {
3951
3974
  console.warn("Could not unschedule pg_cron job:", err);
@@ -3961,15 +3984,31 @@ var SupabaseSetupClient = class {
3961
3984
  try {
3962
3985
  await this.runSQL(`
3963
3986
  SELECT pg_terminate_backend(pid)
3964
- FROM pg_stat_activity
3965
- WHERE datname = current_database()
3966
- AND pid != pg_backend_pid()
3967
- AND query ILIKE '%stripe.%'
3987
+ FROM pg_locks l
3988
+ JOIN pg_class c ON l.relation = c.oid
3989
+ JOIN pg_namespace n ON c.relnamespace = n.oid
3990
+ WHERE n.nspname = 'stripe'
3991
+ AND l.pid != pg_backend_pid()
3968
3992
  `);
3969
3993
  } catch (err) {
3970
3994
  console.warn("Could not terminate connections:", err);
3971
3995
  }
3972
- await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
3996
+ let dropAttempts = 0;
3997
+ const maxAttempts = 3;
3998
+ while (dropAttempts < maxAttempts) {
3999
+ try {
4000
+ await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
4001
+ break;
4002
+ } catch (err) {
4003
+ dropAttempts++;
4004
+ if (dropAttempts >= maxAttempts) {
4005
+ throw new Error(
4006
+ `Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
4007
+ );
4008
+ }
4009
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
4010
+ }
4011
+ }
3973
4012
  } catch (error) {
3974
4013
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
3975
4014
  }
@@ -3986,7 +4025,7 @@ var SupabaseSetupClient = class {
3986
4025
  `from 'npm:stripe-experiment-sync@${version}'`
3987
4026
  );
3988
4027
  }
3989
- async install(stripeKey, packageVersion) {
4028
+ async install(stripeKey, packageVersion, workerIntervalSeconds) {
3990
4029
  const trimmedStripeKey = stripeKey.trim();
3991
4030
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
3992
4031
  throw new Error('Stripe key should start with "sk_" or "rk_"');
@@ -4010,7 +4049,7 @@ var SupabaseSetupClient = class {
4010
4049
  if (!setupResult.success) {
4011
4050
  throw new Error(`Setup failed: ${setupResult.error}`);
4012
4051
  }
4013
- await this.setupPgCronJob();
4052
+ await this.setupPgCronJob(workerIntervalSeconds);
4014
4053
  await this.updateInstallationComment(
4015
4054
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
4016
4055
  );
@@ -4023,14 +4062,20 @@ var SupabaseSetupClient = class {
4023
4062
  }
4024
4063
  };
4025
4064
  async function install(params) {
4026
- const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
4065
+ const {
4066
+ supabaseAccessToken,
4067
+ supabaseProjectRef,
4068
+ stripeKey,
4069
+ packageVersion,
4070
+ workerIntervalSeconds
4071
+ } = params;
4027
4072
  const client = new SupabaseSetupClient({
4028
4073
  accessToken: supabaseAccessToken,
4029
4074
  projectRef: supabaseProjectRef,
4030
4075
  projectBaseUrl: params.baseProjectUrl,
4031
4076
  managementApiBaseUrl: params.baseManagementApiUrl
4032
4077
  });
4033
- await client.install(stripeKey, packageVersion);
4078
+ await client.install(stripeKey, packageVersion, workerIntervalSeconds);
4034
4079
  }
4035
4080
  async function uninstall(params) {
4036
4081
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -4481,7 +4526,8 @@ async function installCommand(options) {
4481
4526
  supabaseAccessToken: accessToken,
4482
4527
  supabaseProjectRef: projectRef,
4483
4528
  stripeKey,
4484
- packageVersion: options.packageVersion
4529
+ packageVersion: options.packageVersion,
4530
+ workerIntervalSeconds: options.workerInterval
4485
4531
  });
4486
4532
  console.log(import_chalk3.default.cyan("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4487
4533
  console.log(import_chalk3.default.cyan.bold(" Installation Complete!"));
@@ -19,6 +19,7 @@ interface DeployOptions {
19
19
  supabaseProjectRef?: string;
20
20
  stripeKey?: string;
21
21
  packageVersion?: string;
22
+ workerInterval?: number;
22
23
  }
23
24
 
24
25
  /**
package/dist/cli/lib.d.ts CHANGED
@@ -19,6 +19,7 @@ interface DeployOptions {
19
19
  supabaseProjectRef?: string;
20
20
  stripeKey?: string;
21
21
  packageVersion?: string;
22
+ workerInterval?: number;
22
23
  }
23
24
 
24
25
  /**
package/dist/cli/lib.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  migrateCommand,
7
7
  syncCommand,
8
8
  uninstallCommand
9
- } from "../chunk-PWJLHHPY.js";
10
- import "../chunk-PQ2T7XTY.js";
11
- import "../chunk-OLVA37VZ.js";
12
- import "../chunk-RR5BGG4F.js";
9
+ } from "../chunk-4MOFTZ53.js";
10
+ import "../chunk-LFXGDVMC.js";
11
+ import "../chunk-EVNKMOYA.js";
12
+ import "../chunk-XTL72K7K.js";
13
13
  export {
14
14
  backfillCommand,
15
15
  createTunnel,
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
46
46
  // package.json
47
47
  var package_default = {
48
48
  name: "stripe-experiment-sync",
49
- version: "1.0.9",
49
+ version: "1.0.11",
50
50
  private: false,
51
51
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
52
52
  type: "module",
package/dist/index.d.cts CHANGED
@@ -334,6 +334,36 @@ interface ProcessNextParams extends SyncParams {
334
334
  /** Who/what triggered this sync (for observability) */
335
335
  triggeredBy?: string;
336
336
  }
337
+ /**
338
+ * Installation status of the stripe-sync package
339
+ */
340
+ type InstallationStatus = 'not_installed' | 'installing' | 'installed' | 'error';
341
+ /**
342
+ * Sync status for a single account (from sync_runs view)
343
+ */
344
+ interface StripeSyncAccountState {
345
+ account_id: string;
346
+ started_at: string;
347
+ closed_at: string | null;
348
+ status: 'pending' | 'running' | 'complete' | 'error';
349
+ error_message: string | null;
350
+ total_processed: number;
351
+ total_objects: number;
352
+ complete_count: number;
353
+ error_count: number;
354
+ running_count: number;
355
+ pending_count: number;
356
+ triggered_by: string;
357
+ max_concurrent: number;
358
+ }
359
+ /**
360
+ * Response schema for the sync status endpoint
361
+ */
362
+ interface StripeSyncState {
363
+ package_version: string;
364
+ installation_status: InstallationStatus;
365
+ sync_status: StripeSyncAccountState[];
366
+ }
337
367
 
338
368
  /**
339
369
  * Identifies a specific sync run.
@@ -654,4 +684,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
654
684
 
655
685
  declare const VERSION: string;
656
686
 
657
- export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
687
+ export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
package/dist/index.d.ts CHANGED
@@ -334,6 +334,36 @@ interface ProcessNextParams extends SyncParams {
334
334
  /** Who/what triggered this sync (for observability) */
335
335
  triggeredBy?: string;
336
336
  }
337
+ /**
338
+ * Installation status of the stripe-sync package
339
+ */
340
+ type InstallationStatus = 'not_installed' | 'installing' | 'installed' | 'error';
341
+ /**
342
+ * Sync status for a single account (from sync_runs view)
343
+ */
344
+ interface StripeSyncAccountState {
345
+ account_id: string;
346
+ started_at: string;
347
+ closed_at: string | null;
348
+ status: 'pending' | 'running' | 'complete' | 'error';
349
+ error_message: string | null;
350
+ total_processed: number;
351
+ total_objects: number;
352
+ complete_count: number;
353
+ error_count: number;
354
+ running_count: number;
355
+ pending_count: number;
356
+ triggered_by: string;
357
+ max_concurrent: number;
358
+ }
359
+ /**
360
+ * Response schema for the sync status endpoint
361
+ */
362
+ interface StripeSyncState {
363
+ package_version: string;
364
+ installation_status: InstallationStatus;
365
+ sync_status: StripeSyncAccountState[];
366
+ }
337
367
 
338
368
  /**
339
369
  * Identifies a specific sync run.
@@ -654,4 +684,4 @@ declare function createStripeWebSocketClient(options: StripeWebSocketOptions): P
654
684
 
655
685
  declare const VERSION: string;
656
686
 
657
- export { type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncConfig, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
687
+ export { type InstallationStatus, type Logger, PostgresClient, type ProcessNextParams, type ProcessNextResult, type RevalidateEntity, StripeSync, type StripeSyncAccountState, type StripeSyncConfig, type StripeSyncState, type StripeWebSocketClient, type StripeWebSocketOptions, type StripeWebhookEvent, type Sync, type SyncBackfill, type SyncEntitlementsParams, type SyncFeaturesParams, type SyncObject, type SyncParams, VERSION, type WebhookProcessingResult, createStripeWebSocketClient, hashApiKey, runMigrations };
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  createStripeWebSocketClient,
6
6
  hashApiKey,
7
7
  runMigrations
8
- } from "./chunk-PQ2T7XTY.js";
9
- import "./chunk-RR5BGG4F.js";
8
+ } from "./chunk-LFXGDVMC.js";
9
+ import "./chunk-XTL72K7K.js";
10
10
  export {
11
11
  PostgresClient,
12
12
  StripeSync,
@@ -47,14 +47,14 @@ module.exports = __toCommonJS(supabase_exports);
47
47
  // src/supabase/supabase.ts
48
48
  var import_supabase_management_js = require("supabase-management-js");
49
49
 
50
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
51
- var stripe_setup_default = "import { StripeSync, runMigrations } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
50
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-setup.ts
51
+ var stripe_setup_default = "import { StripeSync, runMigrations, VERSION } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nDeno.serve(async (req) => {\n // Require authentication for both GET and POST\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n // Handle GET requests for status\n if (req.method === 'GET') {\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n let sql\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n\n // Query installation status from schema comment\n const commentResult = await sql`\n SELECT obj_description(oid, 'pg_namespace') as comment\n FROM pg_namespace\n WHERE nspname = 'stripe'\n `\n\n const comment = commentResult[0]?.comment || null\n let installationStatus = 'not_installed'\n\n if (comment && comment.includes('stripe-sync')) {\n // Parse installation status from comment\n if (comment.includes('installation:started')) {\n installationStatus = 'installing'\n } else if (comment.includes('installation:error')) {\n installationStatus = 'error'\n } else if (comment.includes('installed')) {\n installationStatus = 'installed'\n }\n }\n\n // Query sync runs (only if schema exists)\n let syncStatus = []\n if (comment) {\n try {\n syncStatus = await sql`\n SELECT DISTINCT ON (account_id)\n account_id, started_at, closed_at, status, error_message,\n total_processed, total_objects, complete_count, error_count,\n running_count, pending_count, triggered_by, max_concurrent\n FROM stripe.sync_runs\n ORDER BY account_id, started_at DESC\n `\n } catch (err) {\n // Ignore errors if sync_runs view doesn't exist yet\n console.warn('sync_runs query failed (may not exist yet):', err)\n }\n }\n\n return new Response(\n JSON.stringify({\n package_version: VERSION,\n installation_status: installationStatus,\n sync_status: syncStatus,\n }),\n {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'no-cache, no-store, must-revalidate',\n },\n }\n )\n } catch (error) {\n console.error('Status query error:', error)\n return new Response(\n JSON.stringify({\n error: error.message,\n package_version: VERSION,\n installation_status: 'not_installed',\n }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } finally {\n if (sql) await sql.end()\n }\n }\n\n // Handle POST requests for setup (existing logic)\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n let stripeSync = null\n try {\n // Get and validate database URL\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n throw new Error('SUPABASE_DB_URL environment variable is not set')\n }\n // Remove sslmode from connection string (not supported by pg in Deno)\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n await runMigrations({ databaseUrl: dbUrl })\n\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 2 }, // Need 2 for advisory lock + queries\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY'),\n })\n\n // Release any stale advisory locks from previous timeouts\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n\n // Construct webhook URL from SUPABASE_URL (available in all Edge Functions)\n const supabaseUrl = Deno.env.get('SUPABASE_URL')\n if (!supabaseUrl) {\n throw new Error('SUPABASE_URL environment variable is not set')\n }\n const webhookUrl = supabaseUrl + '/functions/v1/stripe-webhook'\n\n const webhook = await stripeSync.findOrCreateManagedWebhook(webhookUrl)\n\n await stripeSync.postgresClient.pool.end()\n\n return new Response(\n JSON.stringify({\n success: true,\n message: 'Setup complete',\n webhookId: webhook.id,\n }),\n {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n }\n )\n } catch (error) {\n console.error('Setup error:', error)\n // Cleanup on error\n if (stripeSync) {\n try {\n await stripeSync.postgresClient.query('SELECT pg_advisory_unlock_all()')\n await stripeSync.postgresClient.pool.end()\n } catch (cleanupErr) {\n console.warn('Cleanup failed:', cleanupErr)\n }\n }\n return new Response(JSON.stringify({ success: false, error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n})\n";
52
52
 
53
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
53
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-webhook.ts
54
54
  var stripe_webhook_default = "import { StripeSync } from 'npm:stripe-experiment-sync'\n\nDeno.serve(async (req) => {\n if (req.method !== 'POST') {\n return new Response('Method not allowed', { status: 405 })\n }\n\n const sig = req.headers.get('stripe-signature')\n if (!sig) {\n return new Response('Missing stripe-signature header', { status: 400 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n const stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n\n try {\n const rawBody = new Uint8Array(await req.arrayBuffer())\n await stripeSync.processWebhook(rawBody, sig)\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Webhook processing error:', error)\n const isSignatureError =\n error.message?.includes('signature') || error.type === 'StripeSignatureVerificationError'\n const status = isSignatureError ? 400 : 500\n return new Response(JSON.stringify({ error: error.message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n await stripeSync.postgresClient.pool.end()\n }\n})\n";
55
55
 
56
- // raw-ts:/Users/lfdepombo/src/stripe-sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
57
- var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron every 10 seconds. Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
56
+ // raw-ts:/home/runner/work/sync-engine/sync-engine/packages/sync-engine/src/supabase/edge-functions/stripe-worker.ts
57
+ var stripe_worker_default = "/**\n * Stripe Sync Worker\n *\n * Triggered by pg_cron at a configurable interval (default: 60 seconds). Uses pgmq for durable work queue.\n *\n * Flow:\n * 1. Read batch of messages from pgmq (qty=10, vt=60s)\n * 2. If queue empty: enqueue all objects (continuous sync)\n * 3. Process messages in parallel (Promise.all):\n * - processNext(object)\n * - Delete message on success\n * - Re-enqueue if hasMore\n * 4. Return results summary\n *\n * Concurrency:\n * - Multiple workers can run concurrently via overlapping pg_cron triggers.\n * - Each worker processes its batch of messages in parallel (Promise.all).\n * - pgmq visibility timeout prevents duplicate message reads across workers.\n * - processNext() is idempotent (uses internal cursor tracking), so duplicate\n * processing on timeout/crash is safe.\n */\n\nimport { StripeSync } from 'npm:stripe-experiment-sync'\nimport postgres from 'npm:postgres'\n\nconst QUEUE_NAME = 'stripe_sync_work'\nconst VISIBILITY_TIMEOUT = 60 // seconds\nconst BATCH_SIZE = 10\n\nDeno.serve(async (req) => {\n const authHeader = req.headers.get('Authorization')\n if (!authHeader?.startsWith('Bearer ')) {\n return new Response('Unauthorized', { status: 401 })\n }\n\n const rawDbUrl = Deno.env.get('SUPABASE_DB_URL')\n if (!rawDbUrl) {\n return new Response(JSON.stringify({ error: 'SUPABASE_DB_URL not set' }), { status: 500 })\n }\n const dbUrl = rawDbUrl.replace(/[?&]sslmode=[^&]*/g, '').replace(/[?&]$/, '')\n\n let sql\n let stripeSync\n\n try {\n sql = postgres(dbUrl, { max: 1, prepare: false })\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: 'Failed to create postgres connection',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n stripeSync = new StripeSync({\n poolConfig: { connectionString: dbUrl, max: 1 },\n stripeSecretKey: Deno.env.get('STRIPE_SECRET_KEY')!,\n })\n } catch (error) {\n await sql.end()\n return new Response(\n JSON.stringify({\n error: 'Failed to create StripeSync',\n details: error.message,\n stack: error.stack,\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n )\n }\n\n try {\n // Read batch of messages from queue\n const messages = await sql`\n SELECT * FROM pgmq.read(${QUEUE_NAME}::text, ${VISIBILITY_TIMEOUT}::int, ${BATCH_SIZE}::int)\n `\n\n // If queue empty, enqueue all objects for continuous sync\n if (messages.length === 0) {\n // Create sync run to make enqueued work visible (status='pending')\n const { objects } = await stripeSync.joinOrCreateSyncRun('worker')\n const msgs = objects.map((object) => JSON.stringify({ object }))\n\n await sql`\n SELECT pgmq.send_batch(\n ${QUEUE_NAME}::text,\n ${sql.array(msgs)}::jsonb[]\n )\n `\n\n return new Response(JSON.stringify({ enqueued: objects.length, objects }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n }\n\n // Process messages in parallel\n const results = await Promise.all(\n messages.map(async (msg) => {\n const { object } = msg.message as { object: string }\n\n try {\n const result = await stripeSync.processNext(object)\n\n // Delete message on success (cast to bigint to disambiguate overloaded function)\n await sql`SELECT pgmq.delete(${QUEUE_NAME}::text, ${msg.msg_id}::bigint)`\n\n // Re-enqueue if more pages\n if (result.hasMore) {\n await sql`SELECT pgmq.send(${QUEUE_NAME}::text, ${sql.json({ object })}::jsonb)`\n }\n\n return { object, ...result }\n } catch (error) {\n // Log error but continue to next message\n // Message will become visible again after visibility timeout\n console.error(`Error processing ${object}:`, error)\n return {\n object,\n processed: 0,\n hasMore: false,\n error: error.message,\n stack: error.stack,\n }\n }\n })\n )\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n })\n } catch (error) {\n console.error('Worker error:', error)\n return new Response(JSON.stringify({ error: error.message, stack: error.stack }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n })\n } finally {\n if (sql) await sql.end()\n if (stripeSync) await stripeSync.postgresClient.pool.end()\n }\n})\n";
58
58
 
59
59
  // src/supabase/edge-function-code.ts
60
60
  var setupFunctionCode = stripe_setup_default;
@@ -64,7 +64,7 @@ var workerFunctionCode = stripe_worker_default;
64
64
  // package.json
65
65
  var package_default = {
66
66
  name: "stripe-experiment-sync",
67
- version: "1.0.9",
67
+ version: "1.0.11",
68
68
  private: false,
69
69
  description: "Stripe Sync Engine to sync Stripe data to Postgres",
70
70
  type: "module",
@@ -210,8 +210,29 @@ var SupabaseSetupClient = class {
210
210
  }
211
211
  /**
212
212
  * Setup pg_cron job to invoke worker function
213
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
213
214
  */
214
- async setupPgCronJob() {
215
+ async setupPgCronJob(intervalSeconds = 60) {
216
+ if (!Number.isInteger(intervalSeconds) || intervalSeconds < 1) {
217
+ throw new Error(`Invalid interval: ${intervalSeconds}. Must be a positive integer.`);
218
+ }
219
+ let schedule;
220
+ if (intervalSeconds < 60) {
221
+ schedule = `${intervalSeconds} seconds`;
222
+ } else if (intervalSeconds % 60 === 0) {
223
+ const minutes = intervalSeconds / 60;
224
+ if (minutes < 60) {
225
+ schedule = `*/${minutes} * * * *`;
226
+ } else {
227
+ throw new Error(
228
+ `Invalid interval: ${intervalSeconds}. Intervals >= 3600 seconds (1 hour) are not supported. Use a value between 1-3599 seconds.`
229
+ );
230
+ }
231
+ } else {
232
+ throw new Error(
233
+ `Invalid interval: ${intervalSeconds}. Must be either 1-59 seconds or a multiple of 60 (e.g., 60, 120, 180).`
234
+ );
235
+ }
215
236
  const serviceRoleKey = await this.getServiceRoleKey();
216
237
  const escapedServiceRoleKey = serviceRoleKey.replace(/'/g, "''");
217
238
  const sql = `
@@ -239,11 +260,11 @@ var SupabaseSetupClient = class {
239
260
  SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-scheduler'
240
261
  );
241
262
 
242
- -- Create job to invoke worker every 10 seconds
263
+ -- Create job to invoke worker at configured interval
243
264
  -- Worker reads from pgmq, enqueues objects if empty, and processes sync work
244
265
  SELECT cron.schedule(
245
266
  'stripe-sync-worker',
246
- '10 seconds',
267
+ '${schedule}',
247
268
  $$
248
269
  SELECT net.http_post(
249
270
  url := 'https://${this.projectRef}.${this.projectBaseUrl}/functions/v1/stripe-worker',
@@ -427,10 +448,12 @@ var SupabaseSetupClient = class {
427
448
  await this.deleteSecret("STRIPE_SECRET_KEY");
428
449
  try {
429
450
  await this.runSQL(`
430
- SELECT cron.unschedule('stripe-sync-worker')
431
- WHERE EXISTS (
432
- SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker'
433
- )
451
+ DO $$
452
+ BEGIN
453
+ IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'stripe-sync-worker') THEN
454
+ PERFORM cron.unschedule('stripe-sync-worker');
455
+ END IF;
456
+ END $$;
434
457
  `);
435
458
  } catch (err) {
436
459
  console.warn("Could not unschedule pg_cron job:", err);
@@ -446,15 +469,31 @@ var SupabaseSetupClient = class {
446
469
  try {
447
470
  await this.runSQL(`
448
471
  SELECT pg_terminate_backend(pid)
449
- FROM pg_stat_activity
450
- WHERE datname = current_database()
451
- AND pid != pg_backend_pid()
452
- AND query ILIKE '%stripe.%'
472
+ FROM pg_locks l
473
+ JOIN pg_class c ON l.relation = c.oid
474
+ JOIN pg_namespace n ON c.relnamespace = n.oid
475
+ WHERE n.nspname = 'stripe'
476
+ AND l.pid != pg_backend_pid()
453
477
  `);
454
478
  } catch (err) {
455
479
  console.warn("Could not terminate connections:", err);
456
480
  }
457
- await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
481
+ let dropAttempts = 0;
482
+ const maxAttempts = 3;
483
+ while (dropAttempts < maxAttempts) {
484
+ try {
485
+ await this.runSQL(`DROP SCHEMA IF EXISTS stripe CASCADE`);
486
+ break;
487
+ } catch (err) {
488
+ dropAttempts++;
489
+ if (dropAttempts >= maxAttempts) {
490
+ throw new Error(
491
+ `Failed to drop schema after ${maxAttempts} attempts. There may be active connections or locks on the stripe schema. Error: ${err instanceof Error ? err.message : String(err)}`
492
+ );
493
+ }
494
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
495
+ }
496
+ }
458
497
  } catch (error) {
459
498
  throw new Error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
460
499
  }
@@ -471,7 +510,7 @@ var SupabaseSetupClient = class {
471
510
  `from 'npm:stripe-experiment-sync@${version}'`
472
511
  );
473
512
  }
474
- async install(stripeKey, packageVersion) {
513
+ async install(stripeKey, packageVersion, workerIntervalSeconds) {
475
514
  const trimmedStripeKey = stripeKey.trim();
476
515
  if (!trimmedStripeKey.startsWith("sk_") && !trimmedStripeKey.startsWith("rk_")) {
477
516
  throw new Error('Stripe key should start with "sk_" or "rk_"');
@@ -495,7 +534,7 @@ var SupabaseSetupClient = class {
495
534
  if (!setupResult.success) {
496
535
  throw new Error(`Setup failed: ${setupResult.error}`);
497
536
  }
498
- await this.setupPgCronJob();
537
+ await this.setupPgCronJob(workerIntervalSeconds);
499
538
  await this.updateInstallationComment(
500
539
  `${STRIPE_SCHEMA_COMMENT_PREFIX} v${package_default.version} ${INSTALLATION_INSTALLED_SUFFIX}`
501
540
  );
@@ -508,14 +547,20 @@ var SupabaseSetupClient = class {
508
547
  }
509
548
  };
510
549
  async function install(params) {
511
- const { supabaseAccessToken, supabaseProjectRef, stripeKey, packageVersion } = params;
550
+ const {
551
+ supabaseAccessToken,
552
+ supabaseProjectRef,
553
+ stripeKey,
554
+ packageVersion,
555
+ workerIntervalSeconds
556
+ } = params;
512
557
  const client = new SupabaseSetupClient({
513
558
  accessToken: supabaseAccessToken,
514
559
  projectRef: supabaseProjectRef,
515
560
  projectBaseUrl: params.baseProjectUrl,
516
561
  managementApiBaseUrl: params.baseManagementApiUrl
517
562
  });
518
- await client.install(stripeKey, packageVersion);
563
+ await client.install(stripeKey, packageVersion, workerIntervalSeconds);
519
564
  }
520
565
  async function uninstall(params) {
521
566
  const { supabaseAccessToken, supabaseProjectRef, stripeKey } = params;
@@ -39,8 +39,9 @@ declare class SupabaseSetupClient {
39
39
  runSQL(sql: string): Promise<unknown>;
40
40
  /**
41
41
  * Setup pg_cron job to invoke worker function
42
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
42
43
  */
43
- setupPgCronJob(): Promise<void>;
44
+ setupPgCronJob(intervalSeconds?: number): Promise<void>;
44
45
  /**
45
46
  * Get the webhook URL for this project
46
47
  */
@@ -97,13 +98,14 @@ declare class SupabaseSetupClient {
97
98
  * Inject package version into Edge Function code
98
99
  */
99
100
  private injectPackageVersion;
100
- install(stripeKey: string, packageVersion?: string): Promise<void>;
101
+ install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
101
102
  }
102
103
  declare function install(params: {
103
104
  supabaseAccessToken: string;
104
105
  supabaseProjectRef: string;
105
106
  stripeKey: string;
106
107
  packageVersion?: string;
108
+ workerIntervalSeconds?: number;
107
109
  baseProjectUrl?: string;
108
110
  baseManagementApiUrl?: string;
109
111
  }): Promise<void>;
@@ -39,8 +39,9 @@ declare class SupabaseSetupClient {
39
39
  runSQL(sql: string): Promise<unknown>;
40
40
  /**
41
41
  * Setup pg_cron job to invoke worker function
42
+ * @param intervalSeconds - How often to run the worker (default: 60 seconds)
42
43
  */
43
- setupPgCronJob(): Promise<void>;
44
+ setupPgCronJob(intervalSeconds?: number): Promise<void>;
44
45
  /**
45
46
  * Get the webhook URL for this project
46
47
  */
@@ -97,13 +98,14 @@ declare class SupabaseSetupClient {
97
98
  * Inject package version into Edge Function code
98
99
  */
99
100
  private injectPackageVersion;
100
- install(stripeKey: string, packageVersion?: string): Promise<void>;
101
+ install(stripeKey: string, packageVersion?: string, workerIntervalSeconds?: number): Promise<void>;
101
102
  }
102
103
  declare function install(params: {
103
104
  supabaseAccessToken: string;
104
105
  supabaseProjectRef: string;
105
106
  stripeKey: string;
106
107
  packageVersion?: string;
108
+ workerIntervalSeconds?: number;
107
109
  baseProjectUrl?: string;
108
110
  baseManagementApiUrl?: string;
109
111
  }): Promise<void>;
@@ -9,8 +9,8 @@ import {
9
9
  uninstall,
10
10
  webhookFunctionCode,
11
11
  workerFunctionCode
12
- } from "../chunk-OLVA37VZ.js";
13
- import "../chunk-RR5BGG4F.js";
12
+ } from "../chunk-EVNKMOYA.js";
13
+ import "../chunk-XTL72K7K.js";
14
14
  export {
15
15
  INSTALLATION_ERROR_SUFFIX,
16
16
  INSTALLATION_INSTALLED_SUFFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stripe-experiment-sync",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "private": false,
5
5
  "description": "Stripe Sync Engine to sync Stripe data to Postgres",
6
6
  "type": "module",