@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/README.md +143 -0
- package/dist/index.cjs +1196 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +490 -0
- package/dist/index.d.ts +490 -0
- package/dist/index.mjs +1170 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
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
|