stripe-experiment-sync 1.0.13 → 1.0.15
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/{chunk-RCU5ZXAX.js → chunk-3KVILTN4.js} +3 -1
- package/dist/{chunk-5SS5ZEQF.js → chunk-AODI5WO6.js} +27 -10
- package/dist/{chunk-2GSABFXH.js → chunk-GOBVFXU7.js} +806 -209
- package/dist/chunk-XIFB5ALW.js +400 -0
- package/dist/cli/index.cjs +850 -250
- package/dist/cli/index.js +7 -6
- package/dist/cli/lib.cjs +847 -248
- package/dist/cli/lib.d.cts +2 -0
- package/dist/cli/lib.d.ts +2 -0
- package/dist/cli/lib.js +4 -4
- package/dist/index.cjs +807 -208
- package/dist/index.d.cts +110 -3
- package/dist/index.d.ts +110 -3
- package/dist/index.js +2 -2
- package/dist/migrations/0059_sigma_subscription_item_change_events_v2_beta.sql +61 -0
- package/dist/migrations/0060_sigma_exchange_rates_from_usd.sql +38 -0
- package/dist/supabase/index.cjs +18 -33
- package/dist/supabase/index.d.cts +1 -5
- package/dist/supabase/index.d.ts +1 -5
- package/dist/supabase/index.js +2 -2
- package/package.json +3 -1
- package/dist/chunk-O2N4AEQS.js +0 -417
|
@@ -1,16 +1,62 @@
|
|
|
1
1
|
import {
|
|
2
2
|
package_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3KVILTN4.js";
|
|
4
4
|
|
|
5
5
|
// src/stripeSync.ts
|
|
6
|
-
import
|
|
6
|
+
import Stripe3 from "stripe";
|
|
7
7
|
import { pg as sql2 } from "yesql";
|
|
8
8
|
|
|
9
9
|
// src/database/postgres.ts
|
|
10
10
|
import pg from "pg";
|
|
11
11
|
import { pg as sql } from "yesql";
|
|
12
|
+
|
|
13
|
+
// src/database/QueryUtils.ts
|
|
14
|
+
var QueryUtils = class _QueryUtils {
|
|
15
|
+
constructor() {
|
|
16
|
+
}
|
|
17
|
+
static quoteIdent(name) {
|
|
18
|
+
return `"${name}"`;
|
|
19
|
+
}
|
|
20
|
+
static quotedList(names) {
|
|
21
|
+
return names.map(_QueryUtils.quoteIdent).join(", ");
|
|
22
|
+
}
|
|
23
|
+
static buildInsertParts(columns) {
|
|
24
|
+
const columnsSql = columns.map((c) => _QueryUtils.quoteIdent(c.column)).join(", ");
|
|
25
|
+
const valuesSql = columns.map((c, i) => {
|
|
26
|
+
const placeholder = `$${i + 1}`;
|
|
27
|
+
return `${placeholder}::${c.pgType}`;
|
|
28
|
+
}).join(", ");
|
|
29
|
+
const params = columns.map((c) => c.value);
|
|
30
|
+
return { columnsSql, valuesSql, params };
|
|
31
|
+
}
|
|
32
|
+
static buildRawJsonUpsertQuery(schema, table, columns, conflictTarget) {
|
|
33
|
+
const { columnsSql, valuesSql, params } = _QueryUtils.buildInsertParts(columns);
|
|
34
|
+
const conflictSql = _QueryUtils.quotedList(conflictTarget);
|
|
35
|
+
const tsParamIdx = columns.findIndex((c) => c.column === "_last_synced_at") + 1;
|
|
36
|
+
if (tsParamIdx <= 0) {
|
|
37
|
+
throw new Error("buildRawJsonUpsertQuery requires _last_synced_at column");
|
|
38
|
+
}
|
|
39
|
+
const sql3 = `
|
|
40
|
+
INSERT INTO ${_QueryUtils.quoteIdent(schema)}.${_QueryUtils.quoteIdent(table)} (${columnsSql})
|
|
41
|
+
VALUES (${valuesSql})
|
|
42
|
+
ON CONFLICT (${conflictSql})
|
|
43
|
+
DO UPDATE SET
|
|
44
|
+
"_raw_data" = EXCLUDED."_raw_data",
|
|
45
|
+
"_last_synced_at" = $${tsParamIdx},
|
|
46
|
+
"_account_id" = EXCLUDED."_account_id"
|
|
47
|
+
WHERE ${_QueryUtils.quoteIdent(table)}."_last_synced_at" IS NULL
|
|
48
|
+
OR ${_QueryUtils.quoteIdent(table)}."_last_synced_at" < $${tsParamIdx}
|
|
49
|
+
RETURNING *
|
|
50
|
+
`;
|
|
51
|
+
return { sql: sql3, params };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/database/postgres.ts
|
|
12
56
|
var ORDERED_STRIPE_TABLES = [
|
|
57
|
+
"exchange_rates_from_usd",
|
|
13
58
|
"subscription_items",
|
|
59
|
+
"subscription_item_change_events_v2_beta",
|
|
14
60
|
"subscriptions",
|
|
15
61
|
"subscription_schedules",
|
|
16
62
|
"checkout_session_line_items",
|
|
@@ -80,7 +126,7 @@ var PostgresClient = class {
|
|
|
80
126
|
}
|
|
81
127
|
return results.flatMap((it) => it.rows);
|
|
82
128
|
}
|
|
83
|
-
async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp) {
|
|
129
|
+
async upsertManyWithTimestampProtection(entries, table, accountId, syncTimestamp, upsertOptions) {
|
|
84
130
|
const timestamp = syncTimestamp || (/* @__PURE__ */ new Date()).toISOString();
|
|
85
131
|
if (!entries.length) return [];
|
|
86
132
|
const chunkSize = 5;
|
|
@@ -115,20 +161,33 @@ var PostgresClient = class {
|
|
|
115
161
|
const prepared = sql(upsertSql, { useNullForMissing: true })(cleansed);
|
|
116
162
|
queries.push(this.pool.query(prepared.text, prepared.values));
|
|
117
163
|
} else {
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
164
|
+
const conflictTarget = upsertOptions?.conflictTarget ?? ["id"];
|
|
165
|
+
const extraColumns = upsertOptions?.extraColumns ?? [];
|
|
166
|
+
if (!conflictTarget.length) {
|
|
167
|
+
throw new Error(`Invalid upsert config for ${table}: conflictTarget must be non-empty`);
|
|
168
|
+
}
|
|
169
|
+
const columns = [
|
|
170
|
+
{ column: "_raw_data", pgType: "jsonb", value: JSON.stringify(entry) },
|
|
171
|
+
...extraColumns.map((c) => ({
|
|
172
|
+
column: c.column,
|
|
173
|
+
pgType: c.pgType,
|
|
174
|
+
value: entry[c.entryKey]
|
|
175
|
+
})),
|
|
176
|
+
{ column: "_last_synced_at", pgType: "timestamptz", value: timestamp },
|
|
177
|
+
{ column: "_account_id", pgType: "text", value: accountId }
|
|
178
|
+
];
|
|
179
|
+
for (const c of columns) {
|
|
180
|
+
if (c.value === void 0) {
|
|
181
|
+
throw new Error(`Missing required value for ${table}.${c.column}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const { sql: upsertSql, params } = QueryUtils.buildRawJsonUpsertQuery(
|
|
185
|
+
this.config.schema,
|
|
186
|
+
table,
|
|
187
|
+
columns,
|
|
188
|
+
conflictTarget
|
|
189
|
+
);
|
|
190
|
+
queries.push(this.pool.query(upsertSql, params));
|
|
132
191
|
}
|
|
133
192
|
});
|
|
134
193
|
results.push(...await Promise.all(queries));
|
|
@@ -528,7 +587,12 @@ var PostgresClient = class {
|
|
|
528
587
|
} else {
|
|
529
588
|
await this.query(
|
|
530
589
|
`UPDATE "${this.config.schema}"."_sync_obj_runs"
|
|
531
|
-
SET cursor =
|
|
590
|
+
SET cursor = CASE
|
|
591
|
+
WHEN cursor IS NULL THEN $4
|
|
592
|
+
WHEN (cursor COLLATE "C") < ($4::text COLLATE "C") THEN $4
|
|
593
|
+
ELSE cursor
|
|
594
|
+
END,
|
|
595
|
+
updated_at = now()
|
|
532
596
|
WHERE "_account_id" = $1 AND run_started_at = $2 AND object = $3`,
|
|
533
597
|
[accountId, runStartedAt, object, cursor]
|
|
534
598
|
);
|
|
@@ -539,10 +603,17 @@ var PostgresClient = class {
|
|
|
539
603
|
* This considers completed, error, AND running runs to ensure recovery syncs
|
|
540
604
|
* don't re-process data that was already synced before a crash.
|
|
541
605
|
* A 'running' status with a cursor means the process was killed mid-sync.
|
|
606
|
+
*
|
|
607
|
+
* Handles two cursor formats:
|
|
608
|
+
* - Numeric: compared as bigint for correct ordering
|
|
609
|
+
* - Composite cursors: compared as strings with COLLATE "C"
|
|
542
610
|
*/
|
|
543
611
|
async getLastCompletedCursor(accountId, object) {
|
|
544
612
|
const result = await this.query(
|
|
545
|
-
`SELECT
|
|
613
|
+
`SELECT CASE
|
|
614
|
+
WHEN BOOL_OR(o.cursor !~ '^\\d+$') THEN MAX(o.cursor COLLATE "C")
|
|
615
|
+
ELSE MAX(CASE WHEN o.cursor ~ '^\\d+$' THEN o.cursor::bigint END)::text
|
|
616
|
+
END as cursor
|
|
546
617
|
FROM "${this.config.schema}"."_sync_obj_runs" o
|
|
547
618
|
WHERE o."_account_id" = $1
|
|
548
619
|
AND o.object = $2
|
|
@@ -827,6 +898,269 @@ function hashApiKey(apiKey) {
|
|
|
827
898
|
return createHash("sha256").update(apiKey).digest("hex");
|
|
828
899
|
}
|
|
829
900
|
|
|
901
|
+
// src/sigma/sigmaApi.ts
|
|
902
|
+
import Papa from "papaparse";
|
|
903
|
+
import Stripe2 from "stripe";
|
|
904
|
+
var STRIPE_FILES_BASE = "https://files.stripe.com/v1";
|
|
905
|
+
function sleep2(ms) {
|
|
906
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
907
|
+
}
|
|
908
|
+
function parseCsvObjects(csv) {
|
|
909
|
+
const input = csv.replace(/^\uFEFF/, "");
|
|
910
|
+
const parsed = Papa.parse(input, {
|
|
911
|
+
header: true,
|
|
912
|
+
skipEmptyLines: "greedy"
|
|
913
|
+
});
|
|
914
|
+
if (parsed.errors.length > 0) {
|
|
915
|
+
throw new Error(`Failed to parse Sigma CSV: ${parsed.errors[0]?.message ?? "unknown error"}`);
|
|
916
|
+
}
|
|
917
|
+
return parsed.data.filter((row) => row && Object.keys(row).length > 0).map(
|
|
918
|
+
(row) => Object.fromEntries(
|
|
919
|
+
Object.entries(row).map(([k, v]) => [k, v == null || v === "" ? null : String(v)])
|
|
920
|
+
)
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
function normalizeSigmaTimestampToIso(value) {
|
|
924
|
+
const v = value.trim();
|
|
925
|
+
if (!v) return null;
|
|
926
|
+
const hasExplicitTz = /z$|[+-]\d{2}:?\d{2}$/i.test(v);
|
|
927
|
+
const isoish = v.includes("T") ? v : v.replace(" ", "T");
|
|
928
|
+
const candidate = hasExplicitTz ? isoish : `${isoish}Z`;
|
|
929
|
+
const d = new Date(candidate);
|
|
930
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
931
|
+
return d.toISOString();
|
|
932
|
+
}
|
|
933
|
+
async function fetchStripeText(url, apiKey, options) {
|
|
934
|
+
const res = await fetch(url, {
|
|
935
|
+
...options,
|
|
936
|
+
headers: {
|
|
937
|
+
...options.headers ?? {},
|
|
938
|
+
Authorization: `Bearer ${apiKey}`
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
const text = await res.text();
|
|
942
|
+
if (!res.ok) {
|
|
943
|
+
throw new Error(`Sigma file download error (${res.status}) for ${url}: ${text}`);
|
|
944
|
+
}
|
|
945
|
+
return text;
|
|
946
|
+
}
|
|
947
|
+
async function runSigmaQueryAndDownloadCsv(params) {
|
|
948
|
+
const pollTimeoutMs = params.pollTimeoutMs ?? 5 * 60 * 1e3;
|
|
949
|
+
const pollIntervalMs = params.pollIntervalMs ?? 2e3;
|
|
950
|
+
const stripe = new Stripe2(params.apiKey);
|
|
951
|
+
const created = await stripe.rawRequest("POST", "/v1/sigma/query_runs", {
|
|
952
|
+
sql: params.sql
|
|
953
|
+
});
|
|
954
|
+
const queryRunId = created.id;
|
|
955
|
+
const start = Date.now();
|
|
956
|
+
let current = created;
|
|
957
|
+
while (current.status === "running") {
|
|
958
|
+
if (Date.now() - start > pollTimeoutMs) {
|
|
959
|
+
throw new Error(`Sigma query run timed out after ${pollTimeoutMs}ms: ${queryRunId}`);
|
|
960
|
+
}
|
|
961
|
+
await sleep2(pollIntervalMs);
|
|
962
|
+
current = await stripe.rawRequest(
|
|
963
|
+
"GET",
|
|
964
|
+
`/v1/sigma/query_runs/${queryRunId}`,
|
|
965
|
+
{}
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (current.status !== "succeeded") {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`Sigma query run did not succeed (status=${current.status}) id=${queryRunId} error=${JSON.stringify(
|
|
971
|
+
current.error
|
|
972
|
+
)}`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
const fileId = current.result?.file;
|
|
976
|
+
if (!fileId) {
|
|
977
|
+
throw new Error(`Sigma query run succeeded but result.file is missing (id=${queryRunId})`);
|
|
978
|
+
}
|
|
979
|
+
const csv = await fetchStripeText(
|
|
980
|
+
`${STRIPE_FILES_BASE}/files/${fileId}/contents`,
|
|
981
|
+
params.apiKey,
|
|
982
|
+
{ method: "GET" }
|
|
983
|
+
);
|
|
984
|
+
return { queryRunId, fileId, csv };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/sigma/sigmaIngestionConfigs.ts
|
|
988
|
+
var SIGMA_INGESTION_CONFIGS = {
|
|
989
|
+
subscription_item_change_events_v2_beta: {
|
|
990
|
+
sigmaTable: "subscription_item_change_events_v2_beta",
|
|
991
|
+
destinationTable: "subscription_item_change_events_v2_beta",
|
|
992
|
+
pageSize: 1e4,
|
|
993
|
+
cursor: {
|
|
994
|
+
version: 1,
|
|
995
|
+
columns: [
|
|
996
|
+
{ column: "event_timestamp", type: "timestamp" },
|
|
997
|
+
{ column: "event_type", type: "string" },
|
|
998
|
+
{ column: "subscription_item_id", type: "string" }
|
|
999
|
+
]
|
|
1000
|
+
},
|
|
1001
|
+
upsert: {
|
|
1002
|
+
conflictTarget: ["_account_id", "event_timestamp", "event_type", "subscription_item_id"],
|
|
1003
|
+
extraColumns: [
|
|
1004
|
+
{ column: "event_timestamp", pgType: "timestamptz", entryKey: "event_timestamp" },
|
|
1005
|
+
{ column: "event_type", pgType: "text", entryKey: "event_type" },
|
|
1006
|
+
{ column: "subscription_item_id", pgType: "text", entryKey: "subscription_item_id" }
|
|
1007
|
+
]
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
exchange_rates_from_usd: {
|
|
1011
|
+
sigmaTable: "exchange_rates_from_usd",
|
|
1012
|
+
destinationTable: "exchange_rates_from_usd",
|
|
1013
|
+
pageSize: 1e4,
|
|
1014
|
+
cursor: {
|
|
1015
|
+
version: 1,
|
|
1016
|
+
columns: [
|
|
1017
|
+
{ column: "date", type: "string" },
|
|
1018
|
+
{ column: "sell_currency", type: "string" }
|
|
1019
|
+
]
|
|
1020
|
+
},
|
|
1021
|
+
upsert: {
|
|
1022
|
+
conflictTarget: ["_account_id", "date", "sell_currency"],
|
|
1023
|
+
extraColumns: [
|
|
1024
|
+
{ column: "date", pgType: "date", entryKey: "date" },
|
|
1025
|
+
{ column: "sell_currency", pgType: "text", entryKey: "sell_currency" }
|
|
1026
|
+
]
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// src/sigma/sigmaIngestion.ts
|
|
1032
|
+
var SIGMA_CURSOR_DELIM = "";
|
|
1033
|
+
function escapeSigmaSqlStringLiteral(value) {
|
|
1034
|
+
return value.replace(/'/g, "''");
|
|
1035
|
+
}
|
|
1036
|
+
function formatSigmaTimestampForSqlLiteral(date) {
|
|
1037
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
1038
|
+
}
|
|
1039
|
+
function decodeSigmaCursorValues(spec, cursor) {
|
|
1040
|
+
const prefix = `v${spec.version}${SIGMA_CURSOR_DELIM}`;
|
|
1041
|
+
if (!cursor.startsWith(prefix)) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Unrecognized Sigma cursor format (expected prefix ${JSON.stringify(prefix)}): ${cursor}`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const parts = cursor.split(SIGMA_CURSOR_DELIM);
|
|
1047
|
+
const expected = 1 + spec.columns.length;
|
|
1048
|
+
if (parts.length !== expected) {
|
|
1049
|
+
throw new Error(`Malformed Sigma cursor: expected ${expected} parts, got ${parts.length}`);
|
|
1050
|
+
}
|
|
1051
|
+
return parts.slice(1);
|
|
1052
|
+
}
|
|
1053
|
+
function encodeSigmaCursor(spec, values) {
|
|
1054
|
+
if (values.length !== spec.columns.length) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`Cannot encode Sigma cursor: expected ${spec.columns.length} values, got ${values.length}`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
for (const v of values) {
|
|
1060
|
+
if (v.includes(SIGMA_CURSOR_DELIM)) {
|
|
1061
|
+
throw new Error("Cannot encode Sigma cursor: value contains delimiter character");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return [`v${spec.version}`, ...values].join(SIGMA_CURSOR_DELIM);
|
|
1065
|
+
}
|
|
1066
|
+
function sigmaSqlLiteralForCursorValue(spec, rawValue) {
|
|
1067
|
+
switch (spec.type) {
|
|
1068
|
+
case "timestamp": {
|
|
1069
|
+
const d = new Date(rawValue);
|
|
1070
|
+
if (Number.isNaN(d.getTime())) {
|
|
1071
|
+
throw new Error(`Invalid timestamp cursor value for ${spec.column}: ${rawValue}`);
|
|
1072
|
+
}
|
|
1073
|
+
return `timestamp '${formatSigmaTimestampForSqlLiteral(d)}'`;
|
|
1074
|
+
}
|
|
1075
|
+
case "number": {
|
|
1076
|
+
if (!/^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
1077
|
+
throw new Error(`Invalid numeric cursor value for ${spec.column}: ${rawValue}`);
|
|
1078
|
+
}
|
|
1079
|
+
return rawValue;
|
|
1080
|
+
}
|
|
1081
|
+
case "string":
|
|
1082
|
+
return `'${escapeSigmaSqlStringLiteral(rawValue)}'`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function buildSigmaCursorWhereClause(spec, cursorValues) {
|
|
1086
|
+
if (cursorValues.length !== spec.columns.length) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`Cannot build Sigma cursor predicate: expected ${spec.columns.length} values, got ${cursorValues.length}`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
const cols = spec.columns.map((c) => c.column);
|
|
1092
|
+
const lits = spec.columns.map((c, i) => sigmaSqlLiteralForCursorValue(c, cursorValues[i] ?? ""));
|
|
1093
|
+
const ors = [];
|
|
1094
|
+
for (let i = 0; i < cols.length; i++) {
|
|
1095
|
+
const ands = [];
|
|
1096
|
+
for (let j = 0; j < i; j++) {
|
|
1097
|
+
ands.push(`${cols[j]} = ${lits[j]}`);
|
|
1098
|
+
}
|
|
1099
|
+
ands.push(`${cols[i]} > ${lits[i]}`);
|
|
1100
|
+
ors.push(`(${ands.join(" AND ")})`);
|
|
1101
|
+
}
|
|
1102
|
+
return ors.join(" OR ");
|
|
1103
|
+
}
|
|
1104
|
+
function buildSigmaQuery(config, cursor) {
|
|
1105
|
+
const select = config.select === void 0 || config.select === "*" ? "*" : config.select.join(", ");
|
|
1106
|
+
const whereParts = [];
|
|
1107
|
+
if (config.additionalWhere) {
|
|
1108
|
+
whereParts.push(`(${config.additionalWhere})`);
|
|
1109
|
+
}
|
|
1110
|
+
if (cursor) {
|
|
1111
|
+
const values = decodeSigmaCursorValues(config.cursor, cursor);
|
|
1112
|
+
const predicate = buildSigmaCursorWhereClause(config.cursor, values);
|
|
1113
|
+
whereParts.push(`(${predicate})`);
|
|
1114
|
+
}
|
|
1115
|
+
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
|
|
1116
|
+
const orderBy = config.cursor.columns.map((c) => c.column).join(", ");
|
|
1117
|
+
return [
|
|
1118
|
+
`SELECT ${select} FROM ${config.sigmaTable}`,
|
|
1119
|
+
whereClause,
|
|
1120
|
+
`ORDER BY ${orderBy} ASC`,
|
|
1121
|
+
`LIMIT ${config.pageSize}`
|
|
1122
|
+
].filter(Boolean).join(" ");
|
|
1123
|
+
}
|
|
1124
|
+
function defaultSigmaRowToEntry(config, row) {
|
|
1125
|
+
const out = { ...row };
|
|
1126
|
+
for (const col of config.cursor.columns) {
|
|
1127
|
+
const raw = row[col.column];
|
|
1128
|
+
if (raw == null) {
|
|
1129
|
+
throw new Error(`Sigma row missing required cursor column: ${col.column}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (col.type === "timestamp") {
|
|
1132
|
+
const normalized = normalizeSigmaTimestampToIso(raw);
|
|
1133
|
+
if (!normalized) {
|
|
1134
|
+
throw new Error(`Sigma row has invalid timestamp for ${col.column}: ${raw}`);
|
|
1135
|
+
}
|
|
1136
|
+
out[col.column] = normalized;
|
|
1137
|
+
} else if (col.type === "string") {
|
|
1138
|
+
const v = raw.trim();
|
|
1139
|
+
if (!v) {
|
|
1140
|
+
throw new Error(`Sigma row has empty string for required cursor column: ${col.column}`);
|
|
1141
|
+
}
|
|
1142
|
+
out[col.column] = v;
|
|
1143
|
+
} else {
|
|
1144
|
+
const v = raw.trim();
|
|
1145
|
+
if (!v) {
|
|
1146
|
+
throw new Error(`Sigma row has empty value for required cursor column: ${col.column}`);
|
|
1147
|
+
}
|
|
1148
|
+
out[col.column] = v;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1153
|
+
function sigmaCursorFromEntry(config, entry) {
|
|
1154
|
+
const values = config.cursor.columns.map((c) => {
|
|
1155
|
+
const raw = entry[c.column];
|
|
1156
|
+
if (raw == null) {
|
|
1157
|
+
throw new Error(`Cannot build cursor: entry missing ${c.column}`);
|
|
1158
|
+
}
|
|
1159
|
+
return String(raw);
|
|
1160
|
+
});
|
|
1161
|
+
return encodeSigmaCursor(config.cursor, values);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
830
1164
|
// src/stripeSync.ts
|
|
831
1165
|
function getUniqueIds(entries, key) {
|
|
832
1166
|
const set = new Set(
|
|
@@ -837,7 +1171,7 @@ function getUniqueIds(entries, key) {
|
|
|
837
1171
|
var StripeSync = class {
|
|
838
1172
|
constructor(config) {
|
|
839
1173
|
this.config = config;
|
|
840
|
-
const baseStripe = new
|
|
1174
|
+
const baseStripe = new Stripe3(config.stripeSecretKey, {
|
|
841
1175
|
// https://github.com/stripe/stripe-node#configuration
|
|
842
1176
|
// @ts-ignore
|
|
843
1177
|
apiVersion: config.stripeApiVersion,
|
|
@@ -1238,6 +1572,17 @@ var StripeSync = class {
|
|
|
1238
1572
|
listFn: (p) => this.stripe.checkout.sessions.list(p),
|
|
1239
1573
|
upsertFn: (items, id) => this.upsertCheckoutSessions(items, id),
|
|
1240
1574
|
supportsCreatedFilter: true
|
|
1575
|
+
},
|
|
1576
|
+
// Sigma-backed resources
|
|
1577
|
+
subscription_item_change_events_v2_beta: {
|
|
1578
|
+
order: 18,
|
|
1579
|
+
supportsCreatedFilter: false,
|
|
1580
|
+
sigma: SIGMA_INGESTION_CONFIGS.subscription_item_change_events_v2_beta
|
|
1581
|
+
},
|
|
1582
|
+
exchange_rates_from_usd: {
|
|
1583
|
+
order: 19,
|
|
1584
|
+
supportsCreatedFilter: false,
|
|
1585
|
+
sigma: SIGMA_INGESTION_CONFIGS.exchange_rates_from_usd
|
|
1241
1586
|
}
|
|
1242
1587
|
};
|
|
1243
1588
|
async processEvent(event) {
|
|
@@ -1270,7 +1615,13 @@ var StripeSync = class {
|
|
|
1270
1615
|
* Order is determined by the `order` field in resourceRegistry.
|
|
1271
1616
|
*/
|
|
1272
1617
|
getSupportedSyncObjects() {
|
|
1273
|
-
|
|
1618
|
+
const all = Object.entries(this.resourceRegistry).sort(([, a], [, b]) => a.order - b.order).map(([key]) => key);
|
|
1619
|
+
if (!this.config.enableSigma) {
|
|
1620
|
+
return all.filter(
|
|
1621
|
+
(o) => o !== "subscription_item_change_events_v2_beta" && o !== "exchange_rates_from_usd"
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
return all;
|
|
1274
1625
|
}
|
|
1275
1626
|
// Event handler methods
|
|
1276
1627
|
async handleChargeEvent(event, accountId) {
|
|
@@ -1349,7 +1700,7 @@ var StripeSync = class {
|
|
|
1349
1700
|
);
|
|
1350
1701
|
await this.upsertProducts([product], accountId, this.getSyncTimestamp(event, refetched));
|
|
1351
1702
|
} catch (err) {
|
|
1352
|
-
if (err instanceof
|
|
1703
|
+
if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1353
1704
|
const product = event.data.object;
|
|
1354
1705
|
await this.deleteProduct(product.id);
|
|
1355
1706
|
} else {
|
|
@@ -1369,7 +1720,7 @@ var StripeSync = class {
|
|
|
1369
1720
|
);
|
|
1370
1721
|
await this.upsertPrices([price], accountId, false, this.getSyncTimestamp(event, refetched));
|
|
1371
1722
|
} catch (err) {
|
|
1372
|
-
if (err instanceof
|
|
1723
|
+
if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1373
1724
|
const price = event.data.object;
|
|
1374
1725
|
await this.deletePrice(price.id);
|
|
1375
1726
|
} else {
|
|
@@ -1389,7 +1740,7 @@ var StripeSync = class {
|
|
|
1389
1740
|
);
|
|
1390
1741
|
await this.upsertPlans([plan], accountId, false, this.getSyncTimestamp(event, refetched));
|
|
1391
1742
|
} catch (err) {
|
|
1392
|
-
if (err instanceof
|
|
1743
|
+
if (err instanceof Stripe3.errors.StripeAPIError && err.code === "resource_missing") {
|
|
1393
1744
|
const plan = event.data.object;
|
|
1394
1745
|
await this.deletePlan(plan.id);
|
|
1395
1746
|
} else {
|
|
@@ -1636,10 +1987,10 @@ var StripeSync = class {
|
|
|
1636
1987
|
let cursor = null;
|
|
1637
1988
|
if (!params?.created) {
|
|
1638
1989
|
if (objRun?.cursor) {
|
|
1639
|
-
cursor =
|
|
1990
|
+
cursor = objRun.cursor;
|
|
1640
1991
|
} else {
|
|
1641
1992
|
const lastCursor = await this.postgresClient.getLastCompletedCursor(accountId, resourceName);
|
|
1642
|
-
cursor = lastCursor
|
|
1993
|
+
cursor = lastCursor ?? null;
|
|
1643
1994
|
}
|
|
1644
1995
|
}
|
|
1645
1996
|
const result = await this.fetchOnePage(
|
|
@@ -1694,9 +2045,18 @@ var StripeSync = class {
|
|
|
1694
2045
|
throw new Error(`Unsupported object type for processNext: ${object}`);
|
|
1695
2046
|
}
|
|
1696
2047
|
try {
|
|
2048
|
+
if (config.sigma) {
|
|
2049
|
+
return await this.fetchOneSigmaPage(
|
|
2050
|
+
accountId,
|
|
2051
|
+
resourceName,
|
|
2052
|
+
runStartedAt,
|
|
2053
|
+
cursor,
|
|
2054
|
+
config.sigma
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
1697
2057
|
const listParams = { limit };
|
|
1698
2058
|
if (config.supportsCreatedFilter) {
|
|
1699
|
-
const created = params?.created ?? (cursor ? { gte: cursor } : void 0);
|
|
2059
|
+
const created = params?.created ?? (cursor && /^\d+$/.test(cursor) ? { gte: Number.parseInt(cursor, 10) } : void 0);
|
|
1700
2060
|
if (created) {
|
|
1701
2061
|
listParams.created = created;
|
|
1702
2062
|
}
|
|
@@ -1741,6 +2101,97 @@ var StripeSync = class {
|
|
|
1741
2101
|
throw error;
|
|
1742
2102
|
}
|
|
1743
2103
|
}
|
|
2104
|
+
async getSigmaFallbackCursorFromDestination(accountId, sigmaConfig) {
|
|
2105
|
+
const cursorCols = sigmaConfig.cursor.columns;
|
|
2106
|
+
const selectCols = cursorCols.map((c) => `"${c.column}"`).join(", ");
|
|
2107
|
+
const orderBy = cursorCols.map((c) => `"${c.column}" DESC`).join(", ");
|
|
2108
|
+
const result = await this.postgresClient.query(
|
|
2109
|
+
`SELECT ${selectCols}
|
|
2110
|
+
FROM "stripe"."${sigmaConfig.destinationTable}"
|
|
2111
|
+
WHERE "_account_id" = $1
|
|
2112
|
+
ORDER BY ${orderBy}
|
|
2113
|
+
LIMIT 1`,
|
|
2114
|
+
[accountId]
|
|
2115
|
+
);
|
|
2116
|
+
if (result.rows.length === 0) return null;
|
|
2117
|
+
const row = result.rows[0];
|
|
2118
|
+
const entryForCursor = {};
|
|
2119
|
+
for (const c of cursorCols) {
|
|
2120
|
+
const v = row[c.column];
|
|
2121
|
+
if (v == null) {
|
|
2122
|
+
throw new Error(
|
|
2123
|
+
`Sigma fallback cursor query returned null for ${sigmaConfig.destinationTable}.${c.column}`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
if (c.type === "timestamp") {
|
|
2127
|
+
const d = v instanceof Date ? v : new Date(String(v));
|
|
2128
|
+
if (Number.isNaN(d.getTime())) {
|
|
2129
|
+
throw new Error(
|
|
2130
|
+
`Sigma fallback cursor query returned invalid timestamp for ${sigmaConfig.destinationTable}.${c.column}: ${String(
|
|
2131
|
+
v
|
|
2132
|
+
)}`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
entryForCursor[c.column] = d.toISOString();
|
|
2136
|
+
} else {
|
|
2137
|
+
entryForCursor[c.column] = String(v);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
return sigmaCursorFromEntry(sigmaConfig, entryForCursor);
|
|
2141
|
+
}
|
|
2142
|
+
async fetchOneSigmaPage(accountId, resourceName, runStartedAt, cursor, sigmaConfig) {
|
|
2143
|
+
if (!this.config.stripeSecretKey) {
|
|
2144
|
+
throw new Error("Sigma sync requested but stripeSecretKey is not configured.");
|
|
2145
|
+
}
|
|
2146
|
+
if (resourceName !== sigmaConfig.destinationTable) {
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
`Sigma sync config mismatch: resourceName=${resourceName} destinationTable=${sigmaConfig.destinationTable}`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
const effectiveCursor = cursor ?? await this.getSigmaFallbackCursorFromDestination(accountId, sigmaConfig);
|
|
2152
|
+
const sigmaSql = buildSigmaQuery(sigmaConfig, effectiveCursor);
|
|
2153
|
+
this.config.logger?.info(
|
|
2154
|
+
{ object: resourceName, pageSize: sigmaConfig.pageSize, hasCursor: Boolean(effectiveCursor) },
|
|
2155
|
+
"Sigma sync: running query"
|
|
2156
|
+
);
|
|
2157
|
+
const { queryRunId, fileId, csv } = await runSigmaQueryAndDownloadCsv({
|
|
2158
|
+
apiKey: this.config.stripeSecretKey,
|
|
2159
|
+
sql: sigmaSql,
|
|
2160
|
+
logger: this.config.logger
|
|
2161
|
+
});
|
|
2162
|
+
const rows = parseCsvObjects(csv);
|
|
2163
|
+
if (rows.length === 0) {
|
|
2164
|
+
await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
|
|
2165
|
+
return { processed: 0, hasMore: false, runStartedAt };
|
|
2166
|
+
}
|
|
2167
|
+
const entries = rows.map(
|
|
2168
|
+
(row) => defaultSigmaRowToEntry(sigmaConfig, row)
|
|
2169
|
+
);
|
|
2170
|
+
this.config.logger?.info(
|
|
2171
|
+
{ object: resourceName, rows: entries.length, queryRunId, fileId },
|
|
2172
|
+
"Sigma sync: upserting rows"
|
|
2173
|
+
);
|
|
2174
|
+
await this.postgresClient.upsertManyWithTimestampProtection(
|
|
2175
|
+
entries,
|
|
2176
|
+
resourceName,
|
|
2177
|
+
accountId,
|
|
2178
|
+
void 0,
|
|
2179
|
+
sigmaConfig.upsert
|
|
2180
|
+
);
|
|
2181
|
+
await this.postgresClient.incrementObjectProgress(
|
|
2182
|
+
accountId,
|
|
2183
|
+
runStartedAt,
|
|
2184
|
+
resourceName,
|
|
2185
|
+
entries.length
|
|
2186
|
+
);
|
|
2187
|
+
const newCursor = sigmaCursorFromEntry(sigmaConfig, entries[entries.length - 1]);
|
|
2188
|
+
await this.postgresClient.updateObjectCursor(accountId, runStartedAt, resourceName, newCursor);
|
|
2189
|
+
const hasMore = rows.length === sigmaConfig.pageSize;
|
|
2190
|
+
if (!hasMore) {
|
|
2191
|
+
await this.postgresClient.completeObjectSync(accountId, runStartedAt, resourceName);
|
|
2192
|
+
}
|
|
2193
|
+
return { processed: entries.length, hasMore, runStartedAt };
|
|
2194
|
+
}
|
|
1744
2195
|
/**
|
|
1745
2196
|
* Process all pages for all (or specified) object types until complete.
|
|
1746
2197
|
*
|
|
@@ -1869,6 +2320,12 @@ var StripeSync = class {
|
|
|
1869
2320
|
case "checkout_sessions":
|
|
1870
2321
|
results.checkoutSessions = result;
|
|
1871
2322
|
break;
|
|
2323
|
+
case "subscription_item_change_events_v2_beta":
|
|
2324
|
+
results.subscriptionItemChangeEventsV2Beta = result;
|
|
2325
|
+
break;
|
|
2326
|
+
case "exchange_rates_from_usd":
|
|
2327
|
+
results.exchangeRatesFromUsd = result;
|
|
2328
|
+
break;
|
|
1872
2329
|
}
|
|
1873
2330
|
}
|
|
1874
2331
|
}
|
|
@@ -1894,30 +2351,41 @@ var StripeSync = class {
|
|
|
1894
2351
|
const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
|
|
1895
2352
|
this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
|
|
1896
2353
|
let synced = 0;
|
|
1897
|
-
|
|
2354
|
+
const chunkSize = this.config.maxConcurrentCustomers ?? 10;
|
|
2355
|
+
for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
|
|
1898
2356
|
await Promise.all(
|
|
1899
2357
|
customerIdChunk.map(async (customerId) => {
|
|
1900
2358
|
const CHECKPOINT_SIZE = 100;
|
|
1901
2359
|
let currentBatch = [];
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
)
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
currentBatch.length
|
|
1919
|
-
|
|
1920
|
-
|
|
2360
|
+
let hasMore = true;
|
|
2361
|
+
let startingAfter = void 0;
|
|
2362
|
+
while (hasMore) {
|
|
2363
|
+
const response = await this.stripe.paymentMethods.list({
|
|
2364
|
+
limit: 100,
|
|
2365
|
+
customer: customerId,
|
|
2366
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
2367
|
+
});
|
|
2368
|
+
for (const item of response.data) {
|
|
2369
|
+
currentBatch.push(item);
|
|
2370
|
+
if (currentBatch.length >= CHECKPOINT_SIZE) {
|
|
2371
|
+
await this.upsertPaymentMethods(
|
|
2372
|
+
currentBatch,
|
|
2373
|
+
accountId,
|
|
2374
|
+
syncParams?.backfillRelatedEntities
|
|
2375
|
+
);
|
|
2376
|
+
synced += currentBatch.length;
|
|
2377
|
+
await this.postgresClient.incrementObjectProgress(
|
|
2378
|
+
accountId,
|
|
2379
|
+
runStartedAt,
|
|
2380
|
+
resourceName,
|
|
2381
|
+
currentBatch.length
|
|
2382
|
+
);
|
|
2383
|
+
currentBatch = [];
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
hasMore = response.has_more;
|
|
2387
|
+
if (response.data.length > 0) {
|
|
2388
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
1921
2389
|
}
|
|
1922
2390
|
}
|
|
1923
2391
|
if (currentBatch.length > 0) {
|
|
@@ -1961,7 +2429,7 @@ var StripeSync = class {
|
|
|
1961
2429
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
1962
2430
|
}
|
|
1963
2431
|
return this.fetchAndUpsert(
|
|
1964
|
-
() => this.stripe.products.list(params),
|
|
2432
|
+
(pagination) => this.stripe.products.list({ ...params, ...pagination }),
|
|
1965
2433
|
(products) => this.upsertProducts(products, accountId),
|
|
1966
2434
|
accountId,
|
|
1967
2435
|
"products",
|
|
@@ -1981,7 +2449,7 @@ var StripeSync = class {
|
|
|
1981
2449
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
1982
2450
|
}
|
|
1983
2451
|
return this.fetchAndUpsert(
|
|
1984
|
-
() => this.stripe.prices.list(params),
|
|
2452
|
+
(pagination) => this.stripe.prices.list({ ...params, ...pagination }),
|
|
1985
2453
|
(prices) => this.upsertPrices(prices, accountId, syncParams?.backfillRelatedEntities),
|
|
1986
2454
|
accountId,
|
|
1987
2455
|
"prices",
|
|
@@ -2001,7 +2469,7 @@ var StripeSync = class {
|
|
|
2001
2469
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2002
2470
|
}
|
|
2003
2471
|
return this.fetchAndUpsert(
|
|
2004
|
-
() => this.stripe.plans.list(params),
|
|
2472
|
+
(pagination) => this.stripe.plans.list({ ...params, ...pagination }),
|
|
2005
2473
|
(plans) => this.upsertPlans(plans, accountId, syncParams?.backfillRelatedEntities),
|
|
2006
2474
|
accountId,
|
|
2007
2475
|
"plans",
|
|
@@ -2021,7 +2489,7 @@ var StripeSync = class {
|
|
|
2021
2489
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2022
2490
|
}
|
|
2023
2491
|
return this.fetchAndUpsert(
|
|
2024
|
-
() => this.stripe.customers.list(params),
|
|
2492
|
+
(pagination) => this.stripe.customers.list({ ...params, ...pagination }),
|
|
2025
2493
|
// @ts-expect-error
|
|
2026
2494
|
(items) => this.upsertCustomers(items, accountId),
|
|
2027
2495
|
accountId,
|
|
@@ -2042,7 +2510,7 @@ var StripeSync = class {
|
|
|
2042
2510
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2043
2511
|
}
|
|
2044
2512
|
return this.fetchAndUpsert(
|
|
2045
|
-
() => this.stripe.subscriptions.list(params),
|
|
2513
|
+
(pagination) => this.stripe.subscriptions.list({ ...params, ...pagination }),
|
|
2046
2514
|
(items) => this.upsertSubscriptions(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2047
2515
|
accountId,
|
|
2048
2516
|
"subscriptions",
|
|
@@ -2065,7 +2533,7 @@ var StripeSync = class {
|
|
|
2065
2533
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2066
2534
|
}
|
|
2067
2535
|
return this.fetchAndUpsert(
|
|
2068
|
-
() => this.stripe.subscriptionSchedules.list(params),
|
|
2536
|
+
(pagination) => this.stripe.subscriptionSchedules.list({ ...params, ...pagination }),
|
|
2069
2537
|
(items) => this.upsertSubscriptionSchedules(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2070
2538
|
accountId,
|
|
2071
2539
|
"subscription_schedules",
|
|
@@ -2086,7 +2554,7 @@ var StripeSync = class {
|
|
|
2086
2554
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2087
2555
|
}
|
|
2088
2556
|
return this.fetchAndUpsert(
|
|
2089
|
-
() => this.stripe.invoices.list(params),
|
|
2557
|
+
(pagination) => this.stripe.invoices.list({ ...params, ...pagination }),
|
|
2090
2558
|
(items) => this.upsertInvoices(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2091
2559
|
accountId,
|
|
2092
2560
|
"invoices",
|
|
@@ -2106,7 +2574,7 @@ var StripeSync = class {
|
|
|
2106
2574
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2107
2575
|
}
|
|
2108
2576
|
return this.fetchAndUpsert(
|
|
2109
|
-
() => this.stripe.charges.list(params),
|
|
2577
|
+
(pagination) => this.stripe.charges.list({ ...params, ...pagination }),
|
|
2110
2578
|
(items) => this.upsertCharges(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2111
2579
|
accountId,
|
|
2112
2580
|
"charges",
|
|
@@ -2126,7 +2594,7 @@ var StripeSync = class {
|
|
|
2126
2594
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2127
2595
|
}
|
|
2128
2596
|
return this.fetchAndUpsert(
|
|
2129
|
-
() => this.stripe.setupIntents.list(params),
|
|
2597
|
+
(pagination) => this.stripe.setupIntents.list({ ...params, ...pagination }),
|
|
2130
2598
|
(items) => this.upsertSetupIntents(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2131
2599
|
accountId,
|
|
2132
2600
|
"setup_intents",
|
|
@@ -2149,7 +2617,7 @@ var StripeSync = class {
|
|
|
2149
2617
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2150
2618
|
}
|
|
2151
2619
|
return this.fetchAndUpsert(
|
|
2152
|
-
() => this.stripe.paymentIntents.list(params),
|
|
2620
|
+
(pagination) => this.stripe.paymentIntents.list({ ...params, ...pagination }),
|
|
2153
2621
|
(items) => this.upsertPaymentIntents(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2154
2622
|
accountId,
|
|
2155
2623
|
"payment_intents",
|
|
@@ -2164,7 +2632,7 @@ var StripeSync = class {
|
|
|
2164
2632
|
const accountId = await this.getAccountId();
|
|
2165
2633
|
const params = { limit: 100 };
|
|
2166
2634
|
return this.fetchAndUpsert(
|
|
2167
|
-
() => this.stripe.taxIds.list(params),
|
|
2635
|
+
(pagination) => this.stripe.taxIds.list({ ...params, ...pagination }),
|
|
2168
2636
|
(items) => this.upsertTaxIds(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2169
2637
|
accountId,
|
|
2170
2638
|
"tax_ids",
|
|
@@ -2185,30 +2653,41 @@ var StripeSync = class {
|
|
|
2185
2653
|
const customerIds = await this.postgresClient.query(prepared.text, prepared.values).then(({ rows }) => rows.map((it) => it.id));
|
|
2186
2654
|
this.config.logger?.info(`Getting payment methods for ${customerIds.length} customers`);
|
|
2187
2655
|
let synced = 0;
|
|
2188
|
-
|
|
2656
|
+
const chunkSize = this.config.maxConcurrentCustomers ?? 10;
|
|
2657
|
+
for (const customerIdChunk of chunkArray(customerIds, chunkSize)) {
|
|
2189
2658
|
await Promise.all(
|
|
2190
2659
|
customerIdChunk.map(async (customerId) => {
|
|
2191
2660
|
const CHECKPOINT_SIZE = 100;
|
|
2192
2661
|
let currentBatch = [];
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
)
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
currentBatch.length
|
|
2210
|
-
|
|
2211
|
-
|
|
2662
|
+
let hasMore = true;
|
|
2663
|
+
let startingAfter = void 0;
|
|
2664
|
+
while (hasMore) {
|
|
2665
|
+
const response = await this.stripe.paymentMethods.list({
|
|
2666
|
+
limit: 100,
|
|
2667
|
+
customer: customerId,
|
|
2668
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
2669
|
+
});
|
|
2670
|
+
for (const item of response.data) {
|
|
2671
|
+
currentBatch.push(item);
|
|
2672
|
+
if (currentBatch.length >= CHECKPOINT_SIZE) {
|
|
2673
|
+
await this.upsertPaymentMethods(
|
|
2674
|
+
currentBatch,
|
|
2675
|
+
accountId,
|
|
2676
|
+
syncParams?.backfillRelatedEntities
|
|
2677
|
+
);
|
|
2678
|
+
synced += currentBatch.length;
|
|
2679
|
+
await this.postgresClient.incrementObjectProgress(
|
|
2680
|
+
accountId,
|
|
2681
|
+
runStartedAt,
|
|
2682
|
+
"payment_methods",
|
|
2683
|
+
currentBatch.length
|
|
2684
|
+
);
|
|
2685
|
+
currentBatch = [];
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
hasMore = response.has_more;
|
|
2689
|
+
if (response.data.length > 0) {
|
|
2690
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
2212
2691
|
}
|
|
2213
2692
|
}
|
|
2214
2693
|
if (currentBatch.length > 0) {
|
|
@@ -2245,7 +2724,7 @@ var StripeSync = class {
|
|
|
2245
2724
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2246
2725
|
}
|
|
2247
2726
|
return this.fetchAndUpsert(
|
|
2248
|
-
() => this.stripe.disputes.list(params),
|
|
2727
|
+
(pagination) => this.stripe.disputes.list({ ...params, ...pagination }),
|
|
2249
2728
|
(items) => this.upsertDisputes(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2250
2729
|
accountId,
|
|
2251
2730
|
"disputes",
|
|
@@ -2268,7 +2747,7 @@ var StripeSync = class {
|
|
|
2268
2747
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2269
2748
|
}
|
|
2270
2749
|
return this.fetchAndUpsert(
|
|
2271
|
-
() => this.stripe.radar.earlyFraudWarnings.list(params),
|
|
2750
|
+
(pagination) => this.stripe.radar.earlyFraudWarnings.list({ ...params, ...pagination }),
|
|
2272
2751
|
(items) => this.upsertEarlyFraudWarning(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2273
2752
|
accountId,
|
|
2274
2753
|
"early_fraud_warnings",
|
|
@@ -2289,7 +2768,7 @@ var StripeSync = class {
|
|
|
2289
2768
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2290
2769
|
}
|
|
2291
2770
|
return this.fetchAndUpsert(
|
|
2292
|
-
() => this.stripe.refunds.list(params),
|
|
2771
|
+
(pagination) => this.stripe.refunds.list({ ...params, ...pagination }),
|
|
2293
2772
|
(items) => this.upsertRefunds(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2294
2773
|
accountId,
|
|
2295
2774
|
"refunds",
|
|
@@ -2309,7 +2788,7 @@ var StripeSync = class {
|
|
|
2309
2788
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2310
2789
|
}
|
|
2311
2790
|
return this.fetchAndUpsert(
|
|
2312
|
-
() => this.stripe.creditNotes.list(params),
|
|
2791
|
+
(pagination) => this.stripe.creditNotes.list({ ...params, ...pagination }),
|
|
2313
2792
|
(creditNotes) => this.upsertCreditNotes(creditNotes, accountId),
|
|
2314
2793
|
accountId,
|
|
2315
2794
|
"credit_notes",
|
|
@@ -2371,7 +2850,7 @@ var StripeSync = class {
|
|
|
2371
2850
|
this.config.logger?.info(`Incremental sync from cursor: ${cursor}`);
|
|
2372
2851
|
}
|
|
2373
2852
|
return this.fetchAndUpsert(
|
|
2374
|
-
() => this.stripe.checkout.sessions.list(params),
|
|
2853
|
+
(pagination) => this.stripe.checkout.sessions.list({ ...params, ...pagination }),
|
|
2375
2854
|
(items) => this.upsertCheckoutSessions(items, accountId, syncParams?.backfillRelatedEntities),
|
|
2376
2855
|
accountId,
|
|
2377
2856
|
"checkout_sessions",
|
|
@@ -2425,31 +2904,42 @@ var StripeSync = class {
|
|
|
2425
2904
|
try {
|
|
2426
2905
|
this.config.logger?.info("Fetching items to sync from Stripe");
|
|
2427
2906
|
try {
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
currentBatch
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
...currentBatch.map((i) => i.created || 0)
|
|
2442
|
-
);
|
|
2443
|
-
if (maxCreated > 0) {
|
|
2444
|
-
await this.postgresClient.updateObjectCursor(
|
|
2907
|
+
let hasMore = true;
|
|
2908
|
+
let startingAfter = void 0;
|
|
2909
|
+
while (hasMore) {
|
|
2910
|
+
const response = await fetch2(
|
|
2911
|
+
startingAfter ? { starting_after: startingAfter } : void 0
|
|
2912
|
+
);
|
|
2913
|
+
for (const item of response.data) {
|
|
2914
|
+
currentBatch.push(item);
|
|
2915
|
+
if (currentBatch.length >= CHECKPOINT_SIZE) {
|
|
2916
|
+
this.config.logger?.info(`Upserting batch of ${currentBatch.length} items`);
|
|
2917
|
+
await upsert(currentBatch, accountId);
|
|
2918
|
+
totalSynced += currentBatch.length;
|
|
2919
|
+
await this.postgresClient.incrementObjectProgress(
|
|
2445
2920
|
accountId,
|
|
2446
2921
|
runStartedAt,
|
|
2447
2922
|
resourceName,
|
|
2448
|
-
|
|
2923
|
+
currentBatch.length
|
|
2924
|
+
);
|
|
2925
|
+
const maxCreated = Math.max(
|
|
2926
|
+
...currentBatch.map((i) => i.created || 0)
|
|
2449
2927
|
);
|
|
2450
|
-
|
|
2928
|
+
if (maxCreated > 0) {
|
|
2929
|
+
await this.postgresClient.updateObjectCursor(
|
|
2930
|
+
accountId,
|
|
2931
|
+
runStartedAt,
|
|
2932
|
+
resourceName,
|
|
2933
|
+
String(maxCreated)
|
|
2934
|
+
);
|
|
2935
|
+
this.config.logger?.info(`Checkpoint: cursor updated to ${maxCreated}`);
|
|
2936
|
+
}
|
|
2937
|
+
currentBatch = [];
|
|
2451
2938
|
}
|
|
2452
|
-
|
|
2939
|
+
}
|
|
2940
|
+
hasMore = response.has_more;
|
|
2941
|
+
if (response.data.length > 0) {
|
|
2942
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
2453
2943
|
}
|
|
2454
2944
|
}
|
|
2455
2945
|
if (currentBatch.length > 0) {
|
|
@@ -2817,10 +3307,18 @@ var StripeSync = class {
|
|
|
2817
3307
|
async fillCheckoutSessionsLineItems(checkoutSessionIds, accountId, syncTimestamp) {
|
|
2818
3308
|
for (const checkoutSessionId of checkoutSessionIds) {
|
|
2819
3309
|
const lineItemResponses = [];
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
3310
|
+
let hasMore = true;
|
|
3311
|
+
let startingAfter = void 0;
|
|
3312
|
+
while (hasMore) {
|
|
3313
|
+
const response = await this.stripe.checkout.sessions.listLineItems(checkoutSessionId, {
|
|
3314
|
+
limit: 100,
|
|
3315
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
3316
|
+
});
|
|
3317
|
+
lineItemResponses.push(...response.data);
|
|
3318
|
+
hasMore = response.has_more;
|
|
3319
|
+
if (response.data.length > 0) {
|
|
3320
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
3321
|
+
}
|
|
2824
3322
|
}
|
|
2825
3323
|
await this.upsertCheckoutSessionLineItems(
|
|
2826
3324
|
lineItemResponses,
|
|
@@ -3134,14 +3632,25 @@ var StripeSync = class {
|
|
|
3134
3632
|
};
|
|
3135
3633
|
/**
|
|
3136
3634
|
* Stripe only sends the first 10 entries by default, the option will actively fetch all entries.
|
|
3635
|
+
* Uses manual pagination - each fetch() gets automatic retry protection.
|
|
3137
3636
|
*/
|
|
3138
3637
|
async expandEntity(entities, property, listFn) {
|
|
3139
3638
|
if (!this.config.autoExpandLists) return;
|
|
3140
3639
|
for (const entity of entities) {
|
|
3141
3640
|
if (entity[property]?.has_more) {
|
|
3142
3641
|
const allData = [];
|
|
3143
|
-
|
|
3144
|
-
|
|
3642
|
+
let hasMore = true;
|
|
3643
|
+
let startingAfter = void 0;
|
|
3644
|
+
while (hasMore) {
|
|
3645
|
+
const response = await listFn(
|
|
3646
|
+
entity.id,
|
|
3647
|
+
startingAfter ? { starting_after: startingAfter } : void 0
|
|
3648
|
+
);
|
|
3649
|
+
allData.push(...response.data);
|
|
3650
|
+
hasMore = response.has_more;
|
|
3651
|
+
if (response.data.length > 0) {
|
|
3652
|
+
startingAfter = response.data[response.data.length - 1].id;
|
|
3653
|
+
}
|
|
3145
3654
|
}
|
|
3146
3655
|
entity[property] = {
|
|
3147
3656
|
...entity[property],
|
|
@@ -3266,7 +3775,9 @@ async function runMigrations(config) {
|
|
|
3266
3775
|
import WebSocket from "ws";
|
|
3267
3776
|
var CLI_VERSION = "1.33.0";
|
|
3268
3777
|
var PONG_WAIT = 10 * 1e3;
|
|
3269
|
-
var PING_PERIOD = PONG_WAIT *
|
|
3778
|
+
var PING_PERIOD = PONG_WAIT * 9 / 10;
|
|
3779
|
+
var CONNECT_ATTEMPT_WAIT = 10 * 1e3;
|
|
3780
|
+
var DEFAULT_RECONNECT_INTERVAL = 60 * 1e3;
|
|
3270
3781
|
function getClientUserAgent() {
|
|
3271
3782
|
return JSON.stringify({
|
|
3272
3783
|
name: "stripe-cli",
|
|
@@ -3295,129 +3806,215 @@ async function createCliSession(stripeApiKey) {
|
|
|
3295
3806
|
}
|
|
3296
3807
|
return await response.json();
|
|
3297
3808
|
}
|
|
3809
|
+
function sleep3(ms) {
|
|
3810
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3811
|
+
}
|
|
3298
3812
|
async function createStripeWebSocketClient(options) {
|
|
3299
3813
|
const { stripeApiKey, onEvent, onReady, onError, onClose } = options;
|
|
3300
3814
|
const session = await createCliSession(stripeApiKey);
|
|
3815
|
+
const reconnectInterval = session.reconnect_delay ? session.reconnect_delay * 1e3 : DEFAULT_RECONNECT_INTERVAL;
|
|
3301
3816
|
let ws = null;
|
|
3302
3817
|
let pingInterval = null;
|
|
3818
|
+
let reconnectTimer = null;
|
|
3303
3819
|
let connected = false;
|
|
3304
|
-
let
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3820
|
+
let shouldRun = true;
|
|
3821
|
+
let lastPongReceived = Date.now();
|
|
3822
|
+
let notifyCloseResolve = null;
|
|
3823
|
+
let stopResolve = null;
|
|
3824
|
+
function cleanupConnection() {
|
|
3825
|
+
if (pingInterval) {
|
|
3826
|
+
clearInterval(pingInterval);
|
|
3827
|
+
pingInterval = null;
|
|
3828
|
+
}
|
|
3829
|
+
if (reconnectTimer) {
|
|
3830
|
+
clearTimeout(reconnectTimer);
|
|
3831
|
+
reconnectTimer = null;
|
|
3832
|
+
}
|
|
3833
|
+
if (ws) {
|
|
3834
|
+
ws.removeAllListeners();
|
|
3835
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
3836
|
+
ws.close(1e3, "Resetting connection");
|
|
3837
|
+
}
|
|
3838
|
+
ws = null;
|
|
3839
|
+
}
|
|
3840
|
+
connected = false;
|
|
3841
|
+
}
|
|
3842
|
+
function setupWebSocket() {
|
|
3843
|
+
return new Promise((resolve, reject) => {
|
|
3844
|
+
lastPongReceived = Date.now();
|
|
3845
|
+
const wsUrl = `${session.websocket_url}?websocket_feature=${encodeURIComponent(session.websocket_authorized_feature)}`;
|
|
3846
|
+
ws = new WebSocket(wsUrl, {
|
|
3847
|
+
headers: {
|
|
3848
|
+
"Accept-Encoding": "identity",
|
|
3849
|
+
"User-Agent": `Stripe/v1 stripe-cli/${CLI_VERSION}`,
|
|
3850
|
+
"X-Stripe-Client-User-Agent": getClientUserAgent(),
|
|
3851
|
+
"Websocket-Id": session.websocket_id
|
|
3322
3852
|
}
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3853
|
+
});
|
|
3854
|
+
const connectionTimeout = setTimeout(() => {
|
|
3855
|
+
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
|
3856
|
+
ws.terminate();
|
|
3857
|
+
reject(new Error("WebSocket connection timeout"));
|
|
3858
|
+
}
|
|
3859
|
+
}, CONNECT_ATTEMPT_WAIT);
|
|
3860
|
+
ws.on("pong", () => {
|
|
3861
|
+
lastPongReceived = Date.now();
|
|
3862
|
+
});
|
|
3863
|
+
ws.on("open", () => {
|
|
3864
|
+
clearTimeout(connectionTimeout);
|
|
3865
|
+
connected = true;
|
|
3866
|
+
pingInterval = setInterval(() => {
|
|
3867
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3868
|
+
const timeSinceLastPong = Date.now() - lastPongReceived;
|
|
3869
|
+
if (timeSinceLastPong > PONG_WAIT) {
|
|
3870
|
+
if (onError) {
|
|
3871
|
+
onError(new Error(`WebSocket stale: no pong in ${timeSinceLastPong}ms`));
|
|
3872
|
+
}
|
|
3873
|
+
if (notifyCloseResolve) {
|
|
3874
|
+
notifyCloseResolve();
|
|
3875
|
+
notifyCloseResolve = null;
|
|
3876
|
+
}
|
|
3877
|
+
ws.terminate();
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
ws.ping();
|
|
3881
|
+
}
|
|
3882
|
+
}, PING_PERIOD);
|
|
3883
|
+
if (onReady) {
|
|
3884
|
+
onReady(session.secret);
|
|
3339
3885
|
}
|
|
3340
|
-
|
|
3886
|
+
resolve();
|
|
3887
|
+
});
|
|
3888
|
+
ws.on("message", async (data) => {
|
|
3341
3889
|
try {
|
|
3342
|
-
const
|
|
3343
|
-
|
|
3344
|
-
type: "
|
|
3345
|
-
|
|
3890
|
+
const message = JSON.parse(data.toString());
|
|
3891
|
+
const ack = {
|
|
3892
|
+
type: "event_ack",
|
|
3893
|
+
event_id: message.webhook_id,
|
|
3346
3894
|
webhook_conversation_id: message.webhook_conversation_id,
|
|
3347
|
-
|
|
3348
|
-
status: result?.status ?? 200,
|
|
3349
|
-
http_headers: {},
|
|
3350
|
-
body: JSON.stringify({
|
|
3351
|
-
event_type: result?.event_type,
|
|
3352
|
-
event_id: result?.event_id,
|
|
3353
|
-
database_url: result?.databaseUrl,
|
|
3354
|
-
error: result?.error
|
|
3355
|
-
}),
|
|
3356
|
-
request_headers: message.http_headers,
|
|
3357
|
-
request_body: message.event_payload,
|
|
3358
|
-
notification_id: message.webhook_id
|
|
3895
|
+
webhook_id: message.webhook_id
|
|
3359
3896
|
};
|
|
3897
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3898
|
+
ws.send(JSON.stringify(ack));
|
|
3899
|
+
}
|
|
3900
|
+
let response;
|
|
3901
|
+
try {
|
|
3902
|
+
const result = await onEvent(message);
|
|
3903
|
+
response = {
|
|
3904
|
+
type: "webhook_response",
|
|
3905
|
+
webhook_id: message.webhook_id,
|
|
3906
|
+
webhook_conversation_id: message.webhook_conversation_id,
|
|
3907
|
+
forward_url: "stripe-sync-engine",
|
|
3908
|
+
status: result?.status ?? 200,
|
|
3909
|
+
http_headers: {},
|
|
3910
|
+
body: JSON.stringify({
|
|
3911
|
+
event_type: result?.event_type,
|
|
3912
|
+
event_id: result?.event_id,
|
|
3913
|
+
database_url: result?.databaseUrl,
|
|
3914
|
+
error: result?.error
|
|
3915
|
+
}),
|
|
3916
|
+
request_headers: message.http_headers,
|
|
3917
|
+
request_body: message.event_payload,
|
|
3918
|
+
notification_id: message.webhook_id
|
|
3919
|
+
};
|
|
3920
|
+
} catch (err) {
|
|
3921
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3922
|
+
response = {
|
|
3923
|
+
type: "webhook_response",
|
|
3924
|
+
webhook_id: message.webhook_id,
|
|
3925
|
+
webhook_conversation_id: message.webhook_conversation_id,
|
|
3926
|
+
forward_url: "stripe-sync-engine",
|
|
3927
|
+
status: 500,
|
|
3928
|
+
http_headers: {},
|
|
3929
|
+
body: JSON.stringify({ error: errorMessage }),
|
|
3930
|
+
request_headers: message.http_headers,
|
|
3931
|
+
request_body: message.event_payload,
|
|
3932
|
+
notification_id: message.webhook_id
|
|
3933
|
+
};
|
|
3934
|
+
if (onError) {
|
|
3935
|
+
onError(err instanceof Error ? err : new Error(errorMessage));
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3939
|
+
ws.send(JSON.stringify(response));
|
|
3940
|
+
}
|
|
3360
3941
|
} catch (err) {
|
|
3361
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3362
|
-
response = {
|
|
3363
|
-
type: "webhook_response",
|
|
3364
|
-
webhook_id: message.webhook_id,
|
|
3365
|
-
webhook_conversation_id: message.webhook_conversation_id,
|
|
3366
|
-
forward_url: "stripe-sync-engine",
|
|
3367
|
-
status: 500,
|
|
3368
|
-
http_headers: {},
|
|
3369
|
-
body: JSON.stringify({ error: errorMessage }),
|
|
3370
|
-
request_headers: message.http_headers,
|
|
3371
|
-
request_body: message.event_payload,
|
|
3372
|
-
notification_id: message.webhook_id
|
|
3373
|
-
};
|
|
3374
3942
|
if (onError) {
|
|
3375
|
-
onError(err instanceof Error ? err : new Error(
|
|
3943
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
3376
3944
|
}
|
|
3377
3945
|
}
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
} catch (err) {
|
|
3946
|
+
});
|
|
3947
|
+
ws.on("error", (error) => {
|
|
3948
|
+
clearTimeout(connectionTimeout);
|
|
3382
3949
|
if (onError) {
|
|
3383
|
-
onError(
|
|
3950
|
+
onError(error);
|
|
3384
3951
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3952
|
+
if (!connected) {
|
|
3953
|
+
reject(error);
|
|
3954
|
+
}
|
|
3955
|
+
});
|
|
3956
|
+
ws.on("close", (code, reason) => {
|
|
3957
|
+
clearTimeout(connectionTimeout);
|
|
3958
|
+
connected = false;
|
|
3959
|
+
if (pingInterval) {
|
|
3960
|
+
clearInterval(pingInterval);
|
|
3961
|
+
pingInterval = null;
|
|
3962
|
+
}
|
|
3963
|
+
if (onClose) {
|
|
3964
|
+
onClose(code, reason.toString());
|
|
3965
|
+
}
|
|
3966
|
+
if (notifyCloseResolve) {
|
|
3967
|
+
notifyCloseResolve();
|
|
3968
|
+
notifyCloseResolve = null;
|
|
3969
|
+
}
|
|
3970
|
+
});
|
|
3391
3971
|
});
|
|
3392
|
-
|
|
3972
|
+
}
|
|
3973
|
+
async function runLoop() {
|
|
3974
|
+
while (shouldRun) {
|
|
3393
3975
|
connected = false;
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3976
|
+
let connectError = null;
|
|
3977
|
+
do {
|
|
3978
|
+
try {
|
|
3979
|
+
await setupWebSocket();
|
|
3980
|
+
connectError = null;
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
connectError = err instanceof Error ? err : new Error(String(err));
|
|
3983
|
+
if (onError) {
|
|
3984
|
+
onError(connectError);
|
|
3985
|
+
}
|
|
3986
|
+
if (shouldRun) {
|
|
3987
|
+
await sleep3(CONNECT_ATTEMPT_WAIT);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
} while (connectError && shouldRun);
|
|
3991
|
+
if (!shouldRun) break;
|
|
3992
|
+
await new Promise((resolve) => {
|
|
3993
|
+
notifyCloseResolve = resolve;
|
|
3994
|
+
stopResolve = resolve;
|
|
3995
|
+
reconnectTimer = setTimeout(() => {
|
|
3996
|
+
cleanupConnection();
|
|
3997
|
+
resolve();
|
|
3998
|
+
}, reconnectInterval);
|
|
3999
|
+
});
|
|
4000
|
+
if (reconnectTimer) {
|
|
4001
|
+
clearTimeout(reconnectTimer);
|
|
4002
|
+
reconnectTimer = null;
|
|
3406
4003
|
}
|
|
3407
|
-
|
|
4004
|
+
notifyCloseResolve = null;
|
|
4005
|
+
stopResolve = null;
|
|
4006
|
+
}
|
|
4007
|
+
cleanupConnection();
|
|
3408
4008
|
}
|
|
3409
|
-
|
|
4009
|
+
runLoop();
|
|
3410
4010
|
return {
|
|
3411
4011
|
close: () => {
|
|
3412
|
-
|
|
3413
|
-
if (
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
}
|
|
3417
|
-
if (ws) {
|
|
3418
|
-
ws.close(1e3, "Connection Done");
|
|
3419
|
-
ws = null;
|
|
4012
|
+
shouldRun = false;
|
|
4013
|
+
if (stopResolve) {
|
|
4014
|
+
stopResolve();
|
|
4015
|
+
stopResolve = null;
|
|
3420
4016
|
}
|
|
4017
|
+
cleanupConnection();
|
|
3421
4018
|
},
|
|
3422
4019
|
isConnected: () => connected
|
|
3423
4020
|
};
|