mirror of
https://github.com/stxer/stxer-sdk.git
synced 2026-01-12 07:23:57 +08:00
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:
130
README.md
130
README.md
@@ -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
172
src/BatchProcessor.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './batch-api';
|
||||
export * from './BatchAPI';
|
||||
export * from './simulation';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user