Skip to content

Commit fcf82e4

Browse files
committed
(sdk) port sander's pr from js-tableland repo
1 parent 47c3302 commit fcf82e4

18 files changed

Lines changed: 202 additions & 194 deletions

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(undefined, controller);
262262
/*
263263
Error: The operation was aborted.
264264
*/

packages/sdk/src/database.ts

Lines changed: 18 additions & 14 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";
@@ -83,7 +83,7 @@ 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
@@ -92,8 +92,8 @@ export class Database<D = unknown> {
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,9 @@ 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(
128+
async (stmt) => await stmt.all<T>(undefined, controller)
129+
)
128130
);
129131
}
130132

@@ -135,7 +137,8 @@ export class Database<D = unknown> {
135137
await execCreateMany(
136138
this.config,
137139
statements.map((stmt) => stmt.toString())
138-
)
140+
),
141+
controller
139142
);
140143

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

161164
const receipt = await checkWait(
162165
this.config,
163-
await execMutateMany(this.config, runnables)
166+
await execMutateMany(this.config, runnables),
167+
controller
164168
);
165169

166170
// TODO: wrapping in an Array is required for back compat, consider changing this for next major
@@ -186,19 +190,19 @@ export class Database<D = unknown> {
186190
* transaction. In the future, more "intelligent" transaction planning,
187191
* splitting, and batching may be used.
188192
* @param statementStrings A set of SQL statement strings separated by semi-colons.
189-
* @param opts Additional options to control execution.
193+
* @param controller An optional object used to control receipt polling behavior.
190194
* @returns A single run result.
191195
*/
192196
async exec<T = D>(
193197
statementStrings: string,
194-
opts: Signal = {}
198+
controller?: PollingController
195199
): Promise<Result<T>> {
196200
// TODO: Note that this method appears to be the wrong return type in practice.
197201
try {
198202
const { statements } = await normalize(statementStrings);
199203
const count = statements.length;
200204
const statement = this.prepare(statementStrings);
201-
const result = await statement.run(opts);
205+
const result = await statement.run(controller);
202206
// Adds a count property which isn't typed
203207
result.meta.count = count;
204208
return result;
@@ -213,9 +217,9 @@ export class Database<D = unknown> {
213217
/**
214218
* Export a (set of) tables to the SQLite binary format.
215219
* Not implemented yet!
216-
* @param _opts Additional options to control execution.
220+
* @param controller An optional object used to control receipt polling behavior.
217221
*/
218-
async dump(_opts: Signal = {}): Promise<ArrayBuffer> {
222+
async dump(_controller?: PollingController): Promise<ArrayBuffer> {
219223
throw errorWithCause("DUMP_ERROR", new Error("not implemented yet"));
220224
}
221225
}

packages/sdk/src/helpers/await.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
export type Awaitable<T> = T | PromiseLike<T>;
22

33
export interface Signal {
4-
signal?: AbortSignal;
4+
signal: AbortSignal;
5+
abort: () => void;
56
}
67

78
export interface Interval {
8-
interval?: number;
9+
interval: number;
10+
cancel: () => void;
911
}
1012

11-
export type SignalAndInterval = Signal & Interval;
13+
export type PollingController = Signal & Interval;
1214

1315
export interface Wait<T = unknown> {
14-
wait: (opts?: SignalAndInterval) => Promise<T>;
16+
wait: (controller?: PollingController) => Promise<T>;
1517
}
1618

1719
export interface AsyncData<T> {
@@ -21,36 +23,41 @@ export interface AsyncData<T> {
2123

2224
export type AsyncFunction<T> = () => Awaitable<AsyncData<T>>;
2325

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 () {
26+
export function createSignal(): Signal {
27+
const controller = new AbortController();
28+
return {
29+
signal: controller.signal,
30+
abort: () => {
3831
controller.abort();
39-
}, maxTimeout);
40-
} else {
41-
abortSignal = signal;
42-
}
43-
return { signal: abortSignal, timeoutId };
32+
},
33+
};
34+
}
35+
36+
export function createPollingController(
37+
timeout: number = 60_000,
38+
pollingInterval: number = 1500
39+
): PollingController {
40+
const controller = new AbortController();
41+
const timeoutId = setTimeout(function () {
42+
controller.abort();
43+
}, timeout);
44+
return {
45+
signal: controller.signal,
46+
abort: () => {
47+
controller.abort();
48+
},
49+
interval: pollingInterval,
50+
cancel: () => {
51+
clearTimeout(timeoutId);
52+
},
53+
};
4454
}
4555

4656
export async function getAsyncPoller<T = unknown>(
4757
fn: AsyncFunction<T>,
48-
interval: number = 1500,
49-
signal?: AbortSignal
58+
controller?: PollingController
5059
): 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);
60+
const control = controller ?? createPollingController();
5461
const checkCondition = (
5562
resolve: (value: T) => void,
5663
reject: (reason?: any) => void
@@ -59,15 +66,15 @@ export async function getAsyncPoller<T = unknown>(
5966
.then((result: AsyncData<T>) => {
6067
if (result.done && result.data != null) {
6168
// We don't want to call `AbortController.abort()` if the call succeeded
62-
clearTimeout(timeoutId);
69+
control.cancel();
6370
return resolve(result.data);
6471
}
65-
if (abortSignal.aborted) {
72+
if (control.signal.aborted) {
6673
// We don't want to call `AbortController.abort()` if the call is already aborted
67-
clearTimeout(timeoutId);
68-
return reject(abortSignal.reason);
74+
control.cancel();
75+
return reject(control.signal.reason);
6976
} else {
70-
setTimeout(checkCondition, interval, resolve, reject);
77+
setTimeout(checkCondition, control.interval, resolve, reject);
7178
}
7279
})
7380
.catch((err) => {

packages/sdk/src/helpers/config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type WaitableTransactionReceipt } from "../registry/utils.js";
2+
import { type PollingController } from "./await.js";
23
import { type FetchConfig } from "../validator/client/index.js";
34
import { type ChainName, getBaseUrl } from "./chains.js";
45
import { type Signer, type ExternalProvider, getSigner } from "./ethers.js";
@@ -27,11 +28,11 @@ export interface AliasesNameMap {
2728
}
2829

2930
export async function checkWait(
30-
config: Config & Partial<AutoWaitConfig>,
31-
receipt: WaitableTransactionReceipt
31+
receipt: WaitableTransactionReceipt,
32+
controller?: PollingController
3233
): Promise<WaitableTransactionReceipt> {
3334
if (config.autoWait ?? false) {
34-
const waited = await receipt.wait();
35+
const waited = await receipt.wait(controller);
3536
return { ...receipt, ...waited };
3637
}
3738
return receipt;

packages/sdk/src/helpers/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export {
22
type Signal,
33
type Wait,
4-
type SignalAndInterval,
4+
type PollingController,
55
type Interval,
6+
createSignal,
7+
createPollingController,
68
} from "./await.js";
79
export {
810
type ChainName,

packages/sdk/src/lowlevel.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,10 @@ function catchNotFound(err: unknown): [] {
252252
export async function queryRaw<T = unknown>(
253253
config: ReadConfig,
254254
statement: string,
255-
opts: Signal = {}
255+
signal?: Signal
256256
): Promise<Array<ValueOf<T>>> {
257257
const params = { statement, format: "table" } as const;
258-
const response = await getQuery<T>(config, params, opts)
258+
const response = await getQuery<T>(config, params, signal)
259259
.then((res) => res.rows)
260260
.catch(catchNotFound);
261261
return response;
@@ -264,19 +264,21 @@ export async function queryRaw<T = unknown>(
264264
export async function queryAll<T = unknown>(
265265
config: ReadConfig,
266266
statement: string,
267-
opts: Signal = {}
267+
signal?: Signal
268268
): Promise<ObjectsFormat<T>> {
269269
const params = { statement, format: "objects" } as const;
270-
const response = await getQuery<T>(config, params, opts).catch(catchNotFound);
270+
const response = await getQuery<T>(config, params, signal).catch(
271+
catchNotFound
272+
);
271273
return response;
272274
}
273275

274276
export async function queryFirst<T = unknown>(
275277
config: ReadConfig,
276278
statement: string,
277-
opts: Signal = {}
279+
signal?: Signal
278280
): Promise<T | null> {
279-
const response = await queryAll<T>(config, statement, opts).catch(
281+
const response = await queryAll<T>(config, statement, signal).catch(
280282
catchNotFound
281283
);
282284
return response.shift() ?? null;

packages/sdk/src/registry/utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from "../validator/receipt.js";
55
import { type Runnable } from "../registry/index.js";
66
import { normalize } from "../helpers/index.js";
7-
import { type SignalAndInterval, type Wait } from "../helpers/await.js";
7+
import { type PollingController, type Wait } from "../helpers/await.js";
88
import {
99
type Config,
1010
type ReadConfig,
@@ -173,9 +173,9 @@ export async function wrapTransaction(
173173
const name = `${prefix}_${chainId}_${_params.tableIds[0]}`;
174174
const params = { ..._params, chainId, tableId: _params.tableIds[0] };
175175
const wait = async (
176-
opts: SignalAndInterval = {}
176+
controller?: PollingController
177177
): Promise<TransactionReceipt & Named> => {
178-
const receipt = await pollTransactionReceipt(conn, params, opts);
178+
const receipt = await pollTransactionReceipt(conn, params, controller);
179179
if (receipt.error != null) {
180180
throw new Error(receipt.error);
181181
}
@@ -244,9 +244,9 @@ export async function wrapManyTransaction(
244244
};
245245

246246
const wait = async (
247-
opts: SignalAndInterval = {}
247+
controller?: PollingController
248248
): Promise<TransactionReceipt & Named> => {
249-
const receipt = await pollTransactionReceipt(conn, params, opts);
249+
const receipt = await pollTransactionReceipt(conn, params, controller);
250250
if (receipt.error != null) {
251251
throw new Error(receipt.error);
252252
}

0 commit comments

Comments
 (0)