@subsquid/portal-client 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/lib/client.d.ts +102 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.example.d.ts +2 -0
- package/lib/client.example.d.ts.map +1 -0
- package/lib/client.example.js +158 -0
- package/lib/client.example.js.map +1 -0
- package/lib/client.js +440 -0
- package/lib/client.js.map +1 -0
- package/lib/query/common.d.ts +47 -0
- package/lib/query/common.d.ts.map +1 -0
- package/lib/query/common.js +3 -0
- package/lib/query/common.js.map +1 -0
- package/lib/query/evm.d.ts +268 -0
- package/lib/query/evm.d.ts.map +1 -0
- package/lib/query/evm.js +3 -0
- package/lib/query/evm.js.map +1 -0
- package/lib/query/index.d.ts +5 -0
- package/lib/query/index.d.ts.map +1 -0
- package/lib/query/index.js +3 -0
- package/lib/query/index.js.map +1 -0
- package/lib/query/solana.d.ts +224 -0
- package/lib/query/solana.d.ts.map +1 -0
- package/lib/query/solana.js +3 -0
- package/lib/query/solana.js.map +1 -0
- package/lib/query/substrate.d.ts +180 -0
- package/lib/query/substrate.d.ts.map +1 -0
- package/lib/query/substrate.js +3 -0
- package/lib/query/substrate.js.map +1 -0
- package/package.json +32 -0
- package/src/client.example.ts +167 -0
- package/src/client.ts +639 -0
- package/src/query/common.ts +59 -0
- package/src/query/evm.ts +367 -0
- package/src/query/index.ts +4 -0
- package/src/query/solana.ts +276 -0
- package/src/query/substrate.ts +194 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import {HttpBody, HttpClient, HttpClientOptions, HttpError, HttpResponse, RequestOptions} from '@subsquid/http-client'
|
|
2
|
+
import {
|
|
3
|
+
addErrorContext,
|
|
4
|
+
createFuture,
|
|
5
|
+
Future,
|
|
6
|
+
last,
|
|
7
|
+
unexpectedCase,
|
|
8
|
+
wait,
|
|
9
|
+
withErrorContext,
|
|
10
|
+
} from '@subsquid/util-internal'
|
|
11
|
+
import {Readable} from 'stream'
|
|
12
|
+
import {evm, PortalBlock, PortalQuery, solana, substrate} from './query'
|
|
13
|
+
import {Simplify} from './query/common'
|
|
14
|
+
|
|
15
|
+
export type * from './query'
|
|
16
|
+
|
|
17
|
+
const USER_AGENT = `@subsquid/portal-client (https://sqd.ai)`
|
|
18
|
+
|
|
19
|
+
export interface PortalClientOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The URL of the portal dataset.
|
|
22
|
+
*/
|
|
23
|
+
url: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional custom HTTP client to use.
|
|
27
|
+
*/
|
|
28
|
+
http?: HttpClient | HttpClientOptions
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Minimum number of bytes to return.
|
|
32
|
+
* @default 10_485_760 (10MB)
|
|
33
|
+
*/
|
|
34
|
+
minBytes?: number
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of bytes to return.
|
|
38
|
+
* @default minBytes
|
|
39
|
+
*/
|
|
40
|
+
maxBytes?: number
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maximum time between stream data in milliseconds for return.
|
|
44
|
+
* @default 300
|
|
45
|
+
*/
|
|
46
|
+
maxIdleTime?: number
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maximum wait time in milliseconds for return.
|
|
50
|
+
* @default 5_000
|
|
51
|
+
*/
|
|
52
|
+
maxWaitTime?: number
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Interval for polling the head in milliseconds.
|
|
56
|
+
* @default 0
|
|
57
|
+
*/
|
|
58
|
+
headPollInterval?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PortalRequestOptions {
|
|
62
|
+
headers?: HeadersInit
|
|
63
|
+
retryAttempts?: number
|
|
64
|
+
retrySchedule?: number[]
|
|
65
|
+
httpTimeout?: number
|
|
66
|
+
bodyTimeout?: number
|
|
67
|
+
abort?: AbortSignal
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PortalStreamOptions {
|
|
71
|
+
request?: Omit<PortalRequestOptions, 'abort'>
|
|
72
|
+
|
|
73
|
+
minBytes?: number
|
|
74
|
+
maxBytes?: number
|
|
75
|
+
maxIdleTime?: number
|
|
76
|
+
maxWaitTime?: number
|
|
77
|
+
|
|
78
|
+
headPollInterval?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type PortalStreamData<B> = {
|
|
82
|
+
blocks: B[]
|
|
83
|
+
finalizedHead?: BlockRef
|
|
84
|
+
meta: {
|
|
85
|
+
bytes: number
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PortalStream<B> extends AsyncIterable<PortalStreamData<B>> {}
|
|
90
|
+
|
|
91
|
+
export type GetBlock<Q extends evm.Query | solana.Query | substrate.Query> = Q['type'] extends 'evm'
|
|
92
|
+
? evm.Block<Q['fields']>
|
|
93
|
+
: Q['type'] extends 'solana'
|
|
94
|
+
? solana.Block<Q['fields']>
|
|
95
|
+
: Q['type'] extends 'substrate'
|
|
96
|
+
? substrate.Block<Q['fields']>
|
|
97
|
+
: PortalBlock
|
|
98
|
+
|
|
99
|
+
export type BlockRef = {
|
|
100
|
+
hash: string
|
|
101
|
+
number: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createQuery<Q extends evm.Query | solana.Query | substrate.Query>(query: Q): Simplify<Q & PortalQuery> {
|
|
105
|
+
return query
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class PortalClient {
|
|
109
|
+
private url: URL
|
|
110
|
+
private client: HttpClient
|
|
111
|
+
private headPollInterval: number
|
|
112
|
+
private minBytes: number
|
|
113
|
+
private maxBytes: number
|
|
114
|
+
private maxIdleTime: number
|
|
115
|
+
private maxWaitTime: number
|
|
116
|
+
|
|
117
|
+
constructor(options: PortalClientOptions) {
|
|
118
|
+
this.url = new URL(options.url)
|
|
119
|
+
this.client = options.http instanceof HttpClient ? options.http : new HttpClient(options.http)
|
|
120
|
+
this.headPollInterval = options.headPollInterval ?? 0
|
|
121
|
+
this.minBytes = options.minBytes ?? 10 * 1024 * 1024
|
|
122
|
+
this.maxBytes = options.maxBytes ?? this.minBytes
|
|
123
|
+
this.maxIdleTime = options.maxIdleTime ?? 300
|
|
124
|
+
this.maxWaitTime = options.maxWaitTime ?? 5_000
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private getDatasetUrl(path: string): string {
|
|
128
|
+
let u = new URL(this.url)
|
|
129
|
+
if (this.url.pathname.endsWith('/')) {
|
|
130
|
+
u.pathname += path
|
|
131
|
+
} else {
|
|
132
|
+
u.pathname += '/' + path
|
|
133
|
+
}
|
|
134
|
+
return u.toString()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getHead(options?: PortalRequestOptions): Promise<BlockRef | undefined> {
|
|
138
|
+
const res = await this.request('GET', this.getDatasetUrl('head'), options)
|
|
139
|
+
return res.body ?? undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async getFinalizedHead(options?: PortalRequestOptions): Promise<BlockRef | undefined> {
|
|
143
|
+
const res = await this.request('GET', this.getDatasetUrl('finalized-head'), options)
|
|
144
|
+
return res.body ?? undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @deprecated
|
|
149
|
+
*/
|
|
150
|
+
async getFinalizedHeight(options?: PortalRequestOptions): Promise<number> {
|
|
151
|
+
let {body} = await this.request<string>('GET', this.getDatasetUrl('finalized-stream/height'), options)
|
|
152
|
+
let height = parseInt(body)
|
|
153
|
+
return height
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getFinalizedQuery<Q extends evm.Query | solana.Query | substrate.Query, R extends GetBlock<Q> = GetBlock<Q>>(
|
|
157
|
+
query: Q,
|
|
158
|
+
options?: PortalRequestOptions
|
|
159
|
+
): Promise<R[]> {
|
|
160
|
+
// FIXME: is it needed or it is better to always use stream?
|
|
161
|
+
return this.request<Buffer>('POST', this.getDatasetUrl(`finalized-stream`), {
|
|
162
|
+
...options,
|
|
163
|
+
json: query,
|
|
164
|
+
})
|
|
165
|
+
.catch(
|
|
166
|
+
withErrorContext({
|
|
167
|
+
archiveQuery: query,
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
.then((res) => {
|
|
171
|
+
let blocks = res.body
|
|
172
|
+
.toString('utf8')
|
|
173
|
+
.trimEnd()
|
|
174
|
+
.split('\n')
|
|
175
|
+
.map((line) => JSON.parse(line))
|
|
176
|
+
return blocks
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getQuery<Q extends PortalQuery = PortalQuery, R extends PortalBlock = PortalBlock>(
|
|
181
|
+
query: Q,
|
|
182
|
+
options?: PortalRequestOptions
|
|
183
|
+
): Promise<R[]> {
|
|
184
|
+
// FIXME: is it needed or it is better to always use stream?
|
|
185
|
+
return this.request<Buffer>('POST', this.getDatasetUrl(`stream`), {
|
|
186
|
+
...options,
|
|
187
|
+
json: query,
|
|
188
|
+
})
|
|
189
|
+
.catch(
|
|
190
|
+
withErrorContext({
|
|
191
|
+
archiveQuery: query,
|
|
192
|
+
})
|
|
193
|
+
)
|
|
194
|
+
.then((res) => {
|
|
195
|
+
let blocks = res.body
|
|
196
|
+
.toString('utf8')
|
|
197
|
+
.trimEnd()
|
|
198
|
+
.split('\n')
|
|
199
|
+
.map((line) => JSON.parse(line))
|
|
200
|
+
return blocks
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getFinalizedStream<Q extends evm.Query | solana.Query | substrate.Query, R extends GetBlock<Q> = GetBlock<Q>>(
|
|
205
|
+
query: Q,
|
|
206
|
+
options?: PortalStreamOptions
|
|
207
|
+
): PortalStream<R> {
|
|
208
|
+
return createPortalStream(query, this.getStreamOptions(options), async (q, o) =>
|
|
209
|
+
this.getStreamRequest('finalized-stream', q, o)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getStream<Q extends evm.Query | solana.Query | substrate.Query, R extends GetBlock<Q> = GetBlock<Q>>(
|
|
214
|
+
query: Q,
|
|
215
|
+
options?: PortalStreamOptions
|
|
216
|
+
): PortalStream<R> {
|
|
217
|
+
return createPortalStream(query, this.getStreamOptions(options), async (q, o) =>
|
|
218
|
+
this.getStreamRequest('stream', q, o)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private getStreamOptions(options?: PortalStreamOptions) {
|
|
223
|
+
let {
|
|
224
|
+
headPollInterval = this.headPollInterval,
|
|
225
|
+
minBytes = this.minBytes,
|
|
226
|
+
maxBytes = this.maxBytes,
|
|
227
|
+
maxIdleTime = this.maxIdleTime,
|
|
228
|
+
maxWaitTime = this.maxWaitTime,
|
|
229
|
+
request = {},
|
|
230
|
+
} = options ?? {}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
headPollInterval,
|
|
234
|
+
minBytes,
|
|
235
|
+
maxBytes,
|
|
236
|
+
maxIdleTime,
|
|
237
|
+
maxWaitTime,
|
|
238
|
+
request,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async getStreamRequest(path: string, query: PortalQuery, options?: PortalRequestOptions) {
|
|
243
|
+
try {
|
|
244
|
+
let res = await this.request<Readable | undefined>('POST', this.getDatasetUrl(path), {
|
|
245
|
+
...options,
|
|
246
|
+
json: query,
|
|
247
|
+
stream: true,
|
|
248
|
+
}).catch(
|
|
249
|
+
withErrorContext({
|
|
250
|
+
query: query,
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
switch (res.status) {
|
|
255
|
+
case 200:
|
|
256
|
+
let finalizedHead = getFinalizedHeadHeader(res.headers)
|
|
257
|
+
let stream = res.body ? splitLines(res.body) : undefined
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
finalizedHead,
|
|
261
|
+
stream,
|
|
262
|
+
}
|
|
263
|
+
case 204:
|
|
264
|
+
return {
|
|
265
|
+
finalizedHead: getFinalizedHeadHeader(res.headers),
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
throw unexpectedCase(res.status)
|
|
269
|
+
}
|
|
270
|
+
} catch (e: unknown) {
|
|
271
|
+
if (isForkHttpError(e) && query.fromBlock != null && query.parentBlockHash != null) {
|
|
272
|
+
e = new ForkException(e.response.body.lastBlocks, {
|
|
273
|
+
number: query.fromBlock - 1,
|
|
274
|
+
hash: query.parentBlockHash,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw addErrorContext(e as any, {
|
|
279
|
+
query,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private request<T = any>(method: string, url: string, options: RequestOptions & HttpBody = {}) {
|
|
285
|
+
return this.client.request<T>(method, url, {
|
|
286
|
+
...options,
|
|
287
|
+
headers: {
|
|
288
|
+
'User-Agent': USER_AGENT,
|
|
289
|
+
...options?.headers,
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isForkHttpError(err: unknown): err is HttpError {
|
|
296
|
+
if (!(err instanceof HttpError)) return false
|
|
297
|
+
if (err.response.status !== 409) return false
|
|
298
|
+
if (err.response.body.lastBlocks == null) return false
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function createPortalStream<Q extends PortalQuery = PortalQuery, R extends PortalBlock = any>(
|
|
303
|
+
query: Q,
|
|
304
|
+
options: Required<PortalStreamOptions>,
|
|
305
|
+
requestStream: (
|
|
306
|
+
query: Q,
|
|
307
|
+
options?: PortalRequestOptions
|
|
308
|
+
) => Promise<{finalizedHead?: BlockRef; stream?: AsyncIterable<string[]> | null | undefined}>
|
|
309
|
+
): PortalStream<R> {
|
|
310
|
+
let {headPollInterval, request, ...bufferOptions} = options
|
|
311
|
+
|
|
312
|
+
let buffer = new PortalStreamBuffer<R>(bufferOptions)
|
|
313
|
+
|
|
314
|
+
let {fromBlock = 0, toBlock, parentBlockHash} = query
|
|
315
|
+
|
|
316
|
+
const ingest = async () => {
|
|
317
|
+
if (buffer.signal.aborted) return
|
|
318
|
+
if (toBlock != null && fromBlock > toBlock) return
|
|
319
|
+
|
|
320
|
+
let res = await requestStream(
|
|
321
|
+
{
|
|
322
|
+
...query,
|
|
323
|
+
fromBlock,
|
|
324
|
+
parentBlockHash,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
...request,
|
|
328
|
+
abort: buffer.signal,
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
const finalizedHead = res.finalizedHead
|
|
333
|
+
|
|
334
|
+
// we are on head
|
|
335
|
+
if (!('stream' in res)) {
|
|
336
|
+
await buffer.put({blocks: [], meta: {bytes: 0}, finalizedHead})
|
|
337
|
+
buffer.flush()
|
|
338
|
+
if (headPollInterval > 0) {
|
|
339
|
+
await wait(headPollInterval, buffer.signal)
|
|
340
|
+
}
|
|
341
|
+
return ingest()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// no data left on this range
|
|
345
|
+
if (res.stream == null) return
|
|
346
|
+
|
|
347
|
+
let iterator = res.stream[Symbol.asyncIterator]()
|
|
348
|
+
try {
|
|
349
|
+
while (true) {
|
|
350
|
+
let data = await iterator.next()
|
|
351
|
+
if (data.done) break
|
|
352
|
+
|
|
353
|
+
let blocks: R[] = []
|
|
354
|
+
let bytes = 0
|
|
355
|
+
|
|
356
|
+
for (let line of data.value) {
|
|
357
|
+
let block = JSON.parse(line) as R
|
|
358
|
+
blocks.push(block)
|
|
359
|
+
bytes += line.length
|
|
360
|
+
|
|
361
|
+
fromBlock = block.header.number + 1
|
|
362
|
+
parentBlockHash = block.header.hash
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await buffer.put({blocks, finalizedHead, meta: {bytes}})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
buffer.flush()
|
|
369
|
+
} catch (err) {
|
|
370
|
+
if (buffer.signal.aborted || isStreamAbortedError(err)) {
|
|
371
|
+
// ignore
|
|
372
|
+
} else {
|
|
373
|
+
throw err
|
|
374
|
+
}
|
|
375
|
+
} finally {
|
|
376
|
+
await iterator?.return?.().catch(() => {})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return ingest()
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
ingest().then(
|
|
383
|
+
() => buffer.close(),
|
|
384
|
+
(err) => buffer.fail(err)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return buffer.iterate()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
class PortalStreamBuffer<B> {
|
|
391
|
+
private buffer: PortalStreamData<B> | undefined
|
|
392
|
+
private state: 'pending' | 'ready' | 'failed' | 'closed' = 'pending'
|
|
393
|
+
private error: unknown
|
|
394
|
+
|
|
395
|
+
private readyFuture: Future<void> = createFuture()
|
|
396
|
+
private takeFuture: Future<void> = createFuture()
|
|
397
|
+
private putFuture: Future<void> = createFuture()
|
|
398
|
+
|
|
399
|
+
private idleTimeout: ReturnType<typeof setTimeout> | undefined
|
|
400
|
+
private waitTimeout: ReturnType<typeof setTimeout> | undefined
|
|
401
|
+
|
|
402
|
+
private minBytes: number
|
|
403
|
+
private maxBytes: number
|
|
404
|
+
private maxIdleTime: number
|
|
405
|
+
private maxWaitTime: number
|
|
406
|
+
|
|
407
|
+
private abortController = new AbortController()
|
|
408
|
+
|
|
409
|
+
get signal() {
|
|
410
|
+
return this.abortController.signal
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
constructor(options: {maxWaitTime: number; maxBytes: number; maxIdleTime: number; minBytes: number}) {
|
|
414
|
+
this.maxWaitTime = options.maxWaitTime
|
|
415
|
+
this.minBytes = options.minBytes
|
|
416
|
+
this.maxBytes = Math.max(options.maxBytes, options.minBytes)
|
|
417
|
+
this.maxIdleTime = options.maxIdleTime
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async take(): Promise<PortalStreamData<B> | undefined> {
|
|
421
|
+
if (this.state === 'pending') {
|
|
422
|
+
this.waitTimeout = setTimeout(() => this._ready(), this.maxWaitTime)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await Promise.all([this.readyFuture.promise(), this.putFuture.promise()])
|
|
426
|
+
|
|
427
|
+
if (this.state === 'failed') {
|
|
428
|
+
throw this.error
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let result = this.buffer
|
|
432
|
+
this.buffer = undefined
|
|
433
|
+
|
|
434
|
+
if (this.state === 'closed') {
|
|
435
|
+
return result
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (result == null) {
|
|
439
|
+
throw new Error('Buffer is empty')
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.takeFuture.resolve()
|
|
443
|
+
|
|
444
|
+
this.readyFuture = createFuture()
|
|
445
|
+
this.putFuture = createFuture()
|
|
446
|
+
this.takeFuture = createFuture()
|
|
447
|
+
this.state = 'pending'
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async put(data: PortalStreamData<B>) {
|
|
453
|
+
if (this.state === 'closed' || this.state === 'failed') {
|
|
454
|
+
throw new Error('Buffer is closed')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.idleTimeout != null) {
|
|
458
|
+
clearTimeout(this.idleTimeout)
|
|
459
|
+
this.idleTimeout = undefined
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (this.buffer == null) {
|
|
463
|
+
this.buffer = {blocks: [], meta: {bytes: 0}}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.buffer.blocks.push(...data.blocks)
|
|
467
|
+
this.buffer.finalizedHead = data.finalizedHead
|
|
468
|
+
this.buffer.meta.bytes += data.meta.bytes
|
|
469
|
+
|
|
470
|
+
this.putFuture.resolve()
|
|
471
|
+
|
|
472
|
+
if (this.buffer.meta.bytes >= this.minBytes) {
|
|
473
|
+
this.readyFuture.resolve()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.buffer.meta.bytes >= this.maxBytes) {
|
|
477
|
+
await this.takeFuture.promise()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.state === 'pending') {
|
|
481
|
+
this.idleTimeout = setTimeout(() => this._ready(), this.maxIdleTime)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
flush() {
|
|
486
|
+
if (this.buffer == null) return
|
|
487
|
+
this._ready()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
close() {
|
|
491
|
+
if (this.state === 'closed' || this.state === 'failed') return
|
|
492
|
+
this.state = 'closed'
|
|
493
|
+
this._cleanup()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fail(err: any) {
|
|
497
|
+
if (this.state === 'closed' || this.state === 'failed') return
|
|
498
|
+
this.state = 'failed'
|
|
499
|
+
this.error = err
|
|
500
|
+
this._cleanup()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
iterate() {
|
|
504
|
+
return {
|
|
505
|
+
[Symbol.asyncIterator]: (): AsyncIterator<PortalStreamData<B>> => {
|
|
506
|
+
return {
|
|
507
|
+
next: async (): Promise<IteratorResult<PortalStreamData<B>>> => {
|
|
508
|
+
const value = await this.take()
|
|
509
|
+
if (value == null) {
|
|
510
|
+
return {done: true, value: undefined}
|
|
511
|
+
}
|
|
512
|
+
return {done: false, value}
|
|
513
|
+
},
|
|
514
|
+
return: async (): Promise<IteratorResult<PortalStreamData<B>>> => {
|
|
515
|
+
this.close()
|
|
516
|
+
return {done: true, value: undefined}
|
|
517
|
+
},
|
|
518
|
+
throw: async (error?: any): Promise<IteratorResult<PortalStreamData<B>>> => {
|
|
519
|
+
this.fail(error)
|
|
520
|
+
throw error
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private _ready() {
|
|
528
|
+
if (this.state === 'pending') {
|
|
529
|
+
this.state = 'ready'
|
|
530
|
+
this.readyFuture.resolve()
|
|
531
|
+
}
|
|
532
|
+
if (this.idleTimeout != null) {
|
|
533
|
+
clearTimeout(this.idleTimeout)
|
|
534
|
+
this.idleTimeout = undefined
|
|
535
|
+
}
|
|
536
|
+
if (this.waitTimeout != null) {
|
|
537
|
+
clearTimeout(this.waitTimeout)
|
|
538
|
+
this.waitTimeout = undefined
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private _cleanup() {
|
|
543
|
+
if (this.idleTimeout != null) {
|
|
544
|
+
clearTimeout(this.idleTimeout)
|
|
545
|
+
this.idleTimeout = undefined
|
|
546
|
+
}
|
|
547
|
+
if (this.waitTimeout != null) {
|
|
548
|
+
clearTimeout(this.waitTimeout)
|
|
549
|
+
this.waitTimeout = undefined
|
|
550
|
+
}
|
|
551
|
+
this.readyFuture.resolve()
|
|
552
|
+
this.putFuture.resolve()
|
|
553
|
+
this.takeFuture.resolve()
|
|
554
|
+
this.abortController.abort()
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function* splitLines(chunks: AsyncIterable<Uint8Array>) {
|
|
559
|
+
let splitter = new LineSplitter()
|
|
560
|
+
for await (let chunk of chunks) {
|
|
561
|
+
let lines = splitter.push(chunk)
|
|
562
|
+
if (lines) yield lines
|
|
563
|
+
}
|
|
564
|
+
let lastLine = splitter.end()
|
|
565
|
+
if (lastLine) yield [lastLine]
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
class LineSplitter {
|
|
569
|
+
private decoder = new TextDecoder('utf8')
|
|
570
|
+
private line = ''
|
|
571
|
+
|
|
572
|
+
push(data: Uint8Array): string[] | undefined {
|
|
573
|
+
let s = this.decoder.decode(data)
|
|
574
|
+
if (!s) return
|
|
575
|
+
let lines = s.split('\n')
|
|
576
|
+
if (lines.length == 1) {
|
|
577
|
+
this.line += lines[0]
|
|
578
|
+
} else {
|
|
579
|
+
let result: string[] = []
|
|
580
|
+
lines[0] = this.line + lines[0]
|
|
581
|
+
this.line = last(lines)
|
|
582
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
583
|
+
let line = lines[i]
|
|
584
|
+
if (line) {
|
|
585
|
+
result.push(line)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (result.length > 0) return result
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
end(): string | undefined {
|
|
593
|
+
if (this.line) return this.line
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export class ForkException extends Error {
|
|
598
|
+
readonly name = 'ForkError'
|
|
599
|
+
|
|
600
|
+
constructor(readonly lastBlocks: BlockRef[], readonly head: BlockRef) {
|
|
601
|
+
let parent = last(lastBlocks)
|
|
602
|
+
super(
|
|
603
|
+
`expected ${head.number + 1} to have parent ${parent.number}#${parent.hash}, but got ${head.number}#${
|
|
604
|
+
head.hash
|
|
605
|
+
}`
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function isForkException(err: unknown): err is ForkException {
|
|
611
|
+
if (err instanceof ForkException) return true
|
|
612
|
+
if (err instanceof Error && err.name === 'ForkError') return true
|
|
613
|
+
return false
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getFinalizedHeadHeader(headers: HttpResponse['headers']) {
|
|
617
|
+
let finalizedHeadHash = headers.get('X-Sqd-Finalized-Head-Hash')
|
|
618
|
+
let finalizedHeadNumber = headers.get('X-Sqd-Finalized-Head-Number')
|
|
619
|
+
|
|
620
|
+
return finalizedHeadHash != null && finalizedHeadNumber != null
|
|
621
|
+
? {
|
|
622
|
+
hash: finalizedHeadHash,
|
|
623
|
+
number: parseInt(finalizedHeadNumber),
|
|
624
|
+
}
|
|
625
|
+
: undefined
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function isStreamAbortedError(err: unknown) {
|
|
629
|
+
if (!(err instanceof Error)) return false
|
|
630
|
+
if (!('code' in err)) return false
|
|
631
|
+
switch (err.code) {
|
|
632
|
+
case 'ABORT_ERR':
|
|
633
|
+
case 'ERR_STREAM_PREMATURE_CLOSE':
|
|
634
|
+
case 'ECONNRESET':
|
|
635
|
+
return true
|
|
636
|
+
default:
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @example '0x0123456789abcdef'
|
|
3
|
+
*/
|
|
4
|
+
export type Hex = string & {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @example '123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
8
|
+
*/
|
|
9
|
+
export type Base58 = string & {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @example '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/='
|
|
13
|
+
*/
|
|
14
|
+
export type Base64 = string & {}
|
|
15
|
+
|
|
16
|
+
export type Simplify<T> = {[K in keyof T]: T[K]} & {}
|
|
17
|
+
|
|
18
|
+
export type Distribute<T> = T extends any ? T : never
|
|
19
|
+
|
|
20
|
+
export type ConditionalKeys<T, V> = {
|
|
21
|
+
[Key in keyof T]-?: T[Key] extends V ? (T[Key] extends never ? (V extends never ? Key : never) : Key) : never
|
|
22
|
+
}[keyof T]
|
|
23
|
+
|
|
24
|
+
export type ConditionalPick<T, V> = Simplify<Pick<T, ConditionalKeys<T, V>>>
|
|
25
|
+
|
|
26
|
+
export type ConditionalOmit<T, V> = Simplify<Omit<T, ConditionalKeys<T, V>>>
|
|
27
|
+
|
|
28
|
+
export type Selector<Props extends string | number | symbol = string> = {
|
|
29
|
+
[P in Props]?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Select<T, S> = S extends never
|
|
33
|
+
? never
|
|
34
|
+
: {
|
|
35
|
+
[K in keyof T as K extends keyof S ? (S[K] extends true ? K : never) : never]: T[K]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Selection = {
|
|
39
|
+
[P in string]?: boolean | Selection
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Trues<T extends Selection> = Simplify<{
|
|
43
|
+
[K in keyof T]-?: [T[K] & {}] extends [Selection] ? Trues<T[K] & {}> : true
|
|
44
|
+
}>
|
|
45
|
+
|
|
46
|
+
export type PortalQuery = {
|
|
47
|
+
type: string
|
|
48
|
+
fromBlock?: number
|
|
49
|
+
toBlock?: number
|
|
50
|
+
parentBlockHash?: string
|
|
51
|
+
[key: string]: unknown
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type PortalBlock = {
|
|
55
|
+
header: {
|
|
56
|
+
number: number
|
|
57
|
+
hash: string
|
|
58
|
+
}
|
|
59
|
+
}
|