-
Notifications
You must be signed in to change notification settings - Fork 3
SSE auto-reconnect for axons; AbortSignal for poll-based waits #765
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
Changes from all commits
89a30ea
986bcaf
441938a
025909f
2b0357e
ccb5e9e
d415a9a
0f771e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,58 @@ | ||
| import { Stream } from '../streaming'; | ||
| import { APIPromise, StreamBackedAPIPromise, type APIResponseProps } from '../core'; | ||
|
|
||
| /** | ||
| * Wraps a stream with automatic reconnection on timeout. | ||
| * Returns an {@link APIPromise} so callers can use {@link APIPromise.asResponse} and | ||
| * {@link APIPromise.withResponse} like other streaming endpoints. | ||
| */ | ||
| export async function withStreamAutoReconnect<Item>( | ||
| streamCreator: (offset: number | undefined) => Promise<Stream<Item>>, | ||
| export function withStreamAutoReconnect<Item>( | ||
| streamCreator: (offset: number | undefined) => APIPromise<Stream<Item>>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wtf is an API promise
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's part of stainless's core primitives, it's our SDK’s return type for most HTTP calls |
||
| getOffset: (item: Item) => number | undefined, | ||
| ): Promise<Stream<Item>> { | ||
| let lastOffset: number | undefined = undefined; | ||
| let currentStream = await streamCreator(lastOffset); | ||
| ): StreamBackedAPIPromise<Stream<Item>> { | ||
| let firstRequest: APIPromise<Stream<Item>> | undefined; | ||
| const ensureFirst = () => (firstRequest ??= streamCreator(undefined)); | ||
|
|
||
| async function* createReconnectingIterator(): AsyncIterator<Item> { | ||
| while (true) { | ||
| try { | ||
| for await (const item of currentStream) { | ||
| if (getOffset(item) !== undefined) { | ||
| lastOffset = getOffset(item); | ||
| // Defer the first HTTP request until something awaits this promise (avoids eager | ||
| // connection attempts and unhandled rejections when the caller only attaches later). | ||
| const responsePropsPromise = new Promise<APIResponseProps>((resolve, reject) => { | ||
| queueMicrotask(() => { | ||
| ensureFirst()._getResponseProps().then(resolve, reject); | ||
| }); | ||
| }); | ||
|
|
||
| let dataPromiseMemo: Promise<Stream<Item>> | undefined; | ||
| const getDataPromise = () => { | ||
| if (!dataPromiseMemo) { | ||
| dataPromiseMemo = (async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memo? This all looks a little to complicated, did the OG reconnector not work?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we creating a new one?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. memo = single-flight for the first request and for the constructed reconnecting stream promise, so all APIPromise entry points stay consistent and idempotent |
||
| let lastOffset: number | undefined = undefined; | ||
| let currentStream = await ensureFirst(); | ||
|
|
||
| async function* createReconnectingIterator(): AsyncIterator<Item> { | ||
| while (true) { | ||
| try { | ||
| for await (const item of currentStream) { | ||
| if (getOffset(item) !== undefined) { | ||
| lastOffset = getOffset(item); | ||
| } | ||
| yield item; | ||
| } | ||
| return; // Stream completed normally | ||
| } catch (error) { | ||
| if ((error as any)?.status === 408) { | ||
| currentStream = await streamCreator(lastOffset); | ||
| continue; | ||
| } | ||
| throw error; // Not a timeout, rethrow | ||
| } | ||
| } | ||
| yield item; | ||
| } | ||
| return; // Stream completed normally | ||
| } catch (error) { | ||
| if ((error as any)?.status === 408) { | ||
| // Reconnect with the last known offset | ||
| currentStream = await streamCreator(lastOffset); | ||
| continue; | ||
| } | ||
| throw error; // Not a timeout, rethrow | ||
| } | ||
|
|
||
| return new Stream(createReconnectingIterator, currentStream.controller); | ||
| })(); | ||
| } | ||
| } | ||
| return dataPromiseMemo; | ||
| }; | ||
|
|
||
| return new Stream(createReconnectingIterator, currentStream.controller); | ||
| return new StreamBackedAPIPromise(responsePropsPromise, getDataPromise); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.