Skip to content

Commit 8c23061

Browse files
authored
Merge pull request #36 from tablelandnetwork/joe/fix-polling-opts
fix: abort controller handling
2 parents 379d5d3 + 93d2ade commit 8c23061

23 files changed

Lines changed: 457 additions & 245 deletions

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sdk/API.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,16 +249,16 @@ console.log(transactionHash);
249249

250250
The `Database` may also be run in `autoWait` mode, such that each mutating call will not resolve until it has finalized on the Tableland network. This is useful when working with D1 compatible libraries, or to avoid issues with nonce-reuse etc.
251251

252-
Additionally, all async method calls take an optional `AbortSignal` object, which may be used to cancel or otherwise abort an inflight query. Note that this will only abort queries (including wait status), not the actual mutation transaction itself.
252+
Additionally, all async method calls take an optional `Signal` or `PollingController` object, which may be used to abort an inflight query. Note that this will only abort queries (including wait status), not the actual mutation transaction itself.
253253

254254
```typescript
255-
const controller = new AbortController();
256-
const signal = controller.signal;
255+
import { helpers } from "@tableland/sdk";
256+
const controller = helpers.createPollingController(60_000, 1500); // polling timeout and interval
257257

258258
const stmt = db.prepare("SELECT name, age FROM users WHERE age < ?1");
259259

260260
setTimeout(() => controller.abort(), 10);
261-
const young = await stmt.bind(20).all({ signal });
261+
const young = await stmt.bind(20).all({ controller });
262262
/*
263263
Error: The operation was aborted.
264264
*/

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
},
9292
"dependencies": {
9393
"@async-generators/from-emitter": "^0.3.0",
94-
"@tableland/evm": "^4.3.0",
94+
"@tableland/evm": "^4.4.0",
9595
"@tableland/sqlparser": "^1.3.0",
9696
"ethers": "^5.7.2"
9797
}

packages/sdk/src/database.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { wrapResult } from "./registry/utils.js";
44
import {
55
type Config,
66
type AutoWaitConfig,
7+
type ChainName,
8+
type PollingController,
9+
type Signer,
710
checkWait,
811
extractBaseUrl,
9-
type ChainName,
1012
getBaseUrl,
11-
type Signal,
12-
type Signer,
1313
normalize,
1414
validateTableName,
1515
} from "./helpers/index.js";
@@ -45,7 +45,7 @@ export class Database<D = unknown> {
4545
*/
4646
static readOnly(chainNameOrId: ChainName | number): Database {
4747
console.warn(
48-
"`Database.readOnly()` is a depricated method, use `new Database()`"
48+
"`Database.readOnly()` is a deprecated method, use `new Database()`"
4949
);
5050
const baseUrl = getBaseUrl(chainNameOrId);
5151
return new Database({ baseUrl });
@@ -83,17 +83,17 @@ export class Database<D = unknown> {
8383
* in the sequence fails, then an error is returned for that specific
8484
* statement, and it aborts or rolls back the entire sequence.
8585
* @param statements A set of Statement objects to batch and submit.
86-
* @param opts Additional options to control execution.
86+
* @param controller An optional object used to control receipt polling behavior.
8787
* @returns An array of run results.
8888
*/
8989
// Note: if we want this package to mirror the D1 package in a way that
90-
// enables compatability with packages built to exend D1, then the return type
90+
// enables compatability with packages built to extend D1, then the return type
9191
// here will potentially affect if/how those packages work.
9292
// D1-ORM is a good example: https://github.com/Interactions-as-a-Service/d1-orm/
9393
async batch<T = D>(
9494
statements: Statement[],
95-
opts: Signal = {}
96-
// reads returns an Array with legnth equal to the number of batched statements,
95+
controller?: PollingController
96+
// reads returns an Array with length equal to the number of batched statements,
9797
// everything else a single result wrapped in an Array for backward compatability.
9898
): Promise<Array<Result<T>>> {
9999
try {
@@ -124,7 +124,7 @@ export class Database<D = unknown> {
124124
// and return an Array of the query results.
125125
if (type === "read") {
126126
return await Promise.all(
127-
statements.map(async (stmt) => await stmt.all<T>(undefined, opts))
127+
statements.map(async (stmt) => await stmt.all<T>({ controller }))
128128
);
129129
}
130130

@@ -135,7 +135,8 @@ export class Database<D = unknown> {
135135
await execCreateMany(
136136
this.config,
137137
statements.map((stmt) => stmt.toString())
138-
)
138+
),
139+
controller
139140
);
140141

141142
// TODO: wrapping in an Array is required for back compat, consider changing this for next major
@@ -160,7 +161,8 @@ export class Database<D = unknown> {
160161

161162
const receipt = await checkWait(
162163
this.config,
163-
await execMutateMany(this.config, runnables)
164+
await execMutateMany(this.config, runnables),
165+
controller
164166
);
165167

166168
// TODO: wrapping in an Array is required for back compat, consider changing this for next major
@@ -186,19 +188,19 @@ export class Database<D = unknown> {
186188
* transaction. In the future, more "intelligent" transaction planning,
187189
* splitting, and batching may be used.
188190
* @param statementStrings A set of SQL statement strings separated by semi-colons.
189-
* @param opts Additional options to control execution.
191+
* @param controller An optional object used to control receipt polling behavior.
190192
* @returns A single run result.
191193
*/
192194
async exec<T = D>(
193195
statementStrings: string,
194-
opts: Signal = {}
196+
controller?: PollingController
195197
): Promise<Result<T>> {
196198
// TODO: Note that this method appears to be the wrong return type in practice.
197199
try {
198200
const { statements } = await normalize(statementStrings);
199201
const count = statements.length;
200202
const statement = this.prepare(statementStrings);
201-
const result = await statement.run(opts);
203+
const result = await statement.run({ controller });
202204
// Adds a count property which isn't typed
203205
result.meta.count = count;
204206
return result;
@@ -213,9 +215,9 @@ export class Database<D = unknown> {
213215
/**
214216
* Export a (set of) tables to the SQLite binary format.
215217
* Not implemented yet!
216-
* @param _opts Additional options to control execution.
218+
* @param controller An optional object used to control receipt polling behavior.
217219
*/
218-
async dump(_opts: Signal = {}): Promise<ArrayBuffer> {
220+
async dump(_controller?: PollingController): Promise<ArrayBuffer> {
219221
throw errorWithCause("DUMP_ERROR", new Error("not implemented yet"));
220222
}
221223
}
@@ -248,7 +250,7 @@ async function normalizedToRunnables(
248250
// check if these tables are in the normalized table names
249251
// if so, filter them out (i.e., they are not being mutated)
250252
const filteredTables = norm.tables.filter(
251-
(tableName) => !tableNames.includes(tableName)
253+
(tableName: string) => !tableNames.includes(tableName)
252254
);
253255
// if the filtered tables are greater than 1, then there are two
254256
// tables being mutated in a single statement, which is not allowed

packages/sdk/src/helpers/await.ts

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,130 @@
1+
/**
2+
* A type that can be awaited.
3+
* @property T The type to await.
4+
*/
15
export type Awaitable<T> = T | PromiseLike<T>;
26

7+
/**
8+
* A signal to abort a request.
9+
*/
310
export interface Signal {
4-
signal?: AbortSignal;
11+
/**
12+
* The {@link AbortSignal} to abort a request.
13+
*/
14+
signal: AbortSignal;
15+
/**
16+
* A function to abort a request.
17+
*/
18+
abort: () => void;
519
}
620

21+
/**
22+
* A polling interval to check for results.
23+
*/
724
export interface Interval {
8-
interval?: number;
25+
/**
26+
* The interval period to make new requests, in milliseconds.
27+
*/
28+
interval: number;
29+
/**
30+
* A function to cancel a polling interval.
31+
*/
32+
cancel: () => void;
33+
}
34+
35+
/**
36+
* A polling timeout to abort a request.
37+
*/
38+
export interface Timeout {
39+
/**
40+
* The timeout period in milliseconds.
41+
*/
42+
timeout: number;
943
}
1044

11-
export type SignalAndInterval = Signal & Interval;
45+
/**
46+
* A polling controller with a custom timeout & interval.
47+
*/
48+
export type PollingController = Signal & Interval & Timeout;
1249

50+
/**
51+
* A waitable interface to check for results.
52+
*/
1353
export interface Wait<T = unknown> {
14-
wait: (opts?: SignalAndInterval) => Promise<T>;
54+
/**
55+
* A function to check for results.
56+
* @param controller A {@link PollingController} with the custom timeout & interval.
57+
* @returns
58+
*/
59+
wait: (controller?: PollingController) => Promise<T>;
1560
}
1661

62+
/**
63+
* Results from an an asynchronous function.
64+
*/
1765
export interface AsyncData<T> {
1866
done: boolean;
1967
data?: T;
2068
}
2169

70+
/**
71+
* An asynchronous function to check for results.
72+
*/
2273
export type AsyncFunction<T> = () => Awaitable<AsyncData<T>>;
2374

24-
export function getAbortSignal(
25-
signal?: AbortSignal,
26-
maxTimeout: number = 60_000
27-
): {
28-
signal: AbortSignal;
29-
timeoutId: ReturnType<typeof setTimeout> | undefined;
30-
} {
31-
let abortSignal: AbortSignal;
32-
let timeoutId;
33-
if (signal == null) {
34-
const controller = new AbortController();
35-
abortSignal = controller.signal;
36-
// return the timeoutId so the caller can cleanup
37-
timeoutId = setTimeout(function () {
75+
/**
76+
* Create a signal to abort a request.
77+
* @returns A {@link Signal} to abort a request.
78+
*/
79+
export function createSignal(): Signal {
80+
const controller = new AbortController();
81+
return {
82+
signal: controller.signal,
83+
abort: () => {
3884
controller.abort();
39-
}, maxTimeout);
40-
} else {
41-
abortSignal = signal;
42-
}
43-
return { signal: abortSignal, timeoutId };
85+
},
86+
};
87+
}
88+
89+
/**
90+
* Create a polling controller with a custom timeout & interval.
91+
* @param timeout The timeout period in milliseconds.
92+
* @param interval The interval period to make new requests, in milliseconds.
93+
* @returns A {@link PollingController} with the custom timeout & interval.
94+
*/
95+
export function createPollingController(
96+
timeout: number = 60_000,
97+
pollingInterval: number = 1500
98+
): PollingController {
99+
const controller = new AbortController();
100+
const timeoutId = setTimeout(function () {
101+
controller.abort();
102+
}, timeout);
103+
return {
104+
signal: controller.signal,
105+
abort: () => {
106+
clearTimeout(timeoutId);
107+
controller.abort();
108+
},
109+
interval: pollingInterval,
110+
cancel: () => {
111+
clearTimeout(timeoutId);
112+
},
113+
timeout,
114+
};
44115
}
45116

117+
/**
118+
* Create an asynchronous poller to check for results for a given function.
119+
* @param fn An {@link AsyncFunction} to check for results.
120+
* @param controller A {@link PollingController} with the custom timeout & interval.
121+
* @returns Result from the awaited function's execution or resulting error.
122+
*/
46123
export async function getAsyncPoller<T = unknown>(
47124
fn: AsyncFunction<T>,
48-
interval: number = 1500,
49-
signal?: AbortSignal
125+
controller?: PollingController
50126
): Promise<T> {
51-
// in order to set a timeout other than 10 seconds you need to
52-
// create and pass in an abort signal with a different timeout
53-
const { signal: abortSignal, timeoutId } = getAbortSignal(signal, 10_000);
127+
const control = controller ?? createPollingController();
54128
const checkCondition = (
55129
resolve: (value: T) => void,
56130
reject: (reason?: any) => void
@@ -59,15 +133,15 @@ export async function getAsyncPoller<T = unknown>(
59133
.then((result: AsyncData<T>) => {
60134
if (result.done && result.data != null) {
61135
// We don't want to call `AbortController.abort()` if the call succeeded
62-
clearTimeout(timeoutId);
136+
control.cancel();
63137
return resolve(result.data);
64138
}
65-
if (abortSignal.aborted) {
139+
if (control.signal.aborted) {
66140
// We don't want to call `AbortController.abort()` if the call is already aborted
67-
clearTimeout(timeoutId);
68-
return reject(abortSignal.reason);
141+
control.cancel();
142+
return reject(control.signal.reason);
69143
} else {
70-
setTimeout(checkCondition, interval, resolve, reject);
144+
setTimeout(checkCondition, control.interval, resolve, reject);
71145
}
72146
})
73147
.catch((err) => {

0 commit comments

Comments
 (0)