@thru/replay 0.1.36

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/dist/index.mjs ADDED
@@ -0,0 +1,1170 @@
1
+ import { create } from '@bufbuild/protobuf';
2
+ import { createClient } from '@connectrpc/connect';
3
+ import { createGrpcTransport } from '@connectrpc/connect-node';
4
+ import { QueryService, StreamingService, ListBlocksRequestSchema, StreamBlocksRequestSchema, ListTransactionsRequestSchema, StreamTransactionsRequestSchema, ListEventsRequestSchema, StreamEventsRequestSchema, StreamAccountUpdatesRequestSchema, GetHeightRequestSchema, AccountView, FilterParamValueSchema, FilterSchema, PageRequestSchema, EventSchema } from '@thru/proto';
5
+ export { AccountView, FilterParamValueSchema, FilterSchema } from '@thru/proto';
6
+
7
+ // src/chain-client.ts
8
+ var ChainClient = class {
9
+ constructor(options) {
10
+ this.options = options;
11
+ const transport = options.transport ?? this.createTransport();
12
+ this.query = createClient(QueryService, transport);
13
+ this.streaming = createClient(StreamingService, transport);
14
+ this.callOptions = options.callOptions;
15
+ }
16
+ query;
17
+ streaming;
18
+ callOptions;
19
+ listBlocks(request) {
20
+ return this.query.listBlocks(create(ListBlocksRequestSchema, request), this.callOptions);
21
+ }
22
+ streamBlocks(request) {
23
+ return this.streaming.streamBlocks(create(StreamBlocksRequestSchema, request), this.callOptions);
24
+ }
25
+ listTransactions(request) {
26
+ return this.query.listTransactions(create(ListTransactionsRequestSchema, request), this.callOptions);
27
+ }
28
+ streamTransactions(request) {
29
+ return this.streaming.streamTransactions(create(StreamTransactionsRequestSchema, request), this.callOptions);
30
+ }
31
+ listEvents(request) {
32
+ return this.query.listEvents(create(ListEventsRequestSchema, request), this.callOptions);
33
+ }
34
+ streamEvents(request) {
35
+ return this.streaming.streamEvents(create(StreamEventsRequestSchema, request), this.callOptions);
36
+ }
37
+ createTransport() {
38
+ if (!this.options.baseUrl) {
39
+ throw new Error("ChainClient requires baseUrl when no transport is provided");
40
+ }
41
+ const headerInterceptor = this.createHeaderInterceptor();
42
+ const userInterceptors = this.options.interceptors ?? [];
43
+ const mergedInterceptors = [
44
+ ...userInterceptors,
45
+ ...headerInterceptor ? [headerInterceptor] : []
46
+ ];
47
+ return createGrpcTransport({
48
+ baseUrl: this.options.baseUrl,
49
+ useBinaryFormat: this.options.useBinaryFormat ?? true,
50
+ interceptors: mergedInterceptors.length ? mergedInterceptors : void 0
51
+ });
52
+ }
53
+ createHeaderInterceptor() {
54
+ const headers = {};
55
+ if (this.options.apiKey) headers.Authorization = `Bearer ${this.options.apiKey}`;
56
+ if (this.options.userAgent) headers["User-Agent"] = this.options.userAgent;
57
+ if (!Object.keys(headers).length) return null;
58
+ return (next) => async (req) => {
59
+ for (const [key, value] of Object.entries(headers)) req.header.set(key, value);
60
+ return next(req);
61
+ };
62
+ }
63
+ streamAccountUpdates(request) {
64
+ return this.streaming.streamAccountUpdates(create(StreamAccountUpdatesRequestSchema, request), this.callOptions);
65
+ }
66
+ getHeight() {
67
+ return this.query.getHeight(create(GetHeightRequestSchema, {}), this.callOptions);
68
+ }
69
+ };
70
+
71
+ // src/async-queue.ts
72
+ var AsyncQueue = class {
73
+ values = [];
74
+ pending = [];
75
+ closed = false;
76
+ failure;
77
+ push(value) {
78
+ if (this.closed) throw new Error("Cannot push into a closed queue");
79
+ if (this.failure) throw this.failure;
80
+ const waiter = this.pending.shift();
81
+ if (waiter) waiter.resolve({ value, done: false });
82
+ else this.values.push(value);
83
+ }
84
+ next() {
85
+ if (this.failure) return Promise.reject(this.failure);
86
+ if (this.values.length) {
87
+ const value = this.values.shift();
88
+ return Promise.resolve({ value, done: false });
89
+ }
90
+ if (this.closed) return Promise.resolve({ value: void 0, done: true });
91
+ return new Promise((resolve, reject) => {
92
+ this.pending.push({ resolve, reject });
93
+ });
94
+ }
95
+ close() {
96
+ this.closed = true;
97
+ while (this.pending.length) {
98
+ const waiter = this.pending.shift();
99
+ waiter?.resolve({ value: void 0, done: true });
100
+ }
101
+ }
102
+ fail(err) {
103
+ this.failure = err ?? new Error("AsyncQueue failure");
104
+ while (this.pending.length) {
105
+ const waiter = this.pending.shift();
106
+ waiter?.reject(this.failure);
107
+ }
108
+ }
109
+ get isClosed() {
110
+ return this.closed;
111
+ }
112
+ [Symbol.asyncIterator]() {
113
+ return {
114
+ next: () => this.next()
115
+ };
116
+ }
117
+ };
118
+
119
+ // src/dedup-buffer.ts
120
+ var DedupBuffer = class {
121
+ slotOf;
122
+ keyOf;
123
+ slots = [];
124
+ itemsBySlot = /* @__PURE__ */ new Map();
125
+ sizeValue = 0;
126
+ constructor(options) {
127
+ this.slotOf = options.slotOf;
128
+ this.keyOf = options.keyOf;
129
+ }
130
+ insert(item) {
131
+ const slot = this.slotOf(item);
132
+ const key = this.keyOf(item);
133
+ let bucket = this.itemsBySlot.get(slot);
134
+ if (!bucket) {
135
+ bucket = /* @__PURE__ */ new Map();
136
+ this.itemsBySlot.set(slot, bucket);
137
+ const idx = this.findInsertIndex(slot);
138
+ this.slots.splice(idx, 0, slot);
139
+ }
140
+ if (bucket.has(key)) return false;
141
+ bucket.set(key, item);
142
+ this.sizeValue += 1;
143
+ return true;
144
+ }
145
+ discardUpTo(cutoff) {
146
+ let removed = 0;
147
+ while (this.slots.length && this.slots[0] <= cutoff) {
148
+ const slot = this.slots.shift();
149
+ const bucket = this.itemsBySlot.get(slot);
150
+ if (!bucket) continue;
151
+ removed += bucket.size;
152
+ this.itemsBySlot.delete(slot);
153
+ }
154
+ this.sizeValue = Math.max(0, this.sizeValue - removed);
155
+ return removed;
156
+ }
157
+ drainAbove(cutoff) {
158
+ if (!this.slots.length) return [];
159
+ const drained = [];
160
+ const keep = [];
161
+ for (const slot of this.slots) {
162
+ if (slot > cutoff) {
163
+ const bucket = this.itemsBySlot.get(slot);
164
+ if (bucket) {
165
+ for (const item of bucket.values()) drained.push(item);
166
+ this.itemsBySlot.delete(slot);
167
+ this.sizeValue -= bucket.size;
168
+ }
169
+ } else {
170
+ keep.push(slot);
171
+ }
172
+ }
173
+ this.slots.length = 0;
174
+ this.slots.push(...keep);
175
+ return drained;
176
+ }
177
+ drainAll() {
178
+ if (!this.slots.length) return [];
179
+ const drained = [];
180
+ for (const slot of this.slots) {
181
+ const bucket = this.itemsBySlot.get(slot);
182
+ if (!bucket) continue;
183
+ for (const item of bucket.values()) drained.push(item);
184
+ this.sizeValue -= bucket.size;
185
+ this.itemsBySlot.delete(slot);
186
+ }
187
+ this.slots.length = 0;
188
+ return drained;
189
+ }
190
+ minSlot() {
191
+ return this.slots.length ? this.slots[0] : null;
192
+ }
193
+ get size() {
194
+ return this.sizeValue;
195
+ }
196
+ findInsertIndex(slot) {
197
+ let low = 0;
198
+ let high = this.slots.length;
199
+ while (low < high) {
200
+ const mid = low + high >> 1;
201
+ if (this.slots[mid] < slot) low = mid + 1;
202
+ else high = mid;
203
+ }
204
+ return low;
205
+ }
206
+ };
207
+
208
+ // src/logger.ts
209
+ var noop = () => void 0;
210
+ var NOOP_LOGGER = {
211
+ debug: noop,
212
+ info: noop,
213
+ warn: noop,
214
+ error: noop
215
+ };
216
+ function createConsoleLogger(prefix = "Replay") {
217
+ return {
218
+ debug: (message, meta) => console.debug(`[${prefix}] ${message}`, meta ?? ""),
219
+ info: (message, meta) => console.info(`[${prefix}] ${message}`, meta ?? ""),
220
+ warn: (message, meta) => console.warn(`[${prefix}] ${message}`, meta ?? ""),
221
+ error: (message, meta) => console.error(`[${prefix}] ${message}`, meta ?? "")
222
+ };
223
+ }
224
+
225
+ // src/live-pump.ts
226
+ var LivePump = class {
227
+ queue = new AsyncQueue();
228
+ buffer;
229
+ slotOf;
230
+ keyOf;
231
+ source;
232
+ logger;
233
+ mode;
234
+ minSlotSeen = null;
235
+ maxSlotSeen = null;
236
+ minEmitSlot = null;
237
+ pumpPromise;
238
+ constructor(options) {
239
+ this.source = options.source;
240
+ this.slotOf = options.slotOf;
241
+ this.keyOf = options.keyOf ?? ((item) => options.slotOf(item).toString());
242
+ this.logger = options.logger ?? NOOP_LOGGER;
243
+ this.buffer = new DedupBuffer({ slotOf: this.slotOf, keyOf: this.keyOf });
244
+ this.mode = options.startInStreamingMode ? "streaming" : "buffering";
245
+ if (options.startInStreamingMode) this.minEmitSlot = options.initialEmitFloor ?? 0n;
246
+ this.pumpPromise = this.start();
247
+ }
248
+ minSlot() {
249
+ if (this.minSlotSeen !== null) return this.minSlotSeen;
250
+ return this.buffer.minSlot();
251
+ }
252
+ maxSlot() {
253
+ return this.maxSlotSeen;
254
+ }
255
+ bufferedSize() {
256
+ return this.buffer.size;
257
+ }
258
+ discardBufferedUpTo(cutoffSlot) {
259
+ if (this.mode === "streaming") return 0;
260
+ const discarded = this.buffer.discardUpTo(cutoffSlot);
261
+ if (discarded) {
262
+ this.logger.debug(
263
+ `discarded ${discarded} buffered items up to cutoff slot ${cutoffSlot}`
264
+ );
265
+ }
266
+ return discarded;
267
+ }
268
+ enableStreaming(cutoffSlot) {
269
+ if (this.mode === "streaming") return { drained: [], discarded: 0 };
270
+ const discarded = this.discardBufferedUpTo(cutoffSlot);
271
+ const drained = this.buffer.drainAbove(cutoffSlot);
272
+ this.mode = "streaming";
273
+ this.minEmitSlot = cutoffSlot;
274
+ return { drained, discarded };
275
+ }
276
+ updateEmitFloor(slot) {
277
+ this.minEmitSlot = slot;
278
+ }
279
+ async next() {
280
+ return this.queue.next();
281
+ }
282
+ async close() {
283
+ this.queue.close();
284
+ await this.pumpPromise;
285
+ }
286
+ async start() {
287
+ try {
288
+ for await (const item of this.source) {
289
+ const slot = this.slotOf(item);
290
+ if (this.minSlotSeen === null || slot < this.minSlotSeen) this.minSlotSeen = slot;
291
+ if (this.maxSlotSeen === null || slot > this.maxSlotSeen) this.maxSlotSeen = slot;
292
+ if (this.mode === "buffering") this.buffer.insert(item);
293
+ else {
294
+ if (this.minEmitSlot !== null && slot < this.minEmitSlot) continue;
295
+ this.queue.push(item);
296
+ }
297
+ }
298
+ this.queue.close();
299
+ } catch (err) {
300
+ this.queue.fail(err);
301
+ }
302
+ }
303
+ };
304
+
305
+ // src/replay-stream.ts
306
+ var DEFAULT_METRICS = {
307
+ bufferedItems: 0,
308
+ emittedBackfill: 0,
309
+ emittedLive: 0,
310
+ discardedDuplicates: 0
311
+ };
312
+ function compareBigint(a, b) {
313
+ if (a === b) return 0;
314
+ return a < b ? -1 : 1;
315
+ }
316
+ var RETRY_DELAY_MS = 1e3;
317
+ var ReplayStream = class {
318
+ config;
319
+ logger;
320
+ metrics = { ...DEFAULT_METRICS };
321
+ constructor(config) {
322
+ this.config = config;
323
+ this.logger = config.logger ?? NOOP_LOGGER;
324
+ }
325
+ getMetrics() {
326
+ return { ...this.metrics };
327
+ }
328
+ [Symbol.asyncIterator]() {
329
+ return this.run();
330
+ }
331
+ async *run() {
332
+ const {
333
+ startSlot,
334
+ fetchBackfill,
335
+ subscribeLive,
336
+ extractSlot,
337
+ extractKey,
338
+ safetyMargin,
339
+ resubscribeOnEnd
340
+ } = this.config;
341
+ const shouldResubscribeOnEnd = resubscribeOnEnd ?? true;
342
+ const keyOf = extractKey ?? ((item) => extractSlot(item).toString());
343
+ const createLivePump = (slot, startStreaming = false, emitFloor) => new LivePump({
344
+ source: subscribeLive(slot),
345
+ slotOf: extractSlot,
346
+ keyOf,
347
+ logger: this.logger,
348
+ startInStreamingMode: startStreaming,
349
+ initialEmitFloor: emitFloor
350
+ });
351
+ let livePump = createLivePump(startSlot);
352
+ let cursor;
353
+ let backfillDone = false;
354
+ let currentSlot = startSlot > 0n ? startSlot - 1n : 0n;
355
+ let lastEmittedSlot = null;
356
+ let lastSlotKeys = /* @__PURE__ */ new Set();
357
+ const seenItem = (slot, key) => {
358
+ if (lastEmittedSlot === null) return false;
359
+ if (slot < lastEmittedSlot) return true;
360
+ if (slot > lastEmittedSlot) return false;
361
+ return lastSlotKeys.has(key);
362
+ };
363
+ const recordEmission = (slot, key) => {
364
+ if (lastEmittedSlot === null || slot !== lastEmittedSlot) {
365
+ lastEmittedSlot = slot;
366
+ lastSlotKeys = /* @__PURE__ */ new Set([key]);
367
+ } else {
368
+ lastSlotKeys.add(key);
369
+ }
370
+ };
371
+ this.logger.info(
372
+ `replay entering BACKFILLING state (startSlot=${startSlot}, safetyMargin=${safetyMargin})`
373
+ );
374
+ while (!backfillDone) {
375
+ const page = await fetchBackfill({ startSlot, cursor });
376
+ if (!page.items.length && !page.cursor && !page.done) {
377
+ this.logger.warn("empty backfill page without cursor; retrying");
378
+ continue;
379
+ }
380
+ const sorted = [...page.items].sort(
381
+ (a, b) => compareBigint(extractSlot(a), extractSlot(b))
382
+ );
383
+ for (const item of sorted) {
384
+ const slot = extractSlot(item);
385
+ const key = keyOf(item);
386
+ if (slot < startSlot) continue;
387
+ if (seenItem(slot, key)) {
388
+ this.metrics.discardedDuplicates += 1;
389
+ continue;
390
+ }
391
+ currentSlot = slot;
392
+ recordEmission(slot, key);
393
+ this.metrics.emittedBackfill += 1;
394
+ yield item;
395
+ }
396
+ const duplicatesTrimmed = livePump.discardBufferedUpTo(currentSlot);
397
+ this.metrics.discardedDuplicates += duplicatesTrimmed;
398
+ cursor = page.cursor;
399
+ const maxStreamSlot = livePump.maxSlot();
400
+ if (maxStreamSlot !== null) {
401
+ const catchUpSlot = maxStreamSlot > safetyMargin ? maxStreamSlot - safetyMargin : 0n;
402
+ if (currentSlot >= catchUpSlot) {
403
+ this.logger.info(
404
+ `replay reached SWITCHING threshold (currentSlot=${currentSlot}, maxStreamSlot=${maxStreamSlot}, catchUpSlot=${catchUpSlot})`
405
+ );
406
+ backfillDone = true;
407
+ }
408
+ }
409
+ if (page.done || cursor === void 0) backfillDone = true;
410
+ }
411
+ this.logger.info(`replay entering SWITCHING state (currentSlot=${currentSlot})`);
412
+ const { drained, discarded } = livePump.enableStreaming(currentSlot);
413
+ this.metrics.bufferedItems = drained.length;
414
+ this.metrics.discardedDuplicates += discarded;
415
+ for (const item of drained) {
416
+ const slot = extractSlot(item);
417
+ const key = keyOf(item);
418
+ if (seenItem(slot, key)) {
419
+ this.metrics.discardedDuplicates += 1;
420
+ continue;
421
+ }
422
+ currentSlot = slot;
423
+ recordEmission(slot, key);
424
+ this.metrics.emittedLive += 1;
425
+ yield item;
426
+ livePump.updateEmitFloor(currentSlot);
427
+ }
428
+ if (!drained.length) livePump.updateEmitFloor(currentSlot);
429
+ this.logger.info("replay entering STREAMING state");
430
+ while (true) {
431
+ try {
432
+ const next = await livePump.next();
433
+ if (next.done) {
434
+ if (!shouldResubscribeOnEnd) break;
435
+ throw new Error("stream ended");
436
+ }
437
+ const slot = extractSlot(next.value);
438
+ const key = keyOf(next.value);
439
+ if (seenItem(slot, key)) {
440
+ this.metrics.discardedDuplicates += 1;
441
+ continue;
442
+ }
443
+ currentSlot = slot;
444
+ recordEmission(slot, key);
445
+ this.metrics.emittedLive += 1;
446
+ yield next.value;
447
+ livePump.updateEmitFloor(currentSlot);
448
+ } catch (err) {
449
+ const errMsg = err instanceof Error ? err.message : String(err);
450
+ this.logger.warn(
451
+ `live stream disconnected (${errMsg}); reconnecting in ${RETRY_DELAY_MS}ms from slot ${currentSlot}`
452
+ );
453
+ await delay(RETRY_DELAY_MS);
454
+ await safeClose(livePump);
455
+ const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
456
+ livePump = createLivePump(resumeSlot, true, currentSlot);
457
+ }
458
+ }
459
+ }
460
+ };
461
+ function delay(ms) {
462
+ return new Promise((resolve) => setTimeout(resolve, ms));
463
+ }
464
+ async function safeClose(pump) {
465
+ try {
466
+ await pump.close();
467
+ } catch {
468
+ }
469
+ }
470
+ function combineFilters(base, user) {
471
+ if (!base && !user) return void 0;
472
+ if (!base) return user;
473
+ if (!user) return base;
474
+ const expressionParts = [];
475
+ if (base.expression) expressionParts.push(`(${base.expression})`);
476
+ if (user.expression) expressionParts.push(`(${user.expression})`);
477
+ return create(FilterSchema, {
478
+ expression: expressionParts.join(" && ") || void 0,
479
+ params: { ...base.params, ...user.params }
480
+ });
481
+ }
482
+ function slotLiteralFilter(fieldExpr, slot) {
483
+ return create(FilterSchema, {
484
+ expression: `${fieldExpr} >= uint(${slot.toString()})`
485
+ });
486
+ }
487
+ function backfillPage(items, page) {
488
+ const cursor = page?.nextPageToken ?? void 0;
489
+ return {
490
+ items,
491
+ cursor,
492
+ done: !cursor
493
+ };
494
+ }
495
+ async function* mapAsyncIterable(iterable, selector) {
496
+ for await (const value of iterable) {
497
+ const mapped = selector(value);
498
+ if (mapped !== void 0 && mapped !== null) yield mapped;
499
+ }
500
+ }
501
+
502
+ // src/replay/block-replay.ts
503
+ var DEFAULT_PAGE_SIZE = 128;
504
+ var DEFAULT_SAFETY_MARGIN = 32n;
505
+ var PAGE_ORDER_ASC = "slot asc";
506
+ function createBlockReplay(options) {
507
+ const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN;
508
+ const logger = options.logger ?? NOOP_LOGGER;
509
+ const fetchBackfill = async ({
510
+ startSlot,
511
+ cursor
512
+ }) => {
513
+ const page = create(PageRequestSchema, {
514
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE,
515
+ orderBy: PAGE_ORDER_ASC,
516
+ pageToken: cursor
517
+ });
518
+ const mergedFilter = combineFilters(slotLiteralFilter("block.header.slot", startSlot), options.filter);
519
+ logger.debug?.("block backfill request", {
520
+ startSlot: startSlot.toString(),
521
+ cursor,
522
+ pageSize: page.pageSize
523
+ });
524
+ let response;
525
+ try {
526
+ response = await options.client.listBlocks(
527
+ create(ListBlocksRequestSchema, {
528
+ filter: mergedFilter,
529
+ page,
530
+ view: options.view,
531
+ minConsensus: options.minConsensus
532
+ })
533
+ );
534
+ } catch (err) {
535
+ logger.error("block backfill request failed", {
536
+ startSlot: startSlot.toString(),
537
+ cursor,
538
+ err
539
+ });
540
+ throw err;
541
+ }
542
+ return backfillPage(response.blocks, response.page);
543
+ };
544
+ const subscribeLive = (startSlot) => {
545
+ const request = create(StreamBlocksRequestSchema, {
546
+ startSlot,
547
+ filter: options.filter,
548
+ view: options.view,
549
+ minConsensus: options.minConsensus
550
+ });
551
+ return mapAsyncIterable(
552
+ options.client.streamBlocks(request),
553
+ (resp) => resp.block
554
+ );
555
+ };
556
+ return new ReplayStream({
557
+ startSlot: options.startSlot,
558
+ safetyMargin,
559
+ fetchBackfill,
560
+ subscribeLive,
561
+ extractSlot: (block) => block.header?.slot ?? 0n,
562
+ logger: options.logger,
563
+ resubscribeOnEnd: options.resubscribeOnEnd
564
+ });
565
+ }
566
+ var DEFAULT_PAGE_SIZE2 = 256;
567
+ var DEFAULT_SAFETY_MARGIN2 = 64n;
568
+ var PAGE_ORDER_ASC2 = "slot asc";
569
+ function createTransactionReplay(options) {
570
+ const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN2;
571
+ const fetchBackfill = async ({
572
+ startSlot,
573
+ cursor
574
+ }) => {
575
+ const page = create(PageRequestSchema, {
576
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE2,
577
+ orderBy: PAGE_ORDER_ASC2,
578
+ pageToken: cursor
579
+ });
580
+ const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
581
+ const response = await options.client.listTransactions(
582
+ create(ListTransactionsRequestSchema, {
583
+ filter: mergedFilter,
584
+ page,
585
+ minConsensus: options.minConsensus,
586
+ returnEvents: options.returnEvents
587
+ })
588
+ );
589
+ return backfillPage(response.transactions, response.page);
590
+ };
591
+ const subscribeLive = (startSlot) => {
592
+ const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
593
+ const request = create(StreamTransactionsRequestSchema, {
594
+ filter: mergedFilter,
595
+ minConsensus: options.minConsensus
596
+ });
597
+ return mapAsyncIterable(
598
+ options.client.streamTransactions(request),
599
+ (resp) => resp.transaction
600
+ );
601
+ };
602
+ return new ReplayStream({
603
+ startSlot: options.startSlot,
604
+ safetyMargin,
605
+ fetchBackfill,
606
+ subscribeLive,
607
+ extractSlot: (tx) => tx.slot ?? 0n,
608
+ extractKey: transactionKey,
609
+ logger: options.logger,
610
+ resubscribeOnEnd: options.resubscribeOnEnd
611
+ });
612
+ }
613
+ function transactionKey(tx) {
614
+ const signatureBytes = tx.signature?.value;
615
+ if (signatureBytes && signatureBytes.length) return bytesToHex(signatureBytes);
616
+ const slotPart = tx.slot?.toString() ?? "0";
617
+ const offsetPart = tx.blockOffset?.toString() ?? "0";
618
+ return `${slotPart}:${offsetPart}`;
619
+ }
620
+ function bytesToHex(bytes) {
621
+ let hex = "";
622
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
623
+ return hex;
624
+ }
625
+ var DEFAULT_PAGE_SIZE3 = 512;
626
+ var DEFAULT_SAFETY_MARGIN3 = 64n;
627
+ var PAGE_ORDER_ASC3 = "slot asc";
628
+ function createEventReplay(options) {
629
+ const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN3;
630
+ const fetchBackfill = async ({
631
+ startSlot,
632
+ cursor
633
+ }) => {
634
+ const page = create(PageRequestSchema, {
635
+ pageSize: options.pageSize ?? DEFAULT_PAGE_SIZE3,
636
+ orderBy: PAGE_ORDER_ASC3,
637
+ pageToken: cursor
638
+ });
639
+ const baseFilter = slotLiteralFilter("event.slot", startSlot);
640
+ const mergedFilter = combineFilters(baseFilter, options.filter);
641
+ const response = await options.client.listEvents(
642
+ create(ListEventsRequestSchema, {
643
+ filter: mergedFilter,
644
+ page
645
+ })
646
+ );
647
+ return backfillPage(response.events, response.page);
648
+ };
649
+ const subscribeLive = (startSlot) => {
650
+ const mergedFilter = combineFilters(slotLiteralFilter("event.slot", startSlot), options.filter);
651
+ const request = create(StreamEventsRequestSchema, {
652
+ filter: mergedFilter
653
+ });
654
+ return mapAsyncIterable(
655
+ options.client.streamEvents(request),
656
+ (resp) => streamResponseToEvent(resp)
657
+ );
658
+ };
659
+ return new ReplayStream({
660
+ startSlot: options.startSlot,
661
+ safetyMargin,
662
+ fetchBackfill,
663
+ subscribeLive,
664
+ extractSlot: (event) => event.slot ?? 0n,
665
+ extractKey: eventKey,
666
+ logger: options.logger,
667
+ resubscribeOnEnd: options.resubscribeOnEnd
668
+ });
669
+ }
670
+ function streamResponseToEvent(resp) {
671
+ return create(EventSchema, {
672
+ eventId: resp.eventId,
673
+ transactionSignature: resp.signature,
674
+ program: resp.program,
675
+ payload: resp.payload,
676
+ slot: resp.slot,
677
+ callIdx: resp.callIdx,
678
+ timestamp: resp.timestamp
679
+ });
680
+ }
681
+ function eventKey(event) {
682
+ if (event.eventId) return event.eventId;
683
+ const slotPart = event.slot?.toString() ?? "0";
684
+ return `${slotPart}:${event.callIdx ?? 0}`;
685
+ }
686
+
687
+ // src/page-assembler.ts
688
+ var PAGE_SIZE = 4096;
689
+ function addressToKey(address) {
690
+ return Array.from(address).map((b) => b.toString(16).padStart(2, "0")).join("");
691
+ }
692
+ function calculatePageCount(dataSize) {
693
+ if (dataSize === 0) return 0;
694
+ return Math.ceil(dataSize / PAGE_SIZE);
695
+ }
696
+ var PageAssembler = class {
697
+ assemblyTimeout;
698
+ maxPendingPerAddress;
699
+ /**
700
+ * Pending updates keyed by address (hex) -> seq (string) -> PendingUpdate
701
+ */
702
+ pending = /* @__PURE__ */ new Map();
703
+ constructor(options = {}) {
704
+ this.assemblyTimeout = options.assemblyTimeout ?? 3e4;
705
+ this.maxPendingPerAddress = options.maxPendingPerAddress ?? 10;
706
+ }
707
+ /**
708
+ * Process an account update and return assembled account if complete.
709
+ *
710
+ * @param address - Account address bytes
711
+ * @param update - Account update from streaming response
712
+ * @returns Assembled account if all pages received, null otherwise
713
+ */
714
+ processUpdate(address, update) {
715
+ const addressKey = addressToKey(address);
716
+ if (update.delete) {
717
+ return {
718
+ address,
719
+ slot: BigInt(update.slot.toString()),
720
+ seq: update.meta?.seq ? BigInt(update.meta.seq.toString()) : 0n,
721
+ meta: update.meta,
722
+ data: new Uint8Array(0),
723
+ isDelete: true
724
+ };
725
+ }
726
+ if (!update.meta) {
727
+ return null;
728
+ }
729
+ const seq = BigInt(update.meta.seq.toString());
730
+ const seqKey = seq.toString();
731
+ const slot = BigInt(update.slot.toString());
732
+ let addressPending = this.pending.get(addressKey);
733
+ if (!addressPending) {
734
+ addressPending = /* @__PURE__ */ new Map();
735
+ this.pending.set(addressKey, addressPending);
736
+ }
737
+ let pendingUpdate = addressPending.get(seqKey);
738
+ if (!pendingUpdate) {
739
+ const expectedPageCount = calculatePageCount(update.meta.dataSize);
740
+ pendingUpdate = {
741
+ slot,
742
+ seq,
743
+ meta: update.meta,
744
+ pages: /* @__PURE__ */ new Map(),
745
+ expectedPageCount,
746
+ receivedAt: Date.now()
747
+ };
748
+ addressPending.set(seqKey, pendingUpdate);
749
+ this.evictOldPending(addressPending);
750
+ }
751
+ if (update.page) {
752
+ pendingUpdate.pages.set(update.page.pageIdx, {
753
+ pageIdx: update.page.pageIdx,
754
+ pageData: update.page.pageData
755
+ });
756
+ }
757
+ if (pendingUpdate.pages.size >= pendingUpdate.expectedPageCount) {
758
+ addressPending.delete(seqKey);
759
+ if (addressPending.size === 0) {
760
+ this.pending.delete(addressKey);
761
+ }
762
+ const data = this.assemblePages(pendingUpdate);
763
+ return {
764
+ address,
765
+ slot: pendingUpdate.slot,
766
+ seq: pendingUpdate.seq,
767
+ meta: pendingUpdate.meta,
768
+ data,
769
+ isDelete: false
770
+ };
771
+ }
772
+ return null;
773
+ }
774
+ /**
775
+ * Assemble complete data from buffered pages
776
+ */
777
+ assemblePages(pending) {
778
+ const totalSize = pending.meta.dataSize;
779
+ if (totalSize === 0 || pending.expectedPageCount === 0) {
780
+ return new Uint8Array(0);
781
+ }
782
+ const result = new Uint8Array(totalSize);
783
+ let offset = 0;
784
+ for (let i = 0; i < pending.expectedPageCount; i++) {
785
+ const page = pending.pages.get(i);
786
+ if (page) {
787
+ result.set(page.pageData, offset);
788
+ offset += page.pageData.length;
789
+ }
790
+ }
791
+ return result;
792
+ }
793
+ /**
794
+ * Evict old pending updates for an address if limit exceeded
795
+ */
796
+ evictOldPending(addressPending) {
797
+ if (addressPending.size <= this.maxPendingPerAddress) {
798
+ return;
799
+ }
800
+ const entries = Array.from(addressPending.entries());
801
+ entries.sort((a, b) => a[1].receivedAt - b[1].receivedAt);
802
+ const toEvict = entries.length - this.maxPendingPerAddress;
803
+ for (let i = 0; i < toEvict; i++) {
804
+ addressPending.delete(entries[i][0]);
805
+ }
806
+ }
807
+ /**
808
+ * Clean up expired pending assemblies.
809
+ * Call this periodically to prevent memory leaks.
810
+ */
811
+ cleanup() {
812
+ const now = Date.now();
813
+ let evicted = 0;
814
+ for (const [addressKey, addressPending] of this.pending.entries()) {
815
+ for (const [seqKey, pending] of addressPending.entries()) {
816
+ if (now - pending.receivedAt > this.assemblyTimeout) {
817
+ addressPending.delete(seqKey);
818
+ evicted++;
819
+ }
820
+ }
821
+ if (addressPending.size === 0) {
822
+ this.pending.delete(addressKey);
823
+ }
824
+ }
825
+ return evicted;
826
+ }
827
+ /**
828
+ * Get current pending count for debugging
829
+ */
830
+ getPendingCount() {
831
+ let count = 0;
832
+ for (const addressPending of this.pending.values()) {
833
+ count += addressPending.size;
834
+ }
835
+ return count;
836
+ }
837
+ /**
838
+ * Clear all pending assemblies
839
+ */
840
+ clear() {
841
+ this.pending.clear();
842
+ }
843
+ };
844
+
845
+ // src/account-replay.ts
846
+ function bytesToHex2(bytes) {
847
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
848
+ }
849
+ function snapshotToState(account) {
850
+ if (!account.address?.value || !account.meta) {
851
+ return null;
852
+ }
853
+ return {
854
+ address: account.address.value,
855
+ addressHex: bytesToHex2(account.address.value),
856
+ slot: account.versionContext?.slot ?? 0n,
857
+ seq: BigInt(account.meta.seq.toString()),
858
+ meta: account.meta,
859
+ data: account.data?.data ?? new Uint8Array(0),
860
+ isDelete: account.meta.flags?.isDeleted ?? false,
861
+ source: "snapshot"
862
+ };
863
+ }
864
+ function assembledToState(assembled) {
865
+ return {
866
+ address: assembled.address,
867
+ addressHex: bytesToHex2(assembled.address),
868
+ slot: assembled.slot,
869
+ seq: assembled.seq,
870
+ meta: assembled.meta,
871
+ data: assembled.data,
872
+ isDelete: assembled.isDelete,
873
+ source: "update"
874
+ };
875
+ }
876
+ async function* createAccountReplay(options) {
877
+ const {
878
+ client,
879
+ address,
880
+ view = AccountView.FULL,
881
+ filter,
882
+ pageAssemblerOptions,
883
+ cleanupInterval = 1e4
884
+ } = options;
885
+ const assembler = new PageAssembler(pageAssemblerOptions);
886
+ let cleanupTimer = null;
887
+ try {
888
+ cleanupTimer = setInterval(() => {
889
+ assembler.cleanup();
890
+ }, cleanupInterval);
891
+ const filterParams = {
892
+ address: create(FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(address) } })
893
+ };
894
+ if (filter?.params) {
895
+ for (const [key, value] of Object.entries(filter.params)) {
896
+ filterParams[key] = create(FilterParamValueSchema, value);
897
+ }
898
+ }
899
+ const request = {
900
+ view,
901
+ filter: create(FilterSchema, {
902
+ expression: filter?.expression ? `(snapshot.address.value == params.address) && (${filter.expression})` : "snapshot.address.value == params.address",
903
+ params: filterParams
904
+ })
905
+ };
906
+ const stream = client.streamAccountUpdates(request);
907
+ for await (const response of stream) {
908
+ const event = processResponse(response, address, assembler);
909
+ if (event) {
910
+ yield event;
911
+ }
912
+ }
913
+ } finally {
914
+ if (cleanupTimer) {
915
+ clearInterval(cleanupTimer);
916
+ }
917
+ assembler.clear();
918
+ }
919
+ }
920
+ function processResponse(response, address, assembler) {
921
+ switch (response.message.case) {
922
+ case "snapshot": {
923
+ const state = snapshotToState(response.message.value);
924
+ if (state) {
925
+ return { type: "account", account: state };
926
+ }
927
+ return null;
928
+ }
929
+ case "update": {
930
+ const assembled = assembler.processUpdate(address, response.message.value);
931
+ if (assembled) {
932
+ return { type: "account", account: assembledToState(assembled) };
933
+ }
934
+ return null;
935
+ }
936
+ case "finished": {
937
+ return {
938
+ type: "blockFinished",
939
+ block: { slot: BigInt(response.message.value.slot.toString()) }
940
+ };
941
+ }
942
+ default:
943
+ return null;
944
+ }
945
+ }
946
+ function processResponseMulti(response, assembler) {
947
+ switch (response.message.case) {
948
+ case "snapshot": {
949
+ const state = snapshotToState(response.message.value);
950
+ if (state) {
951
+ return { type: "account", account: state };
952
+ }
953
+ return null;
954
+ }
955
+ case "update": {
956
+ const update = response.message.value;
957
+ const address = update.address?.value;
958
+ if (!address) {
959
+ return null;
960
+ }
961
+ const assembled = assembler.processUpdate(address, update);
962
+ if (assembled) {
963
+ return { type: "account", account: assembledToState(assembled) };
964
+ }
965
+ return null;
966
+ }
967
+ case "finished": {
968
+ return {
969
+ type: "blockFinished",
970
+ block: { slot: BigInt(response.message.value.slot.toString()) }
971
+ };
972
+ }
973
+ default:
974
+ return null;
975
+ }
976
+ }
977
+ function buildOwnerFilter(owner, dataSizes, additionalFilter) {
978
+ let expression = "(has(snapshot.meta.owner) && snapshot.meta.owner.value == params.owner) || (has(account_update.meta.owner) && account_update.meta.owner.value == params.owner)";
979
+ if (dataSizes && dataSizes.length > 0) {
980
+ const sizeConditions = dataSizes.map((size) => `snapshot.meta.data_size == uint(${size}) || account_update.meta.data_size == uint(${size})`).join(" || ");
981
+ expression = `(${expression}) && (${sizeConditions})`;
982
+ }
983
+ if (additionalFilter?.expression) {
984
+ expression = `(${expression}) && (${additionalFilter.expression})`;
985
+ }
986
+ const params = {
987
+ owner: create(FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(owner) } })
988
+ };
989
+ if (additionalFilter?.params) {
990
+ for (const [key, value] of Object.entries(additionalFilter.params)) {
991
+ params[key] = create(FilterParamValueSchema, value);
992
+ }
993
+ }
994
+ return create(FilterSchema, { expression, params });
995
+ }
996
+ async function* createAccountsByOwnerReplay(options) {
997
+ const {
998
+ client,
999
+ owner,
1000
+ view = AccountView.FULL,
1001
+ dataSizes,
1002
+ filter,
1003
+ pageAssemblerOptions,
1004
+ cleanupInterval = 1e4
1005
+ } = options;
1006
+ const assembler = new PageAssembler(pageAssemblerOptions);
1007
+ let cleanupTimer = null;
1008
+ try {
1009
+ cleanupTimer = setInterval(() => {
1010
+ assembler.cleanup();
1011
+ }, cleanupInterval);
1012
+ const ownerFilter = buildOwnerFilter(owner, dataSizes, filter);
1013
+ const request = {
1014
+ view,
1015
+ filter: ownerFilter
1016
+ };
1017
+ const stream = client.streamAccountUpdates(request);
1018
+ for await (const response of stream) {
1019
+ const event = processResponseMulti(response, assembler);
1020
+ if (event) {
1021
+ yield event;
1022
+ }
1023
+ }
1024
+ } finally {
1025
+ if (cleanupTimer) {
1026
+ clearInterval(cleanupTimer);
1027
+ }
1028
+ assembler.clear();
1029
+ }
1030
+ }
1031
+ var AccountSeqTracker = class {
1032
+ seqs = /* @__PURE__ */ new Map();
1033
+ /**
1034
+ * Get the current sequence number for an address
1035
+ */
1036
+ getSeq(addressHex) {
1037
+ return this.seqs.get(addressHex);
1038
+ }
1039
+ /**
1040
+ * Check if an update should be applied (seq > current)
1041
+ */
1042
+ shouldApply(addressHex, seq) {
1043
+ const current = this.seqs.get(addressHex);
1044
+ return current === void 0 || seq > current;
1045
+ }
1046
+ /**
1047
+ * Update the sequence number for an address
1048
+ * Only updates if new seq is greater than current
1049
+ */
1050
+ update(addressHex, seq) {
1051
+ const current = this.seqs.get(addressHex);
1052
+ if (current === void 0 || seq > current) {
1053
+ this.seqs.set(addressHex, seq);
1054
+ return true;
1055
+ }
1056
+ return false;
1057
+ }
1058
+ /**
1059
+ * Remove tracking for an address
1060
+ */
1061
+ remove(addressHex) {
1062
+ this.seqs.delete(addressHex);
1063
+ }
1064
+ /**
1065
+ * Clear all tracking
1066
+ */
1067
+ clear() {
1068
+ this.seqs.clear();
1069
+ }
1070
+ /**
1071
+ * Get count of tracked addresses
1072
+ */
1073
+ size() {
1074
+ return this.seqs.size;
1075
+ }
1076
+ };
1077
+ var MultiAccountReplay = class {
1078
+ client;
1079
+ view;
1080
+ seqTracker;
1081
+ activeStreams = /* @__PURE__ */ new Map();
1082
+ constructor(options) {
1083
+ this.client = options.client;
1084
+ this.view = options.view ?? AccountView.FULL;
1085
+ this.seqTracker = new AccountSeqTracker();
1086
+ }
1087
+ /**
1088
+ * Add an account to stream updates for
1089
+ */
1090
+ async *addAccount(address) {
1091
+ const addressHex = bytesToHex2(address);
1092
+ if (this.activeStreams.has(addressHex)) {
1093
+ return;
1094
+ }
1095
+ const controller = new AbortController();
1096
+ this.activeStreams.set(addressHex, controller);
1097
+ try {
1098
+ for await (const event of createAccountReplay({
1099
+ client: this.client,
1100
+ address,
1101
+ view: this.view
1102
+ })) {
1103
+ if (event.type === "account") {
1104
+ if (this.seqTracker.shouldApply(event.account.addressHex, event.account.seq)) {
1105
+ this.seqTracker.update(event.account.addressHex, event.account.seq);
1106
+ yield event;
1107
+ }
1108
+ } else {
1109
+ yield event;
1110
+ }
1111
+ }
1112
+ } finally {
1113
+ this.activeStreams.delete(addressHex);
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Remove an account from streaming
1118
+ */
1119
+ removeAccount(address) {
1120
+ const addressHex = bytesToHex2(address);
1121
+ const controller = this.activeStreams.get(addressHex);
1122
+ if (controller) {
1123
+ controller.abort();
1124
+ this.activeStreams.delete(addressHex);
1125
+ }
1126
+ this.seqTracker.remove(addressHex);
1127
+ }
1128
+ /**
1129
+ * Get the current sequence number for an account
1130
+ */
1131
+ getSeq(addressHex) {
1132
+ return this.seqTracker.getSeq(addressHex);
1133
+ }
1134
+ /**
1135
+ * Stop all streams
1136
+ */
1137
+ stop() {
1138
+ for (const controller of this.activeStreams.values()) {
1139
+ controller.abort();
1140
+ }
1141
+ this.activeStreams.clear();
1142
+ this.seqTracker.clear();
1143
+ }
1144
+ };
1145
+
1146
+ // src/sinks/console.ts
1147
+ var ConsoleSink = class {
1148
+ constructor(prefix = "ReplaySink") {
1149
+ this.prefix = prefix;
1150
+ }
1151
+ open(meta) {
1152
+ const suffix = meta?.stream ? ` (${meta.stream})` : "";
1153
+ console.info(`${this.prefix}${suffix} opened`, meta?.label ?? "");
1154
+ }
1155
+ write(item, ctx) {
1156
+ const slotLabel = ctx.slot.toString();
1157
+ console.info(
1158
+ `${this.prefix} ${ctx.phase.toUpperCase()} slot=${slotLabel}`,
1159
+ item
1160
+ );
1161
+ }
1162
+ close(err) {
1163
+ if (err) console.warn(`${this.prefix} closing with error`, err);
1164
+ else console.info(`${this.prefix} closed`);
1165
+ }
1166
+ };
1167
+
1168
+ export { AccountSeqTracker, ChainClient, ConsoleSink, MultiAccountReplay, NOOP_LOGGER, PAGE_SIZE, PageAssembler, ReplayStream, createAccountReplay, createAccountsByOwnerReplay, createBlockReplay, createConsoleLogger, createEventReplay, createTransactionReplay };
1169
+ //# sourceMappingURL=index.mjs.map
1170
+ //# sourceMappingURL=index.mjs.map