Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor src/autoPagination.ts #1739

Merged
merged 7 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/StripeMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,10 @@ export function stripeMethod(
callback
);

// Please note `spec.methodType === 'search'` is beta functionality and this
// interface is subject to change/removal at any time.
if (spec.methodType === 'list' || spec.methodType === 'search') {
const autoPaginationMethods = makeAutoPaginationMethods(
this,
args,
spec,
requestPromise
);
Object.assign(requestPromise, autoPaginationMethods);
}
Object.assign(
requestPromise,
makeAutoPaginationMethods(this, args, spec, requestPromise)
);

return requestPromise;
};
Expand Down
225 changes: 138 additions & 87 deletions src/autoPagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,68 @@ import {callbackifyPromiseWithTimeout, getDataFromArgs} from './utils.js';
type PromiseCache = {
currentPromise: Promise<any> | undefined | null;
};
type IterationResult = {
done: boolean;
value?: any;
};
type IterationResult<T> =
| {
done: false;
value: T;
}
| {done: true; value?: undefined};
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
type IterationDoneCallback = () => void;
type IterationItemCallback = (
item: any,
type IterationItemCallback<T> = (
item: T,
next: any
) => void | boolean | Promise<void | boolean>;
type ListResult = {
data: Array<any>;
// eslint-disable-next-line camelcase
has_more: boolean;
};
type AutoPagingEach = (
onItem: IterationItemCallback,
type AutoPagingEach<T> = (
onItem: IterationItemCallback<T>,
onDone?: IterationDoneCallback
) => Promise<void>;

type AutoPagingToArrayOptions = {
limit?: number;
};
type AutoPagingToArray = (
type AutoPagingToArray<T> = (
opts: AutoPagingToArrayOptions,
onDone: IterationDoneCallback
) => Promise<Array<any>>;
) => Promise<Array<T>>;

type AutoPaginationMethods = {
autoPagingEach: AutoPagingEach;
autoPagingToArray: AutoPagingToArray;
next: () => Promise<void>;
type AutoPaginationMethods<T> = {
autoPagingEach: AutoPagingEach<T>;
autoPagingToArray: AutoPagingToArray<T>;
next: () => Promise<IterationResult<T>>;
return: () => void;
};

export function makeAutoPaginationMethods(
self: StripeResourceObject,
requestArgs: RequestArgs,
spec: MethodSpec,
firstPagePromise: Promise<any>
): AutoPaginationMethods {
const promiseCache: PromiseCache = {currentPromise: null};
const reverseIteration = isReverseIteration(requestArgs);
let pagePromise = firstPagePromise;
let i = 0;

// Search and List methods iterate differently.
// Search relies on a `next_page` token and can only iterate in one direction.
// List relies on either an `ending_before` or `starting_after` field with
// an item ID to paginate and is bi-directional.
//
// Please note: spec.methodType === 'search' is beta functionality and is
// subject to change/removal at any time.
let getNextPagePromise: (pageResult: any) => Promise<any>;
if (spec.methodType === 'search') {
getNextPagePromise = (pageResult): Promise<any> => {
if (!pageResult.next_page) {
throw Error(
'Unexpected: Stripe API response does not have a well-formed `next_page` field, but `has_more` was true.'
);
}
return self._makeRequest(requestArgs, spec, {
page: pageResult.next_page,
});
};
} else {
getNextPagePromise = (pageResult): Promise<any> => {
const lastId = getLastId(pageResult, reverseIteration);
return self._makeRequest(requestArgs, spec, {
[reverseIteration ? 'ending_before' : 'starting_after']: lastId,
});
};
interface IStripeIterator<T> {
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
next: () => Promise<IterationResult<T>>;
}
type PageResult<T> = {
data: Array<T>;
has_more: boolean;
next_page: string | null;
};
class StripeIterator<T> implements IStripeIterator<T> {
private i: number;
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
private pagePromise: Promise<PageResult<T>>;
private promiseCache: PromiseCache;
protected requestArgs: RequestArgs;
protected spec: MethodSpec;
protected stripeResource: StripeResourceObject;
constructor(
firstPagePromise: Promise<PageResult<T>>,
requestArgs: RequestArgs,
spec: MethodSpec,
stripeResource: StripeResourceObject
) {
this.i = 0;
this.pagePromise = firstPagePromise;
this.promiseCache = {currentPromise: null};
this.requestArgs = requestArgs;
this.spec = spec;
this.stripeResource = stripeResource;
}

function iterate(
pageResult: ListResult
): IterationResult | Promise<IterationResult> {
iterate(
pageResult: PageResult<T>
): IterationResult<T> | Promise<IterationResult<T>> {
if (
!(
pageResult &&
Expand All @@ -92,39 +78,97 @@ export function makeAutoPaginationMethods(
);
}

if (i < pageResult.data.length) {
const idx = reverseIteration ? pageResult.data.length - 1 - i : i;
const reverseIteration = isReverseIteration(this.requestArgs);
if (this.i < pageResult.data.length) {
const idx = reverseIteration
? pageResult.data.length - 1 - this.i
: this.i;
const value = pageResult.data[idx];
i += 1;
this.i += 1;

return {value, done: false};
} else if (pageResult.has_more) {
// Reset counter, request next page, and recurse.
i = 0;
pagePromise = getNextPagePromise(pageResult);
return pagePromise.then(iterate);
this.i = 0;
this.pagePromise = this.getNextPage(pageResult);
return this.pagePromise.then((pageResult) => this.iterate(pageResult));
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
}
return {value: undefined, done: true};
}

function asyncIteratorNext(): Promise<any> {
return memoizedPromise(promiseCache, (resolve, reject) => {
return pagePromise
.then(iterate)
getNextPage(_pageResult: PageResult<T>): Promise<PageResult<T>> {
throw new Error('Unimplemented');
}

next(): Promise<IterationResult<T>> {
return memoizedPromise(this.promiseCache, (resolve, reject) => {
return this.pagePromise
.then((pageResult) => this.iterate(pageResult))
richardm-stripe marked this conversation as resolved.
Show resolved Hide resolved
.then(resolve)
.catch(reject);
});
}
}

class ListIterator<T extends {id: string}> extends StripeIterator<T> {
getNextPage(pageResult: PageResult<T>): Promise<PageResult<T>> {
const reverseIteration = isReverseIteration(this.requestArgs);
const lastId = getLastId(pageResult, reverseIteration);
return this.stripeResource._makeRequest(this.requestArgs, this.spec, {
[reverseIteration ? 'ending_before' : 'starting_after']: lastId,
});
}
}

class SearchIterator<T> extends StripeIterator<T> {
getNextPage(pageResult: PageResult<T>): Promise<PageResult<T>> {
if (!pageResult.next_page) {
throw Error(
'Unexpected: Stripe API response does not have a well-formed `next_page` field, but `has_more` was true.'
);
}
return this.stripeResource._makeRequest(this.requestArgs, this.spec, {
page: pageResult.next_page,
});
}
}

const autoPagingEach = makeAutoPagingEach(asyncIteratorNext);
export const makeAutoPaginationMethods = <
TMethodSpec extends MethodSpec,
TItem extends {id: string}
>(
stripeResource: StripeResourceObject,
requestArgs: RequestArgs,
spec: TMethodSpec,
firstPagePromise: Promise<PageResult<TItem>>
): AutoPaginationMethods<TItem> | null => {
if (spec.methodType === 'search') {
return makeAutoPaginationMethodsFromIterator(
new SearchIterator(firstPagePromise, requestArgs, spec, stripeResource)
);
}
if (spec.methodType === 'list') {
return makeAutoPaginationMethodsFromIterator(
new ListIterator(firstPagePromise, requestArgs, spec, stripeResource)
);
}
return null;
};

const makeAutoPaginationMethodsFromIterator = <T>(
iterator: IStripeIterator<T>
): AutoPaginationMethods<T> => {
const autoPagingEach = makeAutoPagingEach((...args) =>
iterator.next(...args)
);
const autoPagingToArray = makeAutoPagingToArray(autoPagingEach);

const autoPaginationMethods: AutoPaginationMethods = {
const autoPaginationMethods: AutoPaginationMethods<T> = {
autoPagingEach,
autoPagingToArray,

// Async iterator functions:
next: asyncIteratorNext,
next: () => iterator.next(),
return: (): any => {
// This is required for `break`.
return {};
Expand All @@ -134,7 +178,7 @@ export function makeAutoPaginationMethods(
},
};
return autoPaginationMethods;
}
};

/**
* ----------------
Expand Down Expand Up @@ -174,7 +218,9 @@ function getDoneCallback(args: Array<any>): IterationDoneCallback | null {
* In addition to standard validation, this helper
* coalesces the former forms into the latter form.
*/
function getItemCallback(args: Array<any>): IterationItemCallback | undefined {
function getItemCallback<T>(
args: Array<any>
): IterationItemCallback<T> | undefined {
if (args.length === 0) {
return undefined;
}
Expand Down Expand Up @@ -206,7 +252,10 @@ function getItemCallback(args: Array<any>): IterationItemCallback | undefined {
};
}

function getLastId(listResult: ListResult, reverseIteration: boolean): string {
function getLastId<T extends {id: string}>(
listResult: PageResult<T>,
reverseIteration: boolean
): string {
const lastIdx = reverseIteration ? 0 : listResult.data.length - 1;
const lastItem = listResult.data[lastIdx];
const lastId = lastItem && lastItem.id;
Expand Down Expand Up @@ -237,9 +286,9 @@ function memoizedPromise<T>(
return promiseCache.currentPromise;
}

function makeAutoPagingEach(
asyncIteratorNext: () => Promise<IterationResult>
): AutoPagingEach {
function makeAutoPagingEach<T>(
asyncIteratorNext: () => Promise<IterationResult<T>>
): AutoPagingEach<T> {
return function autoPagingEach(/* onItem?, onDone? */): Promise<void> {
const args = [].slice.call(arguments);
const onItem = getItemCallback(args);
Expand All @@ -254,12 +303,12 @@ function makeAutoPagingEach(
onItem
);
return callbackifyPromiseWithTimeout(autoPagePromise, onDone);
} as AutoPagingEach;
} as AutoPagingEach<T>;
}

function makeAutoPagingToArray(
autoPagingEach: AutoPagingEach
): AutoPagingToArray {
function makeAutoPagingToArray<T>(
autoPagingEach: AutoPagingEach<T>
): AutoPagingToArray<T> {
return function autoPagingToArray(
opts,
onDone: IterationDoneCallback
Expand Down Expand Up @@ -293,12 +342,14 @@ function makeAutoPagingToArray(
};
}

function wrapAsyncIteratorWithCallback(
asyncIteratorNext: () => Promise<IterationResult>,
onItem: IterationItemCallback
function wrapAsyncIteratorWithCallback<T>(
asyncIteratorNext: () => Promise<IterationResult<T>>,
onItem: IterationItemCallback<T>
): Promise<void> {
return new Promise<void>((resolve, reject) => {
function handleIteration(iterResult: IterationResult): Promise<any> | void {
function handleIteration(
iterResult: IterationResult<T>
): Promise<any> | void {
if (iterResult.done) {
resolve();
return;
Expand Down
Loading