@secondlayer/sdk 9.0.2 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -17
- package/README.md +98 -31
- package/dist/index.d.ts +234 -113
- package/dist/index.js +768 -436
- package/dist/index.js.map +19 -19
- package/dist/sinks/bun-sqlite.d.ts +18 -4
- package/dist/sinks/bun-sqlite.js +21 -20
- package/dist/sinks/bun-sqlite.js.map +5 -5
- package/dist/sinks/core.d.ts +30 -7
- package/dist/sinks/core.js +16 -19
- package/dist/sinks/core.js.map +4 -4
- package/dist/sinks/drizzle.d.ts +23 -6
- package/dist/sinks/drizzle.js +22 -21
- package/dist/sinks/drizzle.js.map +5 -5
- package/dist/sinks/kysely.d.ts +26 -11
- package/dist/sinks/kysely.js +23 -21
- package/dist/sinks/kysely.js.map +5 -5
- package/dist/sinks/testing.d.ts +4 -1
- package/dist/sinks/testing.js +14 -1
- package/dist/sinks/testing.js.map +3 -3
- package/dist/streams/index.d.ts +92 -23
- package/dist/streams/index.js +395 -183
- package/dist/streams/index.js.map +12 -12
- package/dist/subgraphs/index.d.ts +202 -94
- package/dist/subgraphs/index.js +764 -434
- package/dist/subgraphs/index.js.map +17 -17
- package/package.json +4 -4
package/dist/streams/index.js
CHANGED
|
@@ -88,8 +88,8 @@ class ApiError extends SecondLayerError {
|
|
|
88
88
|
|
|
89
89
|
class AuthError extends ApiError {
|
|
90
90
|
status = 401;
|
|
91
|
-
constructor(message = "API key invalid or expired.") {
|
|
92
|
-
super(401, message);
|
|
91
|
+
constructor(message = "API key invalid or expired.", body, code) {
|
|
92
|
+
super(401, message, body, code);
|
|
93
93
|
this.name = "AuthError";
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -97,8 +97,8 @@ class AuthError extends ApiError {
|
|
|
97
97
|
class RateLimitError extends ApiError {
|
|
98
98
|
retryAfter;
|
|
99
99
|
status = 429;
|
|
100
|
-
constructor(message = "Rate limited. Try again later.", retryAfter) {
|
|
101
|
-
super(429, message,
|
|
100
|
+
constructor(message = "Rate limited. Try again later.", retryAfter, body, code) {
|
|
101
|
+
super(429, message, body, code, {
|
|
102
102
|
retryable: true,
|
|
103
103
|
retryAfterSeconds: parseRetryAfter(retryAfter)
|
|
104
104
|
});
|
|
@@ -108,8 +108,8 @@ class RateLimitError extends ApiError {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
class ValidationError extends ApiError {
|
|
111
|
-
constructor(message, status, body) {
|
|
112
|
-
super(status, message, body,
|
|
111
|
+
constructor(message, status, body, code) {
|
|
112
|
+
super(status, message, body, code, { retryable: false });
|
|
113
113
|
this.name = "ValidationError";
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -125,18 +125,22 @@ function parseRetryAfter(value) {
|
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
class VersionConflictError extends ApiError {
|
|
129
|
-
currentVersion;
|
|
130
|
-
expectedVersion;
|
|
131
|
-
constructor(currentVersion, expectedVersion, message = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`) {
|
|
132
|
-
super(409, message, { currentVersion, expectedVersion });
|
|
133
|
-
this.currentVersion = currentVersion;
|
|
134
|
-
this.expectedVersion = expectedVersion;
|
|
135
|
-
this.name = "VersionConflictError";
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
128
|
// src/base.ts
|
|
129
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
130
|
+
function abortError() {
|
|
131
|
+
return new DOMException("This operation was aborted", "AbortError");
|
|
132
|
+
}
|
|
133
|
+
function raceAbort(promise, signal) {
|
|
134
|
+
if (signal.aborted)
|
|
135
|
+
return Promise.reject(signal.reason ?? abortError());
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const onAbort = () => reject(signal.reason ?? abortError());
|
|
138
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
139
|
+
promise.then(resolve, reject).finally(() => {
|
|
140
|
+
signal.removeEventListener("abort", onAbort);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
140
144
|
var LOCAL_API_URL = "http://127.0.0.1:3800";
|
|
141
145
|
function resolveBaseUrl(explicit) {
|
|
142
146
|
if (explicit && explicit.length > 0)
|
|
@@ -166,6 +170,9 @@ function resolveApiKey(apiKey) {
|
|
|
166
170
|
}
|
|
167
171
|
return instanceToken ?? legacyKey;
|
|
168
172
|
}
|
|
173
|
+
function seg(value) {
|
|
174
|
+
return encodeURIComponent(String(value));
|
|
175
|
+
}
|
|
169
176
|
function buildQuery(params) {
|
|
170
177
|
const search = new URLSearchParams;
|
|
171
178
|
for (const [name, value] of Object.entries(params)) {
|
|
@@ -179,16 +186,39 @@ function buildQuery(params) {
|
|
|
179
186
|
const query = search.toString();
|
|
180
187
|
return query ? `?${query}` : "";
|
|
181
188
|
}
|
|
189
|
+
function parseErrorEnvelope(text) {
|
|
190
|
+
if (text.length === 0)
|
|
191
|
+
return { body: undefined };
|
|
192
|
+
try {
|
|
193
|
+
const json = JSON.parse(text);
|
|
194
|
+
let message;
|
|
195
|
+
let code;
|
|
196
|
+
if (json && typeof json === "object") {
|
|
197
|
+
const err = json.error ?? json.message;
|
|
198
|
+
if (typeof err === "string" && err.length > 0)
|
|
199
|
+
message = err;
|
|
200
|
+
else if (err && typeof err === "object")
|
|
201
|
+
message = JSON.stringify(err);
|
|
202
|
+
if (typeof json.code === "string")
|
|
203
|
+
code = json.code;
|
|
204
|
+
}
|
|
205
|
+
return { message, code, body: json };
|
|
206
|
+
} catch {
|
|
207
|
+
return { message: text, body: text };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
182
210
|
|
|
183
211
|
class BaseClient {
|
|
184
212
|
baseUrl;
|
|
185
213
|
apiKey;
|
|
186
214
|
origin;
|
|
187
215
|
fetchImpl;
|
|
216
|
+
requestTimeoutMs;
|
|
188
217
|
constructor(options = {}) {
|
|
189
218
|
this.baseUrl = resolveBaseUrl(options.baseUrl);
|
|
190
219
|
this.apiKey = resolveApiKey(options.apiKey);
|
|
191
220
|
this.origin = options.origin ?? "cli";
|
|
221
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
192
222
|
this.fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
193
223
|
}
|
|
194
224
|
static authHeaders(apiKey) {
|
|
@@ -200,27 +230,56 @@ class BaseClient {
|
|
|
200
230
|
}
|
|
201
231
|
return headers;
|
|
202
232
|
}
|
|
203
|
-
async request(method, path, body) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
233
|
+
async request(method, path, body, opts = {}) {
|
|
234
|
+
return this.withRequestBudget(opts.signal, async (signal) => {
|
|
235
|
+
const response = await this.fetchResponse(method, path, body, signal);
|
|
236
|
+
if (response.status === 204) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
return await raceAbort(response.json(), signal);
|
|
240
|
+
});
|
|
209
241
|
}
|
|
210
|
-
async requestOrNull(method, path, body) {
|
|
242
|
+
async requestOrNull(method, path, body, opts = {}) {
|
|
211
243
|
try {
|
|
212
|
-
return await this.request(method, path, body);
|
|
244
|
+
return await this.request(method, path, body, opts);
|
|
213
245
|
} catch (err) {
|
|
214
246
|
if (err instanceof ApiError && err.status === 404)
|
|
215
247
|
return null;
|
|
216
248
|
throw err;
|
|
217
249
|
}
|
|
218
250
|
}
|
|
219
|
-
async requestText(method, path, body) {
|
|
220
|
-
|
|
221
|
-
|
|
251
|
+
async requestText(method, path, body, opts = {}) {
|
|
252
|
+
return this.withRequestBudget(opts.signal, async (signal) => {
|
|
253
|
+
const response = await this.fetchResponse(method, path, body, signal);
|
|
254
|
+
return raceAbort(response.text(), signal);
|
|
255
|
+
});
|
|
222
256
|
}
|
|
223
|
-
async
|
|
257
|
+
async withRequestBudget(callerSignal, run) {
|
|
258
|
+
const controller = new AbortController;
|
|
259
|
+
let timedOut = false;
|
|
260
|
+
const forward = () => controller.abort(callerSignal?.reason ?? abortError());
|
|
261
|
+
if (callerSignal?.aborted)
|
|
262
|
+
forward();
|
|
263
|
+
else
|
|
264
|
+
callerSignal?.addEventListener("abort", forward, { once: true });
|
|
265
|
+
const timer = this.requestTimeoutMs > 0 ? setTimeout(() => {
|
|
266
|
+
timedOut = true;
|
|
267
|
+
controller.abort(abortError());
|
|
268
|
+
}, this.requestTimeoutMs) : undefined;
|
|
269
|
+
try {
|
|
270
|
+
return await run(controller.signal);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (timedOut && !callerSignal?.aborted) {
|
|
273
|
+
throw new ApiError(0, `No response from ${this.baseUrl} within ${this.requestTimeoutMs}ms. Retryable. Raise requestTimeoutMs for slow filtered reads.`, undefined, "REQUEST_TIMEOUT", { retryable: true, cause: err instanceof Error ? err : undefined });
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
} finally {
|
|
277
|
+
if (timer !== undefined)
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
callerSignal?.removeEventListener("abort", forward);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async fetchResponse(method, path, body, signal) {
|
|
224
283
|
const url = `${this.baseUrl}${path}`;
|
|
225
284
|
const headers = BaseClient.authHeaders(this.apiKey);
|
|
226
285
|
headers["x-sl-origin"] = this.origin;
|
|
@@ -235,47 +294,34 @@ class BaseClient {
|
|
|
235
294
|
}
|
|
236
295
|
let response;
|
|
237
296
|
try {
|
|
238
|
-
|
|
297
|
+
const pending = this.fetchImpl(url, {
|
|
239
298
|
method,
|
|
240
299
|
headers,
|
|
241
|
-
body: serializedBody
|
|
300
|
+
body: serializedBody,
|
|
301
|
+
signal
|
|
242
302
|
});
|
|
303
|
+
response = signal ? await raceAbort(pending, signal) : await pending;
|
|
243
304
|
} catch (err) {
|
|
305
|
+
if (signal?.aborted)
|
|
306
|
+
throw err;
|
|
244
307
|
throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`, undefined, undefined, { retryable: true, cause: err instanceof Error ? err : undefined });
|
|
245
308
|
}
|
|
246
309
|
if (!response.ok) {
|
|
310
|
+
const errorBody = signal ? await raceAbort(response.text(), signal) : await response.text();
|
|
311
|
+
const envelope = parseErrorEnvelope(errorBody);
|
|
312
|
+
const retryAfter = response.headers.get("Retry-After") ?? undefined;
|
|
247
313
|
if (response.status === 401) {
|
|
248
|
-
throw new AuthError;
|
|
314
|
+
throw new AuthError(envelope.message ?? "API key invalid or expired.", envelope.body, envelope.code);
|
|
249
315
|
}
|
|
250
316
|
if (response.status === 429) {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
throw new RateLimitError(msg, retryAfter ?? undefined);
|
|
317
|
+
const fallback = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
|
|
318
|
+
throw new RateLimitError(envelope.message ?? fallback, retryAfter, envelope.body, envelope.code);
|
|
254
319
|
}
|
|
255
320
|
if (response.status >= 500) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const errorBody = await response.text();
|
|
259
|
-
let message = `HTTP ${response.status}`;
|
|
260
|
-
let parsedBody = errorBody;
|
|
261
|
-
let code;
|
|
262
|
-
try {
|
|
263
|
-
const json = JSON.parse(errorBody);
|
|
264
|
-
parsedBody = json;
|
|
265
|
-
const err = json.error ?? json.message;
|
|
266
|
-
if (typeof err === "string") {
|
|
267
|
-
message = err;
|
|
268
|
-
} else if (err && typeof err === "object") {
|
|
269
|
-
message = JSON.stringify(err);
|
|
270
|
-
}
|
|
271
|
-
if (typeof json.code === "string") {
|
|
272
|
-
code = json.code;
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
if (errorBody)
|
|
276
|
-
message = errorBody;
|
|
321
|
+
const retryAfterSeconds = parseRetryAfter(retryAfter);
|
|
322
|
+
throw new ApiError(response.status, envelope.message ?? `Server error. Try again or check status at ${this.baseUrl}/health`, envelope.body, envelope.code, retryAfterSeconds !== undefined ? { retryAfterSeconds } : {});
|
|
277
323
|
}
|
|
278
|
-
throw new ApiError(response.status, message
|
|
324
|
+
throw new ApiError(response.status, envelope.message ?? `HTTP ${response.status}`, envelope.body, envelope.code);
|
|
279
325
|
}
|
|
280
326
|
return response;
|
|
281
327
|
}
|
|
@@ -290,8 +336,8 @@ import {
|
|
|
290
336
|
|
|
291
337
|
// src/streams/errors.ts
|
|
292
338
|
class StreamsServerError extends ApiError {
|
|
293
|
-
constructor(message, status, body) {
|
|
294
|
-
super(status, message, body,
|
|
339
|
+
constructor(message, status, body, code) {
|
|
340
|
+
super(status, message, body, code, { retryable: true });
|
|
295
341
|
this.name = "StreamsServerError";
|
|
296
342
|
}
|
|
297
343
|
}
|
|
@@ -302,13 +348,26 @@ class StreamsSignatureError extends SecondLayerError {
|
|
|
302
348
|
this.name = "StreamsSignatureError";
|
|
303
349
|
}
|
|
304
350
|
}
|
|
351
|
+
async function mapStreamsError(response) {
|
|
352
|
+
const { message, code, body } = parseErrorEnvelope(await response.text());
|
|
353
|
+
if (response.status === 401) {
|
|
354
|
+
throw new AuthError(message ?? "API key invalid or expired.", body, code);
|
|
355
|
+
}
|
|
356
|
+
if (response.status === 429) {
|
|
357
|
+
const retryAfter = response.headers.get("Retry-After") ?? undefined;
|
|
358
|
+
throw new RateLimitError(message ?? "Rate limited. Try again later.", retryAfter, body, code);
|
|
359
|
+
}
|
|
360
|
+
if (response.status >= 500) {
|
|
361
|
+
throw new StreamsServerError(message ?? `Streams server returned ${response.status}.`, response.status, body, code);
|
|
362
|
+
}
|
|
363
|
+
throw new ValidationError(message ?? `Streams request returned ${response.status}.`, response.status, body, code);
|
|
364
|
+
}
|
|
305
365
|
|
|
306
366
|
// src/streams/cursor.ts
|
|
307
367
|
var Cursor = {
|
|
308
368
|
atHeight(height) {
|
|
309
|
-
if (height <= 0)
|
|
310
|
-
return
|
|
311
|
-
}
|
|
369
|
+
if (height <= 0)
|
|
370
|
+
return null;
|
|
312
371
|
return encodeStreamsCursor(blockEndCursor(height - 1));
|
|
313
372
|
},
|
|
314
373
|
parse(cursor) {
|
|
@@ -328,6 +387,39 @@ var Cursor = {
|
|
|
328
387
|
function reorgKey(reorg) {
|
|
329
388
|
return `${reorg.detected_at}|${reorg.fork_point_height}|${reorg.new_canonical_tip}`;
|
|
330
389
|
}
|
|
390
|
+
var DEFAULT_MAX_ROLLBACK_DEPTH = 1000;
|
|
391
|
+
async function applyReorgs(opts) {
|
|
392
|
+
const fresh = opts.reorgs.filter((reorg) => !opts.handled.has(opts.keyOf(reorg)));
|
|
393
|
+
if (fresh.length === 0)
|
|
394
|
+
return null;
|
|
395
|
+
for (const reorg of fresh) {
|
|
396
|
+
const fork = reorg.fork_point_height;
|
|
397
|
+
if (!Number.isSafeInteger(fork) || fork < 0) {
|
|
398
|
+
throw new ValidationError(`Reorg fork_point_height ${String(fork)} is not a non-negative integer. Rollback deletes every row at or above the fork from every declared table, so a malformed fork point is refused before the sink runs.`, 400);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const position = resumeHeight(opts.cursor);
|
|
402
|
+
const applicable = fresh.filter((reorg) => position !== null && reorg.fork_point_height <= position).sort((a, b) => a.fork_point_height - b.fork_point_height);
|
|
403
|
+
for (const reorg of fresh) {
|
|
404
|
+
if (!applicable.includes(reorg))
|
|
405
|
+
opts.handled.add(opts.keyOf(reorg));
|
|
406
|
+
}
|
|
407
|
+
if (applicable.length === 0 || position === null)
|
|
408
|
+
return null;
|
|
409
|
+
const forkPoint = applicable[0]?.fork_point_height ?? position;
|
|
410
|
+
const depth = position - forkPoint;
|
|
411
|
+
const maxDepth = opts.maxRollbackDepth ?? DEFAULT_MAX_ROLLBACK_DEPTH;
|
|
412
|
+
if (depth > maxDepth) {
|
|
413
|
+
throw new ValidationError(`Reorg at fork ${forkPoint} rewinds ${depth} blocks below the checkpoint at height ${position}, past maxRollbackDepth (${maxDepth}). A rollback this deep deletes every row at or above the fork from every declared table. Confirm the source is trusted, then pass maxRollbackDepth: ${depth} or higher to allow it.`, 400);
|
|
414
|
+
}
|
|
415
|
+
const rewind = Cursor.atHeight(forkPoint);
|
|
416
|
+
for (const reorg of applicable) {
|
|
417
|
+
await opts.sink?.rollback(reorg.fork_point_height, rewind);
|
|
418
|
+
await opts.onReorg?.(reorg, { cursor: rewind });
|
|
419
|
+
opts.handled.add(opts.keyOf(reorg));
|
|
420
|
+
}
|
|
421
|
+
return { forkPoint, cursor: rewind };
|
|
422
|
+
}
|
|
331
423
|
function assertFinalizedCheckpoint(returned, checkpoint) {
|
|
332
424
|
if (returned === null || returned === undefined)
|
|
333
425
|
return;
|
|
@@ -407,13 +499,15 @@ async function defaultSleep(ms, signal) {
|
|
|
407
499
|
if (signal?.aborted)
|
|
408
500
|
return;
|
|
409
501
|
await new Promise((resolve) => {
|
|
410
|
-
const
|
|
411
|
-
if (!signal)
|
|
412
|
-
return;
|
|
413
|
-
signal.addEventListener("abort", () => {
|
|
502
|
+
const onAbort = () => {
|
|
414
503
|
clearTimeout(timeout);
|
|
415
504
|
resolve();
|
|
416
|
-
}
|
|
505
|
+
};
|
|
506
|
+
const timeout = setTimeout(() => {
|
|
507
|
+
signal?.removeEventListener("abort", onAbort);
|
|
508
|
+
resolve();
|
|
509
|
+
}, ms);
|
|
510
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
417
511
|
});
|
|
418
512
|
}
|
|
419
513
|
async function consumeStreamsEvents(opts) {
|
|
@@ -427,6 +521,7 @@ async function consumeStreamsEvents(opts) {
|
|
|
427
521
|
const committedCursor = await opts.sink?.loadCursor() ?? null;
|
|
428
522
|
let cursor = opts.fromCursor ?? committedCursor;
|
|
429
523
|
const handledReorgs = new Set;
|
|
524
|
+
let reorgSince = null;
|
|
430
525
|
let pages = 0;
|
|
431
526
|
let emptyPolls = 0;
|
|
432
527
|
let height = resumeHeight(cursor);
|
|
@@ -450,23 +545,57 @@ async function consumeStreamsEvents(opts) {
|
|
|
450
545
|
signal: opts.signal
|
|
451
546
|
});
|
|
452
547
|
pages++;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
548
|
+
const reorgsOn = !finalizedOnly && Boolean(opts.onReorg || opts.sink);
|
|
549
|
+
if (reorgsOn) {
|
|
550
|
+
const rewound = await applyReorgs({
|
|
551
|
+
reorgs: envelope.reorgs,
|
|
552
|
+
cursor,
|
|
553
|
+
handled: handledReorgs,
|
|
554
|
+
keyOf: reorgKey,
|
|
555
|
+
sink: opts.sink,
|
|
556
|
+
onReorg: opts.onReorg,
|
|
557
|
+
maxRollbackDepth: opts.maxRollbackDepth
|
|
558
|
+
});
|
|
559
|
+
if (rewound) {
|
|
560
|
+
cursor = rewound.cursor;
|
|
561
|
+
height = rewound.forkPoint > 0 ? rewound.forkPoint - 1 : null;
|
|
465
562
|
scanned = height;
|
|
466
563
|
emptyPolls = 0;
|
|
467
564
|
continue;
|
|
468
565
|
}
|
|
469
566
|
}
|
|
567
|
+
if (reorgsOn && opts.fetchReorgs && envelope.events.length === 0) {
|
|
568
|
+
reorgSince ??= cursor;
|
|
569
|
+
if (reorgSince !== null) {
|
|
570
|
+
const since = reorgSince;
|
|
571
|
+
const fetchReorgs = opts.fetchReorgs;
|
|
572
|
+
const listed = await fetchPageWithRetry(() => fetchReorgs({ since }), {
|
|
573
|
+
retryCount: opts.retryCount,
|
|
574
|
+
retryDelay: opts.retryDelay,
|
|
575
|
+
onError: opts.onError,
|
|
576
|
+
sleep,
|
|
577
|
+
signal: opts.signal
|
|
578
|
+
});
|
|
579
|
+
if (listed.next_since !== null)
|
|
580
|
+
reorgSince = listed.next_since;
|
|
581
|
+
const rewound = await applyReorgs({
|
|
582
|
+
reorgs: listed.reorgs,
|
|
583
|
+
cursor,
|
|
584
|
+
handled: handledReorgs,
|
|
585
|
+
keyOf: reorgKey,
|
|
586
|
+
sink: opts.sink,
|
|
587
|
+
onReorg: opts.onReorg,
|
|
588
|
+
maxRollbackDepth: opts.maxRollbackDepth
|
|
589
|
+
});
|
|
590
|
+
if (rewound) {
|
|
591
|
+
cursor = rewound.cursor;
|
|
592
|
+
height = rewound.forkPoint > 0 ? rewound.forkPoint - 1 : null;
|
|
593
|
+
scanned = height;
|
|
594
|
+
emptyPolls = 0;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
470
599
|
const emitted = finalizedOnly ? envelope.events.filter((event) => event.finalized) : envelope.events;
|
|
471
600
|
const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
|
|
472
601
|
height = emitted.at(-1)?.block_height ?? height;
|
|
@@ -744,15 +873,46 @@ function createStreamsDumps(opts) {
|
|
|
744
873
|
return manifest;
|
|
745
874
|
}
|
|
746
875
|
async function download(file) {
|
|
876
|
+
return fetchPageWithRetry(() => downloadOnce(file), {
|
|
877
|
+
sleep: defaultSleep
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function downloadOnce(file) {
|
|
747
881
|
const res = await opts.fetchImpl(fileUrl(file));
|
|
748
882
|
if (!res.ok) {
|
|
749
883
|
throw new StreamsServerError(`Could not download dump ${file.path} (${res.status}).`, res.status);
|
|
750
884
|
}
|
|
751
|
-
|
|
752
|
-
|
|
885
|
+
if (!res.body) {
|
|
886
|
+
throw new StreamsServerError(`Dump ${file.path} response has no body.`, 0);
|
|
887
|
+
}
|
|
888
|
+
const hash = createHash("sha256");
|
|
889
|
+
const chunks = [];
|
|
890
|
+
let total = 0;
|
|
891
|
+
const reader = res.body.getReader();
|
|
892
|
+
try {
|
|
893
|
+
while (true) {
|
|
894
|
+
const { value, done } = await reader.read();
|
|
895
|
+
if (done)
|
|
896
|
+
break;
|
|
897
|
+
hash.update(value);
|
|
898
|
+
chunks.push(value);
|
|
899
|
+
total += value.byteLength;
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
throw new StreamsServerError(`Download of dump ${file.path} was interrupted after ${total} bytes.`, 0);
|
|
903
|
+
}
|
|
904
|
+
const digest = hash.digest("hex");
|
|
753
905
|
if (digest !== file.sha256) {
|
|
754
906
|
throw new StreamsSignatureError(`Dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}).`);
|
|
755
907
|
}
|
|
908
|
+
if (chunks.length === 1 && chunks[0])
|
|
909
|
+
return chunks[0];
|
|
910
|
+
const bytes = new Uint8Array(total);
|
|
911
|
+
let offset = 0;
|
|
912
|
+
for (const chunk of chunks) {
|
|
913
|
+
bytes.set(chunk, offset);
|
|
914
|
+
offset += chunk.byteLength;
|
|
915
|
+
}
|
|
756
916
|
return bytes;
|
|
757
917
|
}
|
|
758
918
|
return { list, fileUrl, download };
|
|
@@ -760,6 +920,19 @@ function createStreamsDumps(opts) {
|
|
|
760
920
|
|
|
761
921
|
// src/streams/subscribe.ts
|
|
762
922
|
import { ed25519 } from "@secondlayer/shared";
|
|
923
|
+
var SUBSCRIBE_MAX_RECONNECT_DELAY_MS = 30000;
|
|
924
|
+
var SUBSCRIBE_DEFAULT_STALE_AFTER_MS = 60000;
|
|
925
|
+
function reconnectDelay(attempt, baseMs, retryAfterSeconds, random = Math.random) {
|
|
926
|
+
const capped = Math.min(baseMs * 2 ** Math.max(0, attempt), SUBSCRIBE_MAX_RECONNECT_DELAY_MS);
|
|
927
|
+
const jittered = capped * (0.5 + random() * 0.5);
|
|
928
|
+
const floor = (retryAfterSeconds ?? 0) * 1000;
|
|
929
|
+
return Math.max(jittered, floor);
|
|
930
|
+
}
|
|
931
|
+
function isTerminal(err) {
|
|
932
|
+
if (err instanceof StreamsSignatureError)
|
|
933
|
+
return true;
|
|
934
|
+
return err?.retryable === false;
|
|
935
|
+
}
|
|
763
936
|
function subscribeStreamsEvents(opts) {
|
|
764
937
|
const { params } = opts;
|
|
765
938
|
const controller = new AbortController;
|
|
@@ -773,7 +946,9 @@ function subscribeStreamsEvents(opts) {
|
|
|
773
946
|
});
|
|
774
947
|
}
|
|
775
948
|
let cursor = params.fromCursor ?? null;
|
|
776
|
-
const
|
|
949
|
+
const baseDelayMs = opts.reconnectDelayMs ?? params.reconnectDelayMs ?? 1000;
|
|
950
|
+
const staleAfterMs = opts.staleAfterMs ?? params.staleAfterMs ?? SUBSCRIBE_DEFAULT_STALE_AFTER_MS;
|
|
951
|
+
let attempt = 0;
|
|
777
952
|
const run = async () => {
|
|
778
953
|
while (!controller.signal.aborted) {
|
|
779
954
|
try {
|
|
@@ -787,55 +962,76 @@ function subscribeStreamsEvents(opts) {
|
|
|
787
962
|
asset_identifier: params.assetIdentifier,
|
|
788
963
|
filters: params.filters ? JSON.stringify(params.filters) : undefined
|
|
789
964
|
})}`;
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
} else {
|
|
820
|
-
const key = await opts.loadKey();
|
|
821
|
-
if (!ed25519.verifyEd25519(JSON.stringify(parsed.event), parsed.sig, key.publicKey)) {
|
|
822
|
-
throw new StreamsSignatureError("Streams SSE frame signature is invalid.");
|
|
965
|
+
await readSse({
|
|
966
|
+
url,
|
|
967
|
+
headers: opts.headers ?? {},
|
|
968
|
+
signal: controller.signal,
|
|
969
|
+
fetchImpl: opts.fetchImpl,
|
|
970
|
+
staleAfterMs,
|
|
971
|
+
mapError: mapStreamsError,
|
|
972
|
+
onFrame: async (frame) => {
|
|
973
|
+
attempt = 0;
|
|
974
|
+
if (frame.event === "ping" || !frame.data)
|
|
975
|
+
return;
|
|
976
|
+
let parsed;
|
|
977
|
+
try {
|
|
978
|
+
parsed = JSON.parse(frame.data);
|
|
979
|
+
} catch {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (!parsed.event)
|
|
983
|
+
return;
|
|
984
|
+
if (opts.verify !== "off") {
|
|
985
|
+
if (!parsed.sig) {
|
|
986
|
+
if (opts.verify === "strict") {
|
|
987
|
+
throw new StreamsSignatureError("Streams SSE frame signature is missing.");
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
const key = await opts.loadKey(parsed.key_id);
|
|
991
|
+
if (!ed25519.verifyEd25519(JSON.stringify(parsed.event), parsed.sig, key.publicKey)) {
|
|
992
|
+
throw new StreamsSignatureError("Streams SSE frame signature is invalid.");
|
|
993
|
+
}
|
|
823
994
|
}
|
|
824
995
|
}
|
|
996
|
+
await params.onEvent(parsed.event);
|
|
997
|
+
cursor = parsed.event.cursor ?? cursor;
|
|
825
998
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
999
|
+
});
|
|
1000
|
+
await sleep(reconnectDelay(attempt, baseDelayMs, undefined, opts.random), controller.signal);
|
|
1001
|
+
attempt += 1;
|
|
829
1002
|
} catch (err) {
|
|
830
1003
|
if (controller.signal.aborted)
|
|
831
1004
|
return;
|
|
832
1005
|
params.onError?.(err);
|
|
833
|
-
|
|
1006
|
+
if (isTerminal(err))
|
|
1007
|
+
throw err;
|
|
1008
|
+
const retryAfterSeconds = err.retryAfterSeconds;
|
|
1009
|
+
await sleep(reconnectDelay(attempt, baseDelayMs, retryAfterSeconds, opts.random), controller.signal);
|
|
1010
|
+
attempt += 1;
|
|
834
1011
|
}
|
|
835
1012
|
}
|
|
836
1013
|
};
|
|
837
|
-
run();
|
|
838
|
-
|
|
1014
|
+
const done = run();
|
|
1015
|
+
done.catch(() => {});
|
|
1016
|
+
const unsubscribe = () => controller.abort();
|
|
1017
|
+
return Object.assign(unsubscribe, { done });
|
|
1018
|
+
}
|
|
1019
|
+
async function readSse(opts) {
|
|
1020
|
+
const res = await opts.fetchImpl(opts.url, {
|
|
1021
|
+
headers: { ...opts.headers ?? {}, Accept: "text/event-stream" },
|
|
1022
|
+
signal: opts.signal
|
|
1023
|
+
});
|
|
1024
|
+
if (!res.ok) {
|
|
1025
|
+
if (opts.mapError)
|
|
1026
|
+
await opts.mapError(res);
|
|
1027
|
+
throw new StreamsServerError(`Streams SSE returned ${res.status}.`, res.status);
|
|
1028
|
+
}
|
|
1029
|
+
if (!res.body) {
|
|
1030
|
+
throw new StreamsServerError("Streams SSE response has no body.", 0);
|
|
1031
|
+
}
|
|
1032
|
+
for await (const frame of parseSseFrames(res.body, opts.signal, opts.staleAfterMs)) {
|
|
1033
|
+
await opts.onFrame(frame);
|
|
1034
|
+
}
|
|
839
1035
|
}
|
|
840
1036
|
function sleep(ms, signal) {
|
|
841
1037
|
return new Promise((resolve) => {
|
|
@@ -852,13 +1048,27 @@ function sleep(ms, signal) {
|
|
|
852
1048
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
853
1049
|
});
|
|
854
1050
|
}
|
|
855
|
-
async function
|
|
1051
|
+
async function readWithStaleTimer(reader, staleAfterMs) {
|
|
1052
|
+
if (staleAfterMs === undefined || !Number.isFinite(staleAfterMs)) {
|
|
1053
|
+
return reader.read();
|
|
1054
|
+
}
|
|
1055
|
+
let timer;
|
|
1056
|
+
const stale = new Promise((_, reject) => {
|
|
1057
|
+
timer = setTimeout(() => reject(new StreamsServerError(`Streams SSE went quiet for ${staleAfterMs} ms; reconnecting.`, 0, undefined, "STREAM_STALE")), staleAfterMs);
|
|
1058
|
+
});
|
|
1059
|
+
try {
|
|
1060
|
+
return await Promise.race([reader.read(), stale]);
|
|
1061
|
+
} finally {
|
|
1062
|
+
clearTimeout(timer);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function* parseSseFrames(body, signal, staleAfterMs) {
|
|
856
1066
|
const reader = body.getReader();
|
|
857
1067
|
const decoder = new TextDecoder;
|
|
858
1068
|
let buffer = "";
|
|
859
1069
|
try {
|
|
860
1070
|
while (!signal.aborted) {
|
|
861
|
-
const { value, done } = await reader
|
|
1071
|
+
const { value, done } = await readWithStaleTimer(reader, staleAfterMs);
|
|
862
1072
|
if (done)
|
|
863
1073
|
break;
|
|
864
1074
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -920,51 +1130,33 @@ function streamsFilters(params) {
|
|
|
920
1130
|
function normalizeBaseUrl(baseUrl) {
|
|
921
1131
|
return baseUrl.replace(/\/+$/, "");
|
|
922
1132
|
}
|
|
923
|
-
|
|
924
|
-
const text = await response.text();
|
|
925
|
-
if (text.length === 0)
|
|
926
|
-
return;
|
|
1133
|
+
function isLoopbackUrl(baseUrl) {
|
|
927
1134
|
try {
|
|
928
|
-
|
|
1135
|
+
const { hostname } = new URL(baseUrl);
|
|
1136
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname.startsWith("127.");
|
|
929
1137
|
} catch {
|
|
930
|
-
return
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
function errorMessage(body, fallback) {
|
|
934
|
-
if (body && typeof body === "object") {
|
|
935
|
-
const record = body;
|
|
936
|
-
const message = record.error ?? record.message;
|
|
937
|
-
if (typeof message === "string" && message.length > 0)
|
|
938
|
-
return message;
|
|
939
|
-
}
|
|
940
|
-
if (typeof body === "string" && body.length > 0)
|
|
941
|
-
return body;
|
|
942
|
-
return fallback;
|
|
943
|
-
}
|
|
944
|
-
async function mapStreamsError(response) {
|
|
945
|
-
const body = await responseBody(response);
|
|
946
|
-
if (response.status === 401) {
|
|
947
|
-
throw new AuthError(errorMessage(body, "API key invalid or expired."));
|
|
948
|
-
}
|
|
949
|
-
if (response.status === 429) {
|
|
950
|
-
const retryAfter = response.headers.get("Retry-After") ?? undefined;
|
|
951
|
-
throw new RateLimitError(errorMessage(body, "Rate limited. Try again later."), retryAfter);
|
|
1138
|
+
return false;
|
|
952
1139
|
}
|
|
953
|
-
if (response.status >= 500) {
|
|
954
|
-
throw new StreamsServerError(errorMessage(body, `Streams server returned ${response.status}.`), response.status, body);
|
|
955
|
-
}
|
|
956
|
-
throw new ValidationError(errorMessage(body, `Streams request returned ${response.status}.`), response.status, body);
|
|
957
1140
|
}
|
|
958
1141
|
function createStreamsClient(options) {
|
|
959
1142
|
const baseUrl = normalizeBaseUrl(resolveBaseUrl(options.baseUrl));
|
|
1143
|
+
const apiKey = resolveApiKey(options.apiKey);
|
|
1144
|
+
const origin = options.origin ?? "cli";
|
|
1145
|
+
const authHeaders = () => ({
|
|
1146
|
+
"x-sl-origin": origin,
|
|
1147
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
1148
|
+
});
|
|
960
1149
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
961
1150
|
const verify = options.verify;
|
|
962
1151
|
const verifyMode = verify === false ? "off" : verify === undefined ? "lenient" : "strict";
|
|
1152
|
+
if (verify === true && baseUrl.startsWith("http:") && !isLoopbackUrl(baseUrl)) {
|
|
1153
|
+
throw new ValidationError(`verify: true over plain http to ${baseUrl} fetches the signing key from the same unprotected path it verifies. Pin the key with verify: { publicKey } (see GET /public/streams/signing-key), or use https.`, 400);
|
|
1154
|
+
}
|
|
963
1155
|
let keyPromise = null;
|
|
964
1156
|
function loadKey() {
|
|
965
1157
|
if (keyPromise)
|
|
966
1158
|
return keyPromise;
|
|
967
|
-
|
|
1159
|
+
const pending = (async () => {
|
|
968
1160
|
if (typeof verify === "object") {
|
|
969
1161
|
return {
|
|
970
1162
|
keyId: ed255192.ed25519KeyId(verify.publicKey),
|
|
@@ -972,8 +1164,16 @@ function createStreamsClient(options) {
|
|
|
972
1164
|
publicKey: ed255192.loadEd25519PublicKey(verify.publicKey)
|
|
973
1165
|
};
|
|
974
1166
|
}
|
|
975
|
-
|
|
1167
|
+
let res;
|
|
1168
|
+
try {
|
|
1169
|
+
res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
|
|
1170
|
+
} catch {
|
|
1171
|
+
throw new StreamsServerError(`Could not reach the signing key at ${baseUrl}/public/streams/signing-key. Retryable.`, 0, undefined, "SIGNING_KEY_UNAVAILABLE");
|
|
1172
|
+
}
|
|
976
1173
|
if (!res.ok) {
|
|
1174
|
+
if (res.status >= 500 || res.status === 429) {
|
|
1175
|
+
throw new StreamsServerError(`Could not fetch signing key (${res.status}). Retryable.`, res.status, undefined, "SIGNING_KEY_UNAVAILABLE");
|
|
1176
|
+
}
|
|
977
1177
|
throw new StreamsSignatureError(`Could not fetch signing key (${res.status}).`);
|
|
978
1178
|
}
|
|
979
1179
|
const body = await res.json();
|
|
@@ -986,7 +1186,26 @@ function createStreamsClient(options) {
|
|
|
986
1186
|
publicKey: ed255192.loadEd25519PublicKey(body.public_key_pem)
|
|
987
1187
|
};
|
|
988
1188
|
})();
|
|
989
|
-
|
|
1189
|
+
keyPromise = pending;
|
|
1190
|
+
pending.catch(() => {
|
|
1191
|
+
if (keyPromise === pending)
|
|
1192
|
+
keyPromise = null;
|
|
1193
|
+
});
|
|
1194
|
+
return pending;
|
|
1195
|
+
}
|
|
1196
|
+
async function loadKeyFor(responseKeyId) {
|
|
1197
|
+
let key = await loadKey();
|
|
1198
|
+
if (!responseKeyId || responseKeyId === key.keyId)
|
|
1199
|
+
return key;
|
|
1200
|
+
if (typeof verify === "object") {
|
|
1201
|
+
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`);
|
|
1202
|
+
}
|
|
1203
|
+
keyPromise = null;
|
|
1204
|
+
key = await loadKey();
|
|
1205
|
+
if (responseKeyId !== key.keyId) {
|
|
1206
|
+
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
|
|
1207
|
+
}
|
|
1208
|
+
return key;
|
|
990
1209
|
}
|
|
991
1210
|
const dumps = createStreamsDumps({
|
|
992
1211
|
baseUrl: options.dumpsBaseUrl,
|
|
@@ -996,7 +1215,7 @@ function createStreamsClient(options) {
|
|
|
996
1215
|
});
|
|
997
1216
|
async function request(path) {
|
|
998
1217
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
999
|
-
headers:
|
|
1218
|
+
headers: authHeaders()
|
|
1000
1219
|
});
|
|
1001
1220
|
if (!response.ok)
|
|
1002
1221
|
await mapStreamsError(response);
|
|
@@ -1008,18 +1227,7 @@ function createStreamsClient(options) {
|
|
|
1008
1227
|
throw new StreamsSignatureError("Response is missing X-Signature.");
|
|
1009
1228
|
}
|
|
1010
1229
|
} else {
|
|
1011
|
-
const
|
|
1012
|
-
let key = await loadKey();
|
|
1013
|
-
if (responseKeyId && responseKeyId !== key.keyId) {
|
|
1014
|
-
if (typeof verify === "object") {
|
|
1015
|
-
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`);
|
|
1016
|
-
}
|
|
1017
|
-
keyPromise = null;
|
|
1018
|
-
key = await loadKey();
|
|
1019
|
-
if (responseKeyId !== key.keyId) {
|
|
1020
|
-
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1230
|
+
const key = await loadKeyFor(response.headers.get("X-Signature-KeyId"));
|
|
1023
1231
|
if (!ed255192.verifyEd25519(text, signature, key.publicKey)) {
|
|
1024
1232
|
throw new StreamsSignatureError;
|
|
1025
1233
|
}
|
|
@@ -1028,6 +1236,13 @@ function createStreamsClient(options) {
|
|
|
1028
1236
|
return JSON.parse(text);
|
|
1029
1237
|
}
|
|
1030
1238
|
const fetchEvents = listEvents;
|
|
1239
|
+
function listReorgs(params) {
|
|
1240
|
+
return request(`/v1/streams/reorgs${buildQuery({
|
|
1241
|
+
since: params.since,
|
|
1242
|
+
limit: params.limit
|
|
1243
|
+
})}`);
|
|
1244
|
+
}
|
|
1245
|
+
const fetchReorgs = (params) => listReorgs(params);
|
|
1031
1246
|
function encodeFilters(filters) {
|
|
1032
1247
|
return filters ? JSON.stringify(filters) : undefined;
|
|
1033
1248
|
}
|
|
@@ -1075,8 +1290,10 @@ function createStreamsClient(options) {
|
|
|
1075
1290
|
...streamsFilters(params),
|
|
1076
1291
|
batchSize: params.batchSize ?? 100,
|
|
1077
1292
|
fetchEvents,
|
|
1293
|
+
fetchReorgs,
|
|
1078
1294
|
onBatch,
|
|
1079
1295
|
onReorg: params.onReorg,
|
|
1296
|
+
maxRollbackDepth: params.maxRollbackDepth,
|
|
1080
1297
|
emptyBackoffMs: params.emptyBackoffMs,
|
|
1081
1298
|
maxPages: params.maxPages,
|
|
1082
1299
|
maxEmptyPolls: params.maxEmptyPolls,
|
|
@@ -1118,10 +1335,12 @@ function createStreamsClient(options) {
|
|
|
1118
1335
|
subscribe(params) {
|
|
1119
1336
|
return subscribeStreamsEvents({
|
|
1120
1337
|
baseUrl,
|
|
1121
|
-
|
|
1338
|
+
headers: authHeaders(),
|
|
1122
1339
|
fetchImpl,
|
|
1123
1340
|
verify: verifyMode,
|
|
1124
|
-
loadKey,
|
|
1341
|
+
loadKey: loadKeyFor,
|
|
1342
|
+
reconnectDelayMs: params.reconnectDelayMs,
|
|
1343
|
+
staleAfterMs: params.staleAfterMs,
|
|
1125
1344
|
params
|
|
1126
1345
|
});
|
|
1127
1346
|
},
|
|
@@ -1129,11 +1348,11 @@ function createStreamsClient(options) {
|
|
|
1129
1348
|
const fromCursor = params.from === "genesis" ? null : params.from ?? null;
|
|
1130
1349
|
const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
|
|
1131
1350
|
const manifest = await dumps.list();
|
|
1132
|
-
const files = manifest.files.filter((file) => file.to_block >= fromBlock).sort((a, b) => a.from_block - b.from_block || a.to_block - b.to_block);
|
|
1351
|
+
const files = manifest.files.filter((file) => file.to_block >= fromBlock).filter((file) => fromCursor === null || file.max_cursor === null || maxCursor(fromCursor, file.max_cursor) !== fromCursor).sort((a, b) => a.from_block - b.from_block || a.to_block - b.to_block);
|
|
1133
1352
|
for (const file of files) {
|
|
1134
1353
|
if (params.signal?.aborted)
|
|
1135
1354
|
break;
|
|
1136
|
-
await params.onDumpFile(file);
|
|
1355
|
+
await params.onDumpFile(file, { from: fromCursor });
|
|
1137
1356
|
}
|
|
1138
1357
|
const seam = maxCursor(fromCursor, manifest.latest_finalized_cursor);
|
|
1139
1358
|
return consumeStreamsEvents({
|
|
@@ -1158,14 +1377,7 @@ function createStreamsClient(options) {
|
|
|
1158
1377
|
return request(`/v1/streams/blocks/${encodeURIComponent(String(heightOrHash))}/events`);
|
|
1159
1378
|
}
|
|
1160
1379
|
},
|
|
1161
|
-
reorgs: {
|
|
1162
|
-
list(params) {
|
|
1163
|
-
return request(`/v1/streams/reorgs${buildQuery({
|
|
1164
|
-
since: params.since,
|
|
1165
|
-
limit: params.limit
|
|
1166
|
-
})}`);
|
|
1167
|
-
}
|
|
1168
|
-
},
|
|
1380
|
+
reorgs: { list: listReorgs },
|
|
1169
1381
|
dumps,
|
|
1170
1382
|
canonical(height) {
|
|
1171
1383
|
return request(`/v1/streams/canonical/${height}`);
|
|
@@ -1240,5 +1452,5 @@ export {
|
|
|
1240
1452
|
AuthError
|
|
1241
1453
|
};
|
|
1242
1454
|
|
|
1243
|
-
//# debugId=
|
|
1455
|
+
//# debugId=6013F1D9BD2F6AD764756E2164756E21
|
|
1244
1456
|
//# sourceMappingURL=index.js.map
|