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

Independent Table Elements #34

Merged
merged 23 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ coverage: build/coverage
ci-test: node_modules
@TS_NODE_PROJECT='./test/tsconfig.json' MOCK_DIR='./test/data/requests' \
${BIN}/nyc ${NYC_OPTS} --reporter=text \
${BIN}/mocha ${MOCHA_OPTS} -R list ${TEST_FILES}
${BIN}/mocha ${MOCHA_OPTS} -R list ${TEST_FILES} --no-timeout

.PHONY: check
check: node_modules
Expand Down
4 changes: 3 additions & 1 deletion src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export class Contract {
throw new Error(`Contract (${this.account}) does not have a table named (${name})`)
}
return Table.from({
contract: this,
abi: this.abi,
account: this.account,
client: this.client,
name,
})
}
Expand Down
198 changes: 99 additions & 99 deletions src/contract/table-cursor.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,78 @@
import {API, Serializer} from '@greymass/eosio'
import {ABI, ABIDef, API, APIClient, Serializer} from '@greymass/eosio'
import {wrapIndexValue} from '../utils'
import {Query, Table} from './table'

interface TableCursorParams {
table: Table
tableParams: API.v1.GetTableRowsParams
/** Mashup of valid types for an APIClient call to v1.chain.get_table_rows */
export type TableRowParamsTypes =
| API.v1.GetTableRowsParams
| API.v1.GetTableRowsParamsKeyed
| API.v1.GetTableRowsParamsTyped

export interface TableCursorArgs {
/** The ABI for the contract this table belongs to */
abi: ABIDef
/** The APIClient instance to use for API requests */
client: APIClient
/** The parameters used for the v1/chain/get_table_rows call */
params: TableRowParamsTypes
/** The maximum number of rows the cursor should retrieve */
maxRows?: number
next_key?: API.v1.TableIndexType | string
indexPositionField?: string
}

/**
* Represents a cursor for a table in the blockchain. Provides methods for
* iterating over the rows of the table.
*
* @typeparam TableRow The type of rows in the table.
*/
export class TableCursor<TableRow> {
private table: Table
/** The default parameters to use on a v1/chain/get_table_rows call */
const defaultParams = {
json: false,
limit: 1000,
}

export class TableCursor<RowType = any> {
/** The ABI for the contract this table belongs to */
readonly abi: ABI
/** The type of the table, as defined in the ABI */
readonly type: string
/** The parameters used for the v1/chain/get_table_rows call */
readonly params: TableRowParamsTypes
/** The APIClient instance to use for API requests */
readonly client: APIClient

/** For iterating on the cursor, the next key to query against lower_bounds */
private next_key: API.v1.TableIndexType | string | undefined
private tableParams: API.v1.GetTableRowsParams
/** Whether or not the cursor believes it has reached the end of its results */
private endReached = false
private indexPositionField?: string
/** The number of rows the cursor has retrieved */
private rowsCount = 0
/** The maximum number of rows the cursor should retrieve */
private maxRows: number = Number.MAX_SAFE_INTEGER

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that these were far from perfect, but we will still want to have some JSDoc statements here, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah absolutely. I just kept seeing so many things changing - that I wanted to redo them all if this was the direction we wanted to go 👍

* @param {TableCursorParams} params - Parameters for creating a new table cursor.
*
* @param {TableRow[]} params.rows - An array of rows that the cursor will iterate over.
* Each row represents an entry in the table.
* Create a new TableCursor instance.
*
* @param {Table} params.table - The table that the rows belong to.
*
* @param {API.v1.GetTableRowsParams} params.tableParams - Parameters for the `get_table_rows`
* API call, which are used to fetch the rows from the blockchain.
*
* @param {(Name | UInt64 | undefined)} [params.next_key] - The key for the next set of rows
* that the cursor can fetch. This is used for pagination when there are more rows than can be
* fetched in a single API call.
* @param args.abi The ABI for the contract.
* @param args.client The APIClient instance to use for API requests.
* @param args.params The parameters to use for the table query.
* @param args.maxRows The maximum number of rows to fetch.
* @returns A new TableCursor instance.
*/
constructor({table, tableParams, indexPositionField, maxRows, next_key}: TableCursorParams) {
this.table = table
this.tableParams = tableParams
this.next_key = next_key
this.indexPositionField = indexPositionField
if (maxRows) {
this.maxRows = maxRows
constructor(args: TableCursorArgs) {
this.abi = ABI.from(args.abi)
this.client = args.client
this.params = {
...defaultParams,
...args.params,
}
if (args.maxRows) {
this.maxRows = args.maxRows
}
const table = this.abi.tables.find((t) => t.name === String(this.params.table))
if (!table) {
throw new Error('Table not found')
}
this.type = table.type
}

/**
* Implements the async iterator protocol for the cursor.
*
* @returns An iterator for the rows in the table.
* @returns An iterator for all rows in the table.
*/
async *[Symbol.asyncIterator]() {
while (true) {
Expand All @@ -63,78 +82,77 @@ export class TableCursor<TableRow> {
yield row
}

// If no rows are returned or next_key is undefined, we have exhausted all rows
if (rows.length === 0 || !this.next_key) {
return
}
}
}

/**
* Fetches more rows from the table and appends them to the cursor.
* Fetch the next batch of rows from the cursor.
*
* @returns The new rows.
* @param rowsPerAPIRequest The number of rows to fetch per API request.
* @returns A promise containing the next batch of rows.
*/
async next(rowsPerAPIRequest?: number): Promise<TableRow[]> {
async next(rowsPerAPIRequest: number = Number.MAX_SAFE_INTEGER): Promise<RowType[]> {
// If the cursor has deemed its at the end, return an empty array
if (this.endReached) {
return []
}

let lower_bound = this.tableParams.lower_bound
const upper_bound = this.tableParams.upper_bound

// Set the lower_bound, and override if the cursor has a next_key value
let lower_bound = this.params.lower_bound
if (this.next_key) {
lower_bound = this.next_key
}

let indexPosition = this.tableParams.index_position || 'primary'
// Determine the maximum number of remaining rows for the cursor
const rowsRemaining = this.maxRows - this.rowsCount

if (this.indexPositionField) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit that I do love the fact that this is no longer needed 😁

const fieldToIndexMapping = this.table.getFieldToIndex()
// Find the lowest amount between rows remaining, rows per request, or the provided query params limit
const limit = Math.min(rowsRemaining, rowsPerAPIRequest, this.params.limit)

if (!fieldToIndexMapping[this.indexPositionField]) {
throw new Error(`Field ${this.indexPositionField} is not a valid index.`)
}

indexPosition = fieldToIndexMapping[this.indexPositionField].index_position
}

const result = await this.table.contract.client!.v1.chain.get_table_rows({
...this.tableParams,
limit: Math.min(
this.maxRows - this.rowsCount,
rowsPerAPIRequest || this.tableParams.limit
),
// Assemble and perform the v1/chain/get_table_rows query
const query = {
...this.params,
limit,
lower_bound: wrapIndexValue(lower_bound),
upper_bound: wrapIndexValue(upper_bound),
index_position: indexPosition,
json: false,
})
upper_bound: wrapIndexValue(this.params.upper_bound),
}

let {rows} = result
const result = await this.client!.v1.chain.get_table_rows(query)

// Determine if we need to decode the rows, based on if:
// - json parameter is false, meaning hex data will be returned
// - type parameter is not set, meaning the APIClient will not automatically decode
const requiresDecoding =
this.params.json === false && !(query as API.v1.GetTableRowsParamsTyped).type

// Retrieve the rows from the result, decoding if needed
const rows: RowType[] = requiresDecoding
? result.rows.map((data) =>
Serializer.decode({
data,
abi: this.abi,
type: this.type,
})
)
: result.rows

// Persist cursor state for subsequent calls
this.next_key = result.next_key

this.rowsCount += rows.length

// Determine if we've reached the end of the cursor
if (!result.next_key || rows.length === 0 || this.rowsCount === this.maxRows) {
this.endReached = true
}

rows = rows.map((row) =>
Serializer.decode({
data: row,
abi: this.table.contract.abi,
type: this.table.abi.type,
})
)

return rows
}

/**
* Resets the cursor to the beginning of the table and returns the first rows.
*
* @returns The first rows in the table.
* Reset the internal state of the cursor
*/
async reset() {
this.next_key = undefined
Expand All @@ -143,33 +161,15 @@ export class TableCursor<TableRow> {
}

/**
* Returns all rows in the cursor query.
* Fetch all rows from the cursor by recursively calling next() until the end is reached.
*
* @returns All rows in the cursor query.
* @returns A promise containing all rows for the cursor.
*/
async all() {
const rows: TableRow[] = []
async all(): Promise<RowType[]> {
const rows: RowType[] = []
for await (const row of this) {
rows.push(row)
}
return rows
}

/**
* Returns a new cursor with updated parameters.
*
* @returns A new cursor with updated parameters.
*/
query(query: Query) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I guess one side effect of decoupling table and table cursor is that this now feels out of place 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah if we want it back - it's going to need to be a function that translates a TableCursor back into a Table call, and that kinda re-adds that dependency this was seeking to remove.

I'm thinking maybe we add this feature into whatever a future QueryBuilder might be, and leave it off for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

return new TableCursor({
table: this.table,
tableParams: {
...this.tableParams,
limit: query.rowsPerAPIRequest || this.tableParams.limit,
scope: query.scope || this.tableParams.scope,
lower_bound: query.from ? wrapIndexValue(query.from) : this.tableParams.lower_bound,
upper_bound: query.to ? wrapIndexValue(query.to) : this.tableParams.upper_bound,
},
})
}
}
Loading