stripe-experiment-sync 1.0.21 → 1.0.23

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.
package/dist/index.js CHANGED
@@ -1,17 +1,25 @@
1
1
  import {
2
2
  PostgresClient,
3
3
  StripeSync,
4
+ StripeSyncWorker,
4
5
  VERSION,
5
6
  createStripeWebSocketClient,
7
+ embeddedMigrations,
8
+ getTableName,
6
9
  hashApiKey,
7
- runMigrations
8
- } from "./chunk-ECLPGCY6.js";
9
- import "./chunk-OWZ4QNLS.js";
10
+ runMigrations,
11
+ runMigrationsFromContent
12
+ } from "./chunk-K22YTC4W.js";
13
+ import "./chunk-5VGSSPW3.js";
10
14
  export {
11
15
  PostgresClient,
12
16
  StripeSync,
17
+ StripeSyncWorker,
13
18
  VERSION,
14
19
  createStripeWebSocketClient,
20
+ embeddedMigrations,
21
+ getTableName,
15
22
  hashApiKey,
16
- runMigrations
23
+ runMigrations,
24
+ runMigrationsFromContent
17
25
  };
@@ -0,0 +1,13 @@
1
+ -- Add created_gte / created_lte columns for time-range partitioned parallel sync.
2
+ -- Workers use these to scope their Stripe list calls to a specific created window.
3
+ -- Stored as Unix epoch seconds (INTEGER) to match Stripe's created filter format.
4
+ -- created_gte defaults to 0 for non-chunked rows (required by PK).
5
+ ALTER TABLE "stripe"."_sync_obj_runs" ADD COLUMN IF NOT EXISTS created_gte INTEGER NOT NULL DEFAULT 0;
6
+ ALTER TABLE "stripe"."_sync_obj_runs" ADD COLUMN IF NOT EXISTS created_lte INTEGER;
7
+
8
+ -- Expand PK to include created_gte so multiple time-range chunks of the same object can coexist.
9
+ -- PK constraint kept original name from 0053 (_sync_obj_run_pkey) after table rename in 0057.
10
+ ALTER TABLE "stripe"."_sync_obj_runs" DROP CONSTRAINT IF EXISTS "_sync_obj_runs_pkey";
11
+ ALTER TABLE "stripe"."_sync_obj_runs" DROP CONSTRAINT IF EXISTS "_sync_obj_run_pkey";
12
+ ALTER TABLE "stripe"."_sync_obj_runs"
13
+ ADD CONSTRAINT "_sync_obj_runs_pkey" PRIMARY KEY ("_account_id", run_started_at, object, created_gte);
@@ -0,0 +1,15 @@
1
+ -- Include created_lte in the PK so chunks with the same created_gte but
2
+ -- different created_lte can coexist. Requires a NOT NULL default first.
3
+ ALTER TABLE "stripe"."_sync_obj_runs"
4
+ ALTER COLUMN created_lte SET DEFAULT 0;
5
+
6
+ UPDATE "stripe"."_sync_obj_runs"
7
+ SET created_lte = 0
8
+ WHERE created_lte IS NULL;
9
+
10
+ ALTER TABLE "stripe"."_sync_obj_runs"
11
+ ALTER COLUMN created_lte SET NOT NULL;
12
+
13
+ ALTER TABLE "stripe"."_sync_obj_runs" DROP CONSTRAINT IF EXISTS "_sync_obj_runs_pkey";
14
+ ALTER TABLE "stripe"."_sync_obj_runs"
15
+ ADD CONSTRAINT "_sync_obj_runs_pkey" PRIMARY KEY ("_account_id", run_started_at, object, created_gte, created_lte);
@@ -0,0 +1,43 @@
1
+ -- Rate limiting table and function for cross-process request throttling.
2
+ -- Used by claimNextTask to cap how many claims/sec hit the database.
3
+
4
+ CREATE TABLE IF NOT EXISTS "stripe"."_rate_limits" (
5
+ key TEXT PRIMARY KEY,
6
+ count INTEGER NOT NULL DEFAULT 0,
7
+ window_start TIMESTAMPTZ NOT NULL DEFAULT now()
8
+ );
9
+
10
+ CREATE OR REPLACE FUNCTION "stripe".check_rate_limit(
11
+ rate_key TEXT,
12
+ max_requests INTEGER,
13
+ window_seconds INTEGER
14
+ )
15
+ RETURNS VOID AS $$
16
+ DECLARE
17
+ now TIMESTAMPTZ := clock_timestamp();
18
+ window_length INTERVAL := make_interval(secs => window_seconds);
19
+ current_count INTEGER;
20
+ BEGIN
21
+ PERFORM pg_advisory_xact_lock(hashtext(rate_key));
22
+
23
+ INSERT INTO "stripe"."_rate_limits" (key, count, window_start)
24
+ VALUES (rate_key, 1, now)
25
+ ON CONFLICT (key) DO UPDATE
26
+ SET count = CASE
27
+ WHEN "_rate_limits".window_start + window_length <= now
28
+ THEN 1
29
+ ELSE "_rate_limits".count + 1
30
+ END,
31
+ window_start = CASE
32
+ WHEN "_rate_limits".window_start + window_length <= now
33
+ THEN now
34
+ ELSE "_rate_limits".window_start
35
+ END;
36
+
37
+ SELECT count INTO current_count FROM "stripe"."_rate_limits" WHERE key = rate_key;
38
+
39
+ IF current_count > max_requests THEN
40
+ RAISE EXCEPTION 'Rate limit exceeded for %', rate_key;
41
+ END IF;
42
+ END;
43
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,10 @@
1
+ -- Add priority column to _sync_obj_runs for deterministic task ordering.
2
+ -- Priority mirrors the `order` field from resourceRegistry so workers
3
+ -- always process parent resources (products, prices) before children
4
+ -- (subscriptions, invoices).
5
+
6
+ ALTER TABLE "stripe"."_sync_obj_runs"
7
+ ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0;
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_sync_obj_runs_priority
10
+ ON "stripe"."_sync_obj_runs" ("_account_id", run_started_at, status, priority);
@@ -0,0 +1,23 @@
1
+ -- Per-object sync progress view for monitoring.
2
+ -- Defaults to the newest run per account; callers can filter by a specific
3
+ -- run_started_at if needed.
4
+
5
+ DROP FUNCTION IF EXISTS "stripe"."sync_obj_progress"(TEXT, TIMESTAMPTZ);
6
+
7
+ CREATE OR REPLACE VIEW "stripe"."sync_obj_progress" AS
8
+ SELECT
9
+ r."_account_id" AS account_id,
10
+ r.run_started_at,
11
+ r.object,
12
+ ROUND(
13
+ 100.0 * COUNT(*) FILTER (WHERE r.status = 'complete') / NULLIF(COUNT(*), 0),
14
+ 1
15
+ ) AS pct_complete,
16
+ COALESCE(SUM(r.processed_count), 0) AS processed
17
+ FROM "stripe"."_sync_obj_runs" r
18
+ WHERE r.run_started_at = (
19
+ SELECT MAX(s.started_at)
20
+ FROM "stripe"."_sync_runs" s
21
+ WHERE s."_account_id" = r."_account_id"
22
+ )
23
+ GROUP BY r."_account_id", r.run_started_at, r.object;
@@ -0,0 +1,238 @@
1
+ // src/supabase/edge-functions/sigma-data-worker.ts
2
+ import { StripeSync } from "npm:stripe-experiment-sync";
3
+ import postgres from "npm:postgres";
4
+ var BATCH_SIZE = 1;
5
+ var MAX_RUN_AGE_MS = 6 * 60 * 60 * 1e3;
6
+ var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
7
+ status,
8
+ headers: { "Content-Type": "application/json" }
9
+ });
10
+ Deno.serve(async (req) => {
11
+ const authHeader = req.headers.get("Authorization");
12
+ if (!authHeader?.startsWith("Bearer ")) {
13
+ return new Response("Unauthorized", { status: 401 });
14
+ }
15
+ const token = authHeader.substring(7);
16
+ const dbUrl = Deno.env.get("SUPABASE_DB_URL");
17
+ if (!dbUrl) {
18
+ return jsonResponse({ error: "SUPABASE_DB_URL not set" }, 500);
19
+ }
20
+ let sql;
21
+ let stripeSync;
22
+ try {
23
+ sql = postgres(dbUrl, { max: 1, prepare: false });
24
+ } catch (error) {
25
+ return jsonResponse(
26
+ {
27
+ error: "Failed to create postgres connection",
28
+ details: error.message,
29
+ stack: error.stack
30
+ },
31
+ 500
32
+ );
33
+ }
34
+ try {
35
+ const vaultResult = await sql`
36
+ SELECT decrypted_secret
37
+ FROM vault.decrypted_secrets
38
+ WHERE name = 'stripe_sigma_worker_secret'
39
+ `;
40
+ if (vaultResult.length === 0) {
41
+ await sql.end();
42
+ return new Response("Sigma worker secret not configured in vault", { status: 500 });
43
+ }
44
+ const storedSecret = vaultResult[0].decrypted_secret;
45
+ if (token !== storedSecret) {
46
+ await sql.end();
47
+ return new Response("Forbidden: Invalid sigma worker secret", { status: 403 });
48
+ }
49
+ stripeSync = await StripeSync.create({
50
+ poolConfig: { connectionString: dbUrl, max: 1 },
51
+ stripeSecretKey: Deno.env.get("STRIPE_SECRET_KEY"),
52
+ enableSigma: true,
53
+ sigmaPageSizeOverride: 1e3
54
+ });
55
+ } catch (error) {
56
+ await sql.end();
57
+ return jsonResponse(
58
+ {
59
+ error: "Failed to create StripeSync",
60
+ details: error.message,
61
+ stack: error.stack
62
+ },
63
+ 500
64
+ );
65
+ }
66
+ try {
67
+ const accountId = await stripeSync.getAccountId();
68
+ const sigmaObjects = stripeSync.getSupportedSigmaObjects();
69
+ if (sigmaObjects.length === 0) {
70
+ return jsonResponse({ message: "No Sigma objects configured for sync" });
71
+ }
72
+ const runResult = await stripeSync.postgresClient.getOrCreateSyncRun(accountId, "sigma-worker");
73
+ const runStartedAt = runResult?.runStartedAt ?? (await stripeSync.postgresClient.getActiveSyncRun(accountId, "sigma-worker"))?.runStartedAt;
74
+ if (!runStartedAt) {
75
+ throw new Error("Failed to get or create sync run for sigma worker");
76
+ }
77
+ await stripeSync.postgresClient.query(
78
+ `UPDATE "stripe"."_sync_obj_runs"
79
+ SET status = 'error',
80
+ error_message = 'Legacy sigma worker prefix run (sigma.*); superseded by unprefixed runs',
81
+ completed_at = now()
82
+ WHERE "_account_id" = $1
83
+ AND run_started_at = $2
84
+ AND object LIKE 'sigma.%'
85
+ AND status IN ('pending', 'running')`,
86
+ [accountId, runStartedAt]
87
+ );
88
+ const runAgeMs = Date.now() - runStartedAt.getTime();
89
+ if (runAgeMs > MAX_RUN_AGE_MS) {
90
+ console.warn(
91
+ `Sigma worker: run too old (${Math.round(runAgeMs / 1e3 / 60)} min), closing without self-trigger`
92
+ );
93
+ await stripeSync.postgresClient.closeSyncRun(accountId, runStartedAt);
94
+ return jsonResponse({
95
+ message: "Sigma run exceeded max age, closed without processing",
96
+ runAgeMinutes: Math.round(runAgeMs / 1e3 / 60),
97
+ selfTriggered: false
98
+ });
99
+ }
100
+ await stripeSync.postgresClient.createObjectRuns(accountId, runStartedAt, sigmaObjects);
101
+ await stripeSync.postgresClient.ensureSyncRunMaxConcurrent(accountId, runStartedAt, BATCH_SIZE);
102
+ const runningObjects = await stripeSync.postgresClient.listObjectsByStatus(
103
+ accountId,
104
+ runStartedAt,
105
+ "running",
106
+ sigmaObjects
107
+ );
108
+ const objectsToProcess = runningObjects.slice(0, BATCH_SIZE);
109
+ let pendingObjects = [];
110
+ if (objectsToProcess.length === 0) {
111
+ pendingObjects = await stripeSync.postgresClient.listObjectsByStatus(
112
+ accountId,
113
+ runStartedAt,
114
+ "pending",
115
+ sigmaObjects
116
+ );
117
+ for (const objectKey of pendingObjects) {
118
+ if (objectsToProcess.length >= BATCH_SIZE) break;
119
+ const started = await stripeSync.postgresClient.tryStartObjectSync(
120
+ accountId,
121
+ runStartedAt,
122
+ objectKey
123
+ );
124
+ if (started) {
125
+ objectsToProcess.push(objectKey);
126
+ }
127
+ }
128
+ }
129
+ if (objectsToProcess.length === 0) {
130
+ if (pendingObjects.length === 0) {
131
+ console.info("Sigma worker: all objects complete or errored - run finished");
132
+ return jsonResponse({ message: "Sigma sync run complete", selfTriggered: false });
133
+ }
134
+ console.info("Sigma worker: at concurrency limit, will self-trigger", {
135
+ pendingCount: pendingObjects.length
136
+ });
137
+ let selfTriggered2 = false;
138
+ try {
139
+ await sql`SELECT stripe.trigger_sigma_worker()`;
140
+ selfTriggered2 = true;
141
+ } catch (error) {
142
+ console.warn("Failed to self-trigger sigma worker:", error.message);
143
+ }
144
+ return jsonResponse({
145
+ message: "At concurrency limit",
146
+ pendingCount: pendingObjects.length,
147
+ selfTriggered: selfTriggered2
148
+ });
149
+ }
150
+ const results = [];
151
+ for (const object of objectsToProcess) {
152
+ const objectKey = object;
153
+ try {
154
+ console.info(`Sigma worker: processing ${object}`);
155
+ const result = await stripeSync.processNext(object, {
156
+ runStartedAt,
157
+ triggeredBy: "sigma-worker"
158
+ });
159
+ results.push({
160
+ object,
161
+ processed: result.processed,
162
+ hasMore: result.hasMore,
163
+ status: "success"
164
+ });
165
+ if (result.hasMore) {
166
+ console.info(
167
+ `Sigma worker: ${object} has more pages, processed ${result.processed} rows so far`
168
+ );
169
+ } else {
170
+ console.info(`Sigma worker: ${object} complete, processed ${result.processed} rows`);
171
+ }
172
+ } catch (error) {
173
+ console.error(`Sigma worker: error processing ${object}:`, error);
174
+ await stripeSync.postgresClient.failObjectSync(
175
+ accountId,
176
+ runStartedAt,
177
+ objectKey,
178
+ error.message ?? "Unknown error"
179
+ );
180
+ results.push({
181
+ object,
182
+ processed: 0,
183
+ hasMore: false,
184
+ status: "error",
185
+ error: error.message
186
+ });
187
+ }
188
+ }
189
+ const pendingAfter = await stripeSync.postgresClient.listObjectsByStatus(
190
+ accountId,
191
+ runStartedAt,
192
+ "pending",
193
+ sigmaObjects
194
+ );
195
+ const runningAfter = await stripeSync.postgresClient.listObjectsByStatus(
196
+ accountId,
197
+ runStartedAt,
198
+ "running",
199
+ sigmaObjects
200
+ );
201
+ const remainingMs = MAX_RUN_AGE_MS - (Date.now() - runStartedAt.getTime());
202
+ const remainingMinutes = Math.round(remainingMs / 1e3 / 60);
203
+ const shouldSelfTrigger = (pendingAfter.length > 0 || runningAfter.length > 0) && remainingMs > 0;
204
+ let selfTriggered = false;
205
+ if (shouldSelfTrigger) {
206
+ console.info("Sigma worker: more work remains, self-triggering", {
207
+ pending: pendingAfter.length,
208
+ running: runningAfter.length,
209
+ remainingMinutes
210
+ });
211
+ try {
212
+ await sql`SELECT stripe.trigger_sigma_worker()`;
213
+ selfTriggered = true;
214
+ } catch (error) {
215
+ console.warn("Failed to self-trigger sigma worker:", error.message);
216
+ }
217
+ } else if (pendingAfter.length > 0 || runningAfter.length > 0) {
218
+ console.warn("Sigma worker: work remains but run timed out, closing", {
219
+ pending: pendingAfter.length,
220
+ running: runningAfter.length
221
+ });
222
+ await stripeSync.postgresClient.closeSyncRun(accountId, runStartedAt);
223
+ } else {
224
+ console.info("Sigma worker: no more work, run complete");
225
+ }
226
+ return jsonResponse({
227
+ results,
228
+ selfTriggered,
229
+ remaining: { pending: pendingAfter.length, running: runningAfter.length }
230
+ });
231
+ } catch (error) {
232
+ console.error("Sigma worker error:", error);
233
+ return jsonResponse({ error: error.message, stack: error.stack }, 500);
234
+ } finally {
235
+ if (sql) await sql.end();
236
+ if (stripeSync) await stripeSync.postgresClient.pool.end();
237
+ }
238
+ });