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