fix abort controller handling#576
Conversation
f2191f3 to
55cf81d
Compare
Signed-off-by: Sander Pick <sanderpick@gmail.com>
55cf81d to
b59a0db
Compare
sanderpick
left a comment
There was a problem hiding this comment.
@joewagner @carsonfarmer here's a draft fix for the polling behavior. Let me know what you think.
| const controller = new AbortController(); | ||
| const signal = controller.signal; | ||
| mport { helpers } from "@tableland/sdk"; | ||
| const controller = helpers.createPollingController(60_000, 1500); // polling timeout and interval |
There was a problem hiding this comment.
Here's a good view of the API. Use a helper method to create a "controller" that is passed to the .all, .run, etc. methods. The user may use this controller to abort the request via its abort method.
| statements: Statement[], | ||
| opts: Signal = {} | ||
| // reads returns an Array with legnth equal to the number of batched statements, | ||
| controller?: PollingController |
There was a problem hiding this comment.
Couple things here:
- This wasn't really an "options" object, it was a specific type of object. I usually think of "options" as an object that mixes different type of options, e.g., color, maxHeight, etc. I changed the param name to reflect.
- The value of
optswasSignal, but this method may call mutable methods that take polling options.
| return await Promise.all( | ||
| statements.map(async (stmt) => await stmt.all<T>(undefined, opts)) | ||
| statements.map( | ||
| async (stmt) => await stmt.all<T>(undefined, controller) |
There was a problem hiding this comment.
I'm guessing this won't work... the timeout would be cancelled after the first result is returned. Sound right? This might need some special handling... ie, create a new controller for each statement (clone of controller) and control each of those with the single user-provider one. I can take a stab if sounds right.
| } | ||
|
|
||
| export type SignalAndInterval = Signal & Interval; | ||
| export type PollingController = Signal & Interval; |
There was a problem hiding this comment.
Changing this to a name that more closely describes what it does. I had to dig into the source code to figure this out when I first saw this.
| abortSignal = controller.signal; | ||
| // return the timeoutId so the caller can cleanup | ||
| timeoutId = setTimeout(function () { | ||
| export function createSignal(): Signal { |
There was a problem hiding this comment.
Helper method for use with read-only APIs
| const timeoutId = setTimeout(function () { | ||
| controller.abort(); | ||
| }, timeout); | ||
| return { |
There was a problem hiding this comment.
This object doubles as an options object for fetch and as a controller for the user to call abort on.
| ): Promise<WaitableTransactionReceipt> { | ||
| if (config.autoWait ?? false) { | ||
| const waited = await receipt.wait(); | ||
| const waited = await receipt.wait(controller); |
There was a problem hiding this comment.
This is the actual fix for the polling handling. wait was being called without any options / controller.
| config: ReadConfig, | ||
| statement: string, | ||
| opts: Signal = {} | ||
| signal?: Signal |
There was a problem hiding this comment.
Renamed. As mentioned above for controller, this isn't really an "options" object.
There was a problem hiding this comment.
We left it as options in case we wanted to add additional opts to it at a later date.
There was a problem hiding this comment.
I see. So you would have unioned Signal with some other type in the future? Are you suggesting I change it back?
There was a problem hiding this comment.
Yes we could have, but naw it is probably fine. Worst case is that we have to change the parameter name in the future. But since it is already an object, it'll be a pretty safe change.
| async first<T = S, K extends keyof T = keyof T>( | ||
| colName?: K, | ||
| opts: SignalAndInterval = {} | ||
| controller?: PollingController |
There was a problem hiding this comment.
Is there any reason this was not optional and was instead an empty object by default. I made this change in a bunch of places.
There was a problem hiding this comment.
Providing a default value makes it effectively optional. If you look at the typescript signature, it includes the ?. That way, you don't have to double check that a) opts is provided, and then b) if a given property on opts is provided. With chained ? this isn't as much of an issue, but it seemed to lead to cleaner code.
There was a problem hiding this comment.
right, but if you look at the type, it doesn't have optional values anymore. you only ever need to do things like controller?.<param>.
There was a problem hiding this comment.
Yeh that's true, with those chained ? it is pretty clean. Ok!
| await this.#waitExec( | ||
| { | ||
| type, | ||
| sql, | ||
| tables, | ||
| }, | ||
| controller | ||
| ); |
There was a problem hiding this comment.
The old opts var was being injected into the statement object, never to be seen again. Pulling the controller out into its own var makes it easier to see what's going on.
|
Related, after poking around in here, wouldn't it be cleaner to use a traditional options object on the statement api that included the Although, maybe this is part of the codebase that has to conform to D1? |
|
Yeh those are D1 APIs, please don't change without ensuring they are backward compatible with 3rd party libs. |
K, do you have a ref to the API that you're conforming to here? I see a mention of D1 on the |
|
Yeh, we should actually have a reference to its types. IIRC there was an issue a while back where we couldn't just do "implements" or "extends" because the D1 types didn't actually match the response you would get in practice. But they come from here: https://github.com/cloudflare/workerd/blob/main/types/defines/d1.d.ts and we can probably use the types from |
|
Ooop, just noticed that all doesn't support colName anymore! So we could probably remove that to simplify the API? |
|
Cool, thanks! I see |
|
@sanderpick This looks like great improvements in general. I agree the current API is very confusing, and turns out broken. |
|
@joewagner Absolutely. Just wanted to get a first take on this. I'll try to get that done today. |
Summary
Fixes abort controller handling which allows a user to abort and configure the polling timeout and interval for SDK methods that poll the gateway for a receipt.
Details
The core of this PR was adding the wiring to make the underlying polling handling aware of the user's abort controller. However, I found the API to be unintuitive and spent some time refactoring how a user goes about providing a abort-able controller with a custom timeout and polling interval. See comments below for details.
NOTE: This would be a breaking API change and therefore would require a major version bump. We probably could make the core fix while not breaking the API, but I don't think there's any need to be bashful about frequently releasing new major versions at this point. What do you guys think?
How it was tested
Updated tests to use the new API.
Checklist: