Skip to content

Commit 4529f5b

Browse files
committed
adjust migration
1 parent e5f7cbe commit 4529f5b

4 files changed

Lines changed: 91 additions & 23 deletions

File tree

‎src/cli.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BunContext, BunRuntime } from "@effect/platform-bun";
33
import { Effect, Layer } from "effect";
44
import { listen } from "./commands/listen";
55
import { login } from "./commands/login";
6+
import { migrate } from "./commands/migrate";
67
import { update } from "./commands/update";
78
import * as Migration from "./services/migration/migrate";
89
import * as OAuth from "./services/oauth";
@@ -13,7 +14,7 @@ import { VERSION } from "./version";
1314
const mainCommand = Command.make("polar").pipe(
1415
Command.withSubcommands([
1516
login,
16-
// migrate, Disabled until ready for now
17+
migrate,
1718
listen,
1819
update
1920
])

‎src/prompts/migration.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const storePrompt = (provider: LemonSqueezy.LemonSqueezyImpl) =>
3838

3939
return yield* Prompt.select({
4040
message: "Select Store to Migrate",
41-
choices: stores.data.map((store) => ({
41+
choices: stores.map((store) => ({
4242
value: store.id,
4343
title: store.attributes.name,
4444
})),

‎src/services/migration/lemon/provider.ts‎

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import { Context, Data, Effect, Layer, Schema } from "effect";
1313
import { CustomerCreate } from "../../../schemas/Customer";
1414
import { ProductCreate } from "../../../schemas/Product";
15-
import { fetchAllPages } from "../../../utils";
15+
import { fetchAllPages, promiseAllInBatches } from "../../../utils";
1616
import { parseCustomers, parseVariants } from "./transform";
1717

1818
export class LemonSqueezyError extends Data.TaggedError("LemonSqueezyError")<{
@@ -21,7 +21,11 @@ export class LemonSqueezyError extends Data.TaggedError("LemonSqueezyError")<{
2121
}> {}
2222

2323
export interface LemonSqueezyImpl {
24-
stores: () => Effect.Effect<ListStores, LemonSqueezyError, never>;
24+
stores: () => Effect.Effect<
25+
ListStores["data"],
26+
LemonSqueezyError,
27+
never
28+
>;
2529
customers: (
2630
storeId: string
2731
) => Effect.Effect<readonly CustomerCreate[], LemonSqueezyError, never>;
@@ -43,11 +47,16 @@ export const make = (apiKey: string) =>
4347
stores: () =>
4448
Effect.tryPromise({
4549
try: () =>
46-
client
47-
.listStores()
48-
.then(
49-
(response) => response.data ?? ([] as unknown as ListStores)
50-
),
50+
fetchAllPages((pageNumber: number) =>
51+
client
52+
.listStores({
53+
page: { number: pageNumber, size: 50 },
54+
})
55+
.then((response) => ({
56+
data: response.data?.data,
57+
lastPage: response.data?.meta.page.lastPage ?? 1,
58+
}))
59+
),
5160
catch: (error) =>
5261
new LemonSqueezyError({
5362
message: "Failed to list Lemon Squeezy stores",
@@ -93,22 +102,41 @@ export const make = (apiKey: string) =>
93102
})
94103
)
95104
),
96-
products: (_storeId: string) =>
105+
products: (storeId: string) =>
97106
Effect.tryPromise({
98107
try: async () => {
99-
return await fetchAllPages((pageNumber: number) =>
108+
// Fetch all products for this store
109+
const products = await fetchAllPages((pageNumber: number) =>
100110
client
101-
.listVariants({
102-
page: {
103-
number: pageNumber,
104-
size: 50,
105-
},
111+
.listProducts({
112+
filter: { storeId },
113+
page: { number: pageNumber, size: 50 },
106114
})
107115
.then((response) => ({
108116
data: response.data?.data,
109117
lastPage: response.data?.meta.page.lastPage ?? 1,
110118
}))
111119
);
120+
121+
// Fetch variants for each product in batches to respect rate limits
122+
const variantGroups = await promiseAllInBatches(
123+
(product) =>
124+
fetchAllPages((pageNumber: number) =>
125+
client
126+
.listVariants({
127+
filter: { productId: product.id },
128+
page: { number: pageNumber, size: 50 },
129+
})
130+
.then((response) => ({
131+
data: response.data?.data,
132+
lastPage: response.data?.meta.page.lastPage ?? 1,
133+
}))
134+
),
135+
products,
136+
5
137+
);
138+
139+
return variantGroups.flat();
112140
},
113141
catch: (error) =>
114142
new LemonSqueezyError({

‎src/utils.ts‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,61 @@ export async function promiseAllInBatches<A, B>(
3939
return results;
4040
}
4141

42+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
43+
44+
const withRetry = async <T>(
45+
fn: () => Promise<T>,
46+
maxRetries = 5,
47+
baseDelay = 1000
48+
): Promise<T> => {
49+
let lastError: unknown;
50+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
51+
try {
52+
return await fn();
53+
} catch (error) {
54+
lastError = error;
55+
if (attempt < maxRetries) {
56+
const delay = baseDelay * Math.pow(2, attempt);
57+
await sleep(delay);
58+
}
59+
}
60+
}
61+
throw lastError;
62+
};
63+
4264
export const fetchAllPages = async <T>(
4365
task: (pageNumber: number) => Promise<{
4466
data?: T[];
4567
lastPage: number;
46-
}>
68+
}>,
69+
options: { batchSize?: number; delayMs?: number } = {}
4770
): Promise<T[]> => {
48-
const { data, lastPage } = await task(1);
49-
const allItems: T[] = [...(data ?? [])];
71+
const { batchSize = 5, delayMs = 250 } = options;
5072

51-
if (!data) {
73+
const firstPage = await withRetry(() => task(1));
74+
const allItems: T[] = [...(firstPage.data ?? [])];
75+
76+
if (!firstPage.data || firstPage.lastPage <= 1) {
5277
return allItems;
5378
}
5479

55-
const results = await Promise.all(
56-
Array.from({ length: lastPage - 1 }, (_, i) => task(i + 2))
80+
const remainingPages = Array.from(
81+
{ length: firstPage.lastPage - 1 },
82+
(_, i) => i + 2
5783
);
5884

59-
return [...allItems, ...results.flatMap((result) => result.data ?? [])];
85+
for (let i = 0; i < remainingPages.length; i += batchSize) {
86+
const batch = remainingPages.slice(i, i + batchSize);
87+
const results = await Promise.all(
88+
batch.map((pageNumber) => withRetry(() => task(pageNumber)))
89+
);
90+
for (const result of results) {
91+
allItems.push(...(result.data ?? []));
92+
}
93+
if (i + batchSize < remainingPages.length) {
94+
await sleep(delayMs);
95+
}
96+
}
97+
98+
return allItems;
6099
};

0 commit comments

Comments
 (0)