feat: add BatchProcessor for efficient queued blockchain reads

- Introduced BatchProcessor to enable queue-based batch reading from Stacks blockchain
- Updated README.md with comprehensive documentation and usage examples
- Added BatchProcessor and BatchAPI implementation with support for variables, map entries, and readonly function calls
- Implemented automatic batching and result distribution for multiple read operations
- Updated sample code to demonstrate BatchProcessor usage
This commit is contained in:
Kyle Fang
2025-02-23 12:46:50 +00:00
parent c42c475c48
commit 8a84661463
5 changed files with 316 additions and 30 deletions

130
README.md
View File

@@ -12,33 +12,7 @@ yarn add stxer
## Features
### 1. Batch Operations
The SDK provides efficient batch reading capabilities for Stacks blockchain:
```typescript
import { batchRead, batchReadonly } from 'stxer';
// Batch read variables and maps
const result = await batchRead({
variables: [{
contract: contractPrincipalCV(...),
variableName: 'my-var'
}],
maps: [{
contract: contractPrincipalCV(...),
mapName: 'my-map',
mapKey: someCV
}],
readonly: [{
contract: contractPrincipalCV(...),
functionName: 'my-function',
functionArgs: [/* clarity values */]
}]
});
```
### 2. Transaction Simulation
### 1. Transaction Simulation
Simulate complex transaction sequences before executing them on-chain:
@@ -67,6 +41,108 @@ const simulationId = await SimulationBuilder.new({
// View simulation results at: https://stxer.xyz/simulations/{network}/{simulationId}
```
### 2. Batch Operations
The SDK provides two approaches for efficient batch reading from the Stacks blockchain:
#### Direct Batch Reading
```typescript
import { batchRead } from 'stxer';
// Batch read variables and maps
const result = await batchRead({
variables: [{
contract: contractPrincipalCV(...),
variableName: 'my-var'
}],
maps: [{
contract: contractPrincipalCV(...),
mapName: 'my-map',
mapKey: someCV
}],
readonly: [{
contract: contractPrincipalCV(...),
functionName: 'my-function',
functionArgs: [/* clarity values */]
}]
});
```
#### BatchProcessor for Queue-based Operations
The BatchProcessor allows you to queue multiple read operations and automatically batch them together after a specified delay:
```typescript
import { BatchProcessor } from 'stxer';
const processor = new BatchProcessor({
stxerAPIEndpoint: 'https://api.stxer.xyz', // optional
batchDelayMs: 1000, // delay before processing batch
});
// Queue multiple operations that will be batched together
const [resultA, resultB] = await Promise.all([
new Promise((resolve, reject) => {
processor.enqueue({
request: {
mode: 'variable',
contractAddress: 'SP...',
contractName: 'my-contract',
variableName: 'variable-a'
},
resolve,
reject
});
}),
new Promise((resolve, reject) => {
processor.enqueue({
request: {
mode: 'variable',
contractAddress: 'SP...',
contractName: 'my-contract',
variableName: 'variable-b'
},
resolve,
reject
});
})
]);
// You can also queue different types of operations
processor.enqueue({
request: {
mode: 'readonly',
contractAddress: 'SP...',
contractName: 'my-contract',
functionName: 'get-value',
functionArgs: []
},
resolve: (value) => console.log('Function result:', value),
reject: (error) => console.error('Error:', error)
});
processor.enqueue({
request: {
mode: 'mapEntry',
contractAddress: 'SP...',
contractName: 'my-contract',
mapName: 'my-map',
mapKey: someKey
},
resolve: (value) => console.log('Map entry:', value),
reject: (error) => console.error('Error:', error)
});
```
The BatchProcessor automatically:
- Queues read operations
- Batches them together after the specified delay
- Makes a single API call for all queued operations
- Distributes results back to the respective promises
This is particularly useful when you need to make multiple blockchain reads and want to optimize network calls.
## Configuration
You can customize the API endpoints:

172
src/BatchProcessor.ts Normal file
View File

@@ -0,0 +1,172 @@
/**
* WARNING:
*
* this file will be used in cross-runtime environments (browser, cloudflare workers, XLinkSDK, etc.),
* so please be careful when adding `import`s to it.
*/
import {
type ClarityValue,
type OptionalCV,
contractPrincipalCV,
} from '@stacks/transactions';
import { type BatchReads, batchRead } from './BatchAPI';
export interface ReadOnlyRequest {
mode: 'readonly';
contractAddress: string;
contractName: string;
functionName: string;
functionArgs: ClarityValue[];
}
export interface MapEntryRequest {
mode: 'mapEntry';
contractAddress: string;
contractName: string;
mapName: string;
mapKey: ClarityValue;
}
export interface VariableRequest {
mode: 'variable';
contractAddress: string;
contractName: string;
variableName: string;
}
export type BatchRequest = MapEntryRequest | VariableRequest | ReadOnlyRequest;
export interface QueuedRequest {
request: BatchRequest;
tip?: string;
resolve: (value: ClarityValue | OptionalCV) => void;
reject: (error: Error) => void;
}
export class BatchProcessor {
private queues = new Map<string, QueuedRequest[]>();
private timeoutIds = new Map<string, ReturnType<typeof setTimeout>>();
private readonly stxerAPIEndpoint: string;
private readonly batchDelayMs: number;
constructor(options: { stxerAPIEndpoint?: string; batchDelayMs: number }) {
this.stxerAPIEndpoint = options.stxerAPIEndpoint ?? 'https://api.stxer.xyz';
this.batchDelayMs = options.batchDelayMs;
}
private getQueueKey(tip?: string): string {
return tip ?? '_undefined';
}
enqueue(request: QueuedRequest): void {
const queueKey = this.getQueueKey(request.tip);
const queue = this.queues.get(queueKey) ?? [];
if (!this.queues.has(queueKey)) {
this.queues.set(queueKey, queue);
}
queue.push(request);
if (!this.timeoutIds.has(queueKey)) {
const timeoutId = setTimeout(
() => this.processBatch(queueKey),
this.batchDelayMs,
);
this.timeoutIds.set(queueKey, timeoutId);
}
}
private async processBatch(queueKey: string): Promise<void> {
const currentQueue = this.queues.get(queueKey) ?? [];
this.queues.delete(queueKey);
const timeoutId = this.timeoutIds.get(queueKey);
if (timeoutId) {
clearTimeout(timeoutId);
this.timeoutIds.delete(queueKey);
}
if (currentQueue.length === 0) return;
try {
const readonlyRequests = currentQueue.filter(
(q): q is QueuedRequest & { request: ReadOnlyRequest } =>
q.request.mode === 'readonly',
);
const mapRequests = currentQueue.filter(
(q): q is QueuedRequest & { request: MapEntryRequest } =>
q.request.mode === 'mapEntry',
);
const variableRequests = currentQueue.filter(
(q): q is QueuedRequest & { request: VariableRequest } =>
q.request.mode === 'variable',
);
const tip = queueKey === '_undefined' ? undefined : queueKey;
const batchRequest: BatchReads = {
readonly: readonlyRequests.map(({ request }) => ({
contract: contractPrincipalCV(
request.contractAddress,
request.contractName,
),
functionName: request.functionName,
functionArgs: request.functionArgs,
})),
maps: mapRequests.map(({ request }) => ({
contract: contractPrincipalCV(
request.contractAddress,
request.contractName,
),
mapName: request.mapName,
mapKey: request.mapKey,
})),
variables: variableRequests.map(({ request }) => ({
contract: contractPrincipalCV(
request.contractAddress,
request.contractName,
),
variableName: request.variableName,
})),
index_block_hash: tip,
};
const results = await batchRead(batchRequest, {
stxerApi: this.stxerAPIEndpoint,
});
// Handle readonly results
for (const [index, result] of results.readonly.entries()) {
if (result instanceof Error) {
readonlyRequests[index].reject(result);
} else {
readonlyRequests[index].resolve(result);
}
}
// Handle variable results
for (const [index, result] of results.vars.entries()) {
if (result instanceof Error) {
variableRequests[index].reject(result);
} else {
variableRequests[index].resolve(result);
}
}
// Handle map results
for (const [index, result] of results.maps.entries()) {
if (result instanceof Error) {
mapRequests[index].reject(result);
} else {
mapRequests[index].resolve(result);
}
}
} catch (error) {
for (const item of currentQueue) {
item.reject(error as Error);
}
}
}
}

View File

@@ -1,2 +1,2 @@
export * from './batch-api';
export * from './BatchAPI';
export * from './simulation';

View File

@@ -5,7 +5,8 @@ import {
tupleCV,
uintCV,
} from '@stacks/transactions';
import { batchRead } from '../batch-api';
import { batchRead } from '../BatchAPI';
import { BatchProcessor } from '../BatchProcessor';
async function batchReadsExample() {
const rs = await batchRead({
@@ -72,9 +73,46 @@ async function batchReadsExample() {
console.log(rs);
}
async function batchQueueProcessorExample() {
const processor = new BatchProcessor({
stxerAPIEndpoint: 'https://api.stxer.xyz',
batchDelayMs: 1000,
});
const promiseA = new Promise((resolve, reject) => {
processor.enqueue({
request: {
mode: 'variable',
contractAddress: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
contractName: 'liquidity-token-v5kbe3oqvac',
variableName: 'balance-x',
},
resolve: resolve,
reject: reject,
});
});
const promiseB = new Promise((resolve, reject) => {
processor.enqueue({
request: {
mode: 'variable',
contractAddress: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
contractName: 'liquidity-token-v5kbe3oqvac',
variableName: 'balance-y',
},
resolve: resolve,
reject: reject,
});
});
const result = await Promise.all([promiseA, promiseB]);
console.log(result);
}
async function main() {
await batchReadsExample();
// await batchReadonlyExample();
await batchQueueProcessorExample();
}
if (require.main === module) {