@secondlayer/sdk 9.0.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +78 -17
- package/README.md +98 -31
- package/dist/index.d.ts +234 -113
- package/dist/index.js +782 -445
- package/dist/index.js.map +19 -19
- package/dist/sinks/bun-sqlite.d.ts +4 -1
- package/dist/sinks/bun-sqlite.js +18 -19
- package/dist/sinks/bun-sqlite.js.map +5 -5
- package/dist/sinks/core.d.ts +8 -1
- package/dist/sinks/core.js +15 -19
- package/dist/sinks/core.js.map +4 -4
- package/dist/sinks/drizzle.d.ts +4 -1
- package/dist/sinks/drizzle.js +19 -20
- package/dist/sinks/drizzle.js.map +5 -5
- package/dist/sinks/kysely.d.ts +4 -1
- package/dist/sinks/kysely.js +20 -20
- 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 +94 -24
- package/dist/streams/index.js +411 -194
- package/dist/streams/index.js.map +13 -13
- package/dist/subgraphs/index.d.ts +198 -92
- package/dist/subgraphs/index.js +778 -443
- 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
|
+
});
|
|
256
|
+
}
|
|
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
|
+
}
|
|
222
281
|
}
|
|
223
|
-
async fetchResponse(method, path, body) {
|
|
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,56 +294,50 @@ 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
|
-
|
|
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 } : {});
|
|
257
323
|
}
|
|
258
|
-
|
|
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;
|
|
277
|
-
}
|
|
278
|
-
throw new ApiError(response.status, message, parsedBody, code);
|
|
324
|
+
throw new ApiError(response.status, envelope.message ?? `HTTP ${response.status}`, envelope.body, envelope.code);
|
|
279
325
|
}
|
|
280
326
|
return response;
|
|
281
327
|
}
|
|
282
328
|
}
|
|
283
329
|
|
|
330
|
+
// src/streams/cursor.ts
|
|
331
|
+
import {
|
|
332
|
+
blockEndCursor,
|
|
333
|
+
decodeStreamsCursor,
|
|
334
|
+
encodeStreamsCursor
|
|
335
|
+
} from "@secondlayer/shared";
|
|
336
|
+
|
|
284
337
|
// src/streams/errors.ts
|
|
285
338
|
class StreamsServerError extends ApiError {
|
|
286
|
-
constructor(message, status, body) {
|
|
287
|
-
super(status, message, body,
|
|
339
|
+
constructor(message, status, body, code) {
|
|
340
|
+
super(status, message, body, code, { retryable: true });
|
|
288
341
|
this.name = "StreamsServerError";
|
|
289
342
|
}
|
|
290
343
|
}
|
|
@@ -295,23 +348,38 @@ class StreamsSignatureError extends SecondLayerError {
|
|
|
295
348
|
this.name = "StreamsSignatureError";
|
|
296
349
|
}
|
|
297
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
|
+
}
|
|
298
365
|
|
|
299
366
|
// src/streams/cursor.ts
|
|
300
|
-
var REWIND_FOOT_INDEX_SENTINEL = 2147483647;
|
|
301
367
|
var Cursor = {
|
|
302
368
|
atHeight(height) {
|
|
303
369
|
if (height <= 0)
|
|
304
|
-
return
|
|
305
|
-
return
|
|
370
|
+
return null;
|
|
371
|
+
return encodeStreamsCursor(blockEndCursor(height - 1));
|
|
306
372
|
},
|
|
307
373
|
parse(cursor) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
374
|
+
try {
|
|
375
|
+
const decoded = decodeStreamsCursor(cursor);
|
|
376
|
+
return {
|
|
377
|
+
blockHeight: decoded.block_height,
|
|
378
|
+
eventIndex: decoded.event_index
|
|
379
|
+
};
|
|
380
|
+
} catch {
|
|
312
381
|
throw new ValidationError(`Invalid stream cursor "${cursor}"; expected "<block>:<index>" (e.g. "951475:3").`, 400);
|
|
313
382
|
}
|
|
314
|
-
return { blockHeight, eventIndex };
|
|
315
383
|
}
|
|
316
384
|
};
|
|
317
385
|
|
|
@@ -319,6 +387,39 @@ var Cursor = {
|
|
|
319
387
|
function reorgKey(reorg) {
|
|
320
388
|
return `${reorg.detected_at}|${reorg.fork_point_height}|${reorg.new_canonical_tip}`;
|
|
321
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
|
+
}
|
|
322
423
|
function assertFinalizedCheckpoint(returned, checkpoint) {
|
|
323
424
|
if (returned === null || returned === undefined)
|
|
324
425
|
return;
|
|
@@ -398,13 +499,15 @@ async function defaultSleep(ms, signal) {
|
|
|
398
499
|
if (signal?.aborted)
|
|
399
500
|
return;
|
|
400
501
|
await new Promise((resolve) => {
|
|
401
|
-
const
|
|
402
|
-
if (!signal)
|
|
403
|
-
return;
|
|
404
|
-
signal.addEventListener("abort", () => {
|
|
502
|
+
const onAbort = () => {
|
|
405
503
|
clearTimeout(timeout);
|
|
406
504
|
resolve();
|
|
407
|
-
}
|
|
505
|
+
};
|
|
506
|
+
const timeout = setTimeout(() => {
|
|
507
|
+
signal?.removeEventListener("abort", onAbort);
|
|
508
|
+
resolve();
|
|
509
|
+
}, ms);
|
|
510
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
408
511
|
});
|
|
409
512
|
}
|
|
410
513
|
async function consumeStreamsEvents(opts) {
|
|
@@ -418,6 +521,7 @@ async function consumeStreamsEvents(opts) {
|
|
|
418
521
|
const committedCursor = await opts.sink?.loadCursor() ?? null;
|
|
419
522
|
let cursor = opts.fromCursor ?? committedCursor;
|
|
420
523
|
const handledReorgs = new Set;
|
|
524
|
+
let reorgSince = null;
|
|
421
525
|
let pages = 0;
|
|
422
526
|
let emptyPolls = 0;
|
|
423
527
|
let height = resumeHeight(cursor);
|
|
@@ -441,23 +545,57 @@ async function consumeStreamsEvents(opts) {
|
|
|
441
545
|
signal: opts.signal
|
|
442
546
|
});
|
|
443
547
|
pages++;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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;
|
|
456
562
|
scanned = height;
|
|
457
563
|
emptyPolls = 0;
|
|
458
564
|
continue;
|
|
459
565
|
}
|
|
460
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
|
+
}
|
|
461
599
|
const emitted = finalizedOnly ? envelope.events.filter((event) => event.finalized) : envelope.events;
|
|
462
600
|
const checkpoint = finalizedOnly ? emitted.at(-1)?.cursor ?? cursor : envelope.next_cursor;
|
|
463
601
|
height = emitted.at(-1)?.block_height ?? height;
|
|
@@ -735,15 +873,46 @@ function createStreamsDumps(opts) {
|
|
|
735
873
|
return manifest;
|
|
736
874
|
}
|
|
737
875
|
async function download(file) {
|
|
876
|
+
return fetchPageWithRetry(() => downloadOnce(file), {
|
|
877
|
+
sleep: defaultSleep
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async function downloadOnce(file) {
|
|
738
881
|
const res = await opts.fetchImpl(fileUrl(file));
|
|
739
882
|
if (!res.ok) {
|
|
740
883
|
throw new StreamsServerError(`Could not download dump ${file.path} (${res.status}).`, res.status);
|
|
741
884
|
}
|
|
742
|
-
|
|
743
|
-
|
|
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");
|
|
744
905
|
if (digest !== file.sha256) {
|
|
745
906
|
throw new StreamsSignatureError(`Dump ${file.path} sha256 mismatch (expected ${file.sha256}, got ${digest}).`);
|
|
746
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
|
+
}
|
|
747
916
|
return bytes;
|
|
748
917
|
}
|
|
749
918
|
return { list, fileUrl, download };
|
|
@@ -751,6 +920,19 @@ function createStreamsDumps(opts) {
|
|
|
751
920
|
|
|
752
921
|
// src/streams/subscribe.ts
|
|
753
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
|
+
}
|
|
754
936
|
function subscribeStreamsEvents(opts) {
|
|
755
937
|
const { params } = opts;
|
|
756
938
|
const controller = new AbortController;
|
|
@@ -764,7 +946,9 @@ function subscribeStreamsEvents(opts) {
|
|
|
764
946
|
});
|
|
765
947
|
}
|
|
766
948
|
let cursor = params.fromCursor ?? null;
|
|
767
|
-
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;
|
|
768
952
|
const run = async () => {
|
|
769
953
|
while (!controller.signal.aborted) {
|
|
770
954
|
try {
|
|
@@ -778,55 +962,76 @@ function subscribeStreamsEvents(opts) {
|
|
|
778
962
|
asset_identifier: params.assetIdentifier,
|
|
779
963
|
filters: params.filters ? JSON.stringify(params.filters) : undefined
|
|
780
964
|
})}`;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
} else {
|
|
811
|
-
const key = await opts.loadKey();
|
|
812
|
-
if (!ed25519.verifyEd25519(JSON.stringify(parsed.event), parsed.sig, key.publicKey)) {
|
|
813
|
-
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
|
+
}
|
|
814
994
|
}
|
|
815
995
|
}
|
|
996
|
+
await params.onEvent(parsed.event);
|
|
997
|
+
cursor = parsed.event.cursor ?? cursor;
|
|
816
998
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
999
|
+
});
|
|
1000
|
+
await sleep(reconnectDelay(attempt, baseDelayMs, undefined, opts.random), controller.signal);
|
|
1001
|
+
attempt += 1;
|
|
820
1002
|
} catch (err) {
|
|
821
1003
|
if (controller.signal.aborted)
|
|
822
1004
|
return;
|
|
823
1005
|
params.onError?.(err);
|
|
824
|
-
|
|
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;
|
|
825
1011
|
}
|
|
826
1012
|
}
|
|
827
1013
|
};
|
|
828
|
-
run();
|
|
829
|
-
|
|
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
|
+
}
|
|
830
1035
|
}
|
|
831
1036
|
function sleep(ms, signal) {
|
|
832
1037
|
return new Promise((resolve) => {
|
|
@@ -843,13 +1048,27 @@ function sleep(ms, signal) {
|
|
|
843
1048
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
844
1049
|
});
|
|
845
1050
|
}
|
|
846
|
-
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) {
|
|
847
1066
|
const reader = body.getReader();
|
|
848
1067
|
const decoder = new TextDecoder;
|
|
849
1068
|
let buffer = "";
|
|
850
1069
|
try {
|
|
851
1070
|
while (!signal.aborted) {
|
|
852
|
-
const { value, done } = await reader
|
|
1071
|
+
const { value, done } = await readWithStaleTimer(reader, staleAfterMs);
|
|
853
1072
|
if (done)
|
|
854
1073
|
break;
|
|
855
1074
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -889,12 +1108,8 @@ function parseFrame(raw) {
|
|
|
889
1108
|
function cursorTuple(cursor) {
|
|
890
1109
|
if (!cursor)
|
|
891
1110
|
return [-1, -1];
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
if (parts.length !== 2 || !Number.isInteger(block) || !Number.isInteger(index)) {
|
|
895
|
-
throw new ValidationError(`Invalid stream cursor "${cursor}"; expected "<block>:<index>" (e.g. "951475:3").`, 400);
|
|
896
|
-
}
|
|
897
|
-
return [block, index];
|
|
1111
|
+
const { blockHeight, eventIndex } = Cursor.parse(cursor);
|
|
1112
|
+
return [blockHeight, eventIndex];
|
|
898
1113
|
}
|
|
899
1114
|
function maxCursor(a, b) {
|
|
900
1115
|
const [ah, ai] = cursorTuple(a);
|
|
@@ -915,51 +1130,33 @@ function streamsFilters(params) {
|
|
|
915
1130
|
function normalizeBaseUrl(baseUrl) {
|
|
916
1131
|
return baseUrl.replace(/\/+$/, "");
|
|
917
1132
|
}
|
|
918
|
-
|
|
919
|
-
const text = await response.text();
|
|
920
|
-
if (text.length === 0)
|
|
921
|
-
return;
|
|
1133
|
+
function isLoopbackUrl(baseUrl) {
|
|
922
1134
|
try {
|
|
923
|
-
|
|
1135
|
+
const { hostname } = new URL(baseUrl);
|
|
1136
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname.startsWith("127.");
|
|
924
1137
|
} catch {
|
|
925
|
-
return
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
function errorMessage(body, fallback) {
|
|
929
|
-
if (body && typeof body === "object") {
|
|
930
|
-
const record = body;
|
|
931
|
-
const message = record.error ?? record.message;
|
|
932
|
-
if (typeof message === "string" && message.length > 0)
|
|
933
|
-
return message;
|
|
934
|
-
}
|
|
935
|
-
if (typeof body === "string" && body.length > 0)
|
|
936
|
-
return body;
|
|
937
|
-
return fallback;
|
|
938
|
-
}
|
|
939
|
-
async function mapStreamsError(response) {
|
|
940
|
-
const body = await responseBody(response);
|
|
941
|
-
if (response.status === 401) {
|
|
942
|
-
throw new AuthError(errorMessage(body, "API key invalid or expired."));
|
|
943
|
-
}
|
|
944
|
-
if (response.status === 429) {
|
|
945
|
-
const retryAfter = response.headers.get("Retry-After") ?? undefined;
|
|
946
|
-
throw new RateLimitError(errorMessage(body, "Rate limited. Try again later."), retryAfter);
|
|
947
|
-
}
|
|
948
|
-
if (response.status >= 500) {
|
|
949
|
-
throw new StreamsServerError(errorMessage(body, `Streams server returned ${response.status}.`), response.status, body);
|
|
1138
|
+
return false;
|
|
950
1139
|
}
|
|
951
|
-
throw new ValidationError(errorMessage(body, `Streams request returned ${response.status}.`), response.status, body);
|
|
952
1140
|
}
|
|
953
1141
|
function createStreamsClient(options) {
|
|
954
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
|
+
});
|
|
955
1149
|
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
956
1150
|
const verify = options.verify;
|
|
957
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
|
+
}
|
|
958
1155
|
let keyPromise = null;
|
|
959
1156
|
function loadKey() {
|
|
960
1157
|
if (keyPromise)
|
|
961
1158
|
return keyPromise;
|
|
962
|
-
|
|
1159
|
+
const pending = (async () => {
|
|
963
1160
|
if (typeof verify === "object") {
|
|
964
1161
|
return {
|
|
965
1162
|
keyId: ed255192.ed25519KeyId(verify.publicKey),
|
|
@@ -967,8 +1164,16 @@ function createStreamsClient(options) {
|
|
|
967
1164
|
publicKey: ed255192.loadEd25519PublicKey(verify.publicKey)
|
|
968
1165
|
};
|
|
969
1166
|
}
|
|
970
|
-
|
|
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
|
+
}
|
|
971
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
|
+
}
|
|
972
1177
|
throw new StreamsSignatureError(`Could not fetch signing key (${res.status}).`);
|
|
973
1178
|
}
|
|
974
1179
|
const body = await res.json();
|
|
@@ -981,7 +1186,26 @@ function createStreamsClient(options) {
|
|
|
981
1186
|
publicKey: ed255192.loadEd25519PublicKey(body.public_key_pem)
|
|
982
1187
|
};
|
|
983
1188
|
})();
|
|
984
|
-
|
|
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;
|
|
985
1209
|
}
|
|
986
1210
|
const dumps = createStreamsDumps({
|
|
987
1211
|
baseUrl: options.dumpsBaseUrl,
|
|
@@ -991,7 +1215,7 @@ function createStreamsClient(options) {
|
|
|
991
1215
|
});
|
|
992
1216
|
async function request(path) {
|
|
993
1217
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
994
|
-
headers:
|
|
1218
|
+
headers: authHeaders()
|
|
995
1219
|
});
|
|
996
1220
|
if (!response.ok)
|
|
997
1221
|
await mapStreamsError(response);
|
|
@@ -1003,18 +1227,7 @@ function createStreamsClient(options) {
|
|
|
1003
1227
|
throw new StreamsSignatureError("Response is missing X-Signature.");
|
|
1004
1228
|
}
|
|
1005
1229
|
} else {
|
|
1006
|
-
const
|
|
1007
|
-
let key = await loadKey();
|
|
1008
|
-
if (responseKeyId && responseKeyId !== key.keyId) {
|
|
1009
|
-
if (typeof verify === "object") {
|
|
1010
|
-
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}', expected pinned key '${key.keyId}'.`);
|
|
1011
|
-
}
|
|
1012
|
-
keyPromise = null;
|
|
1013
|
-
key = await loadKey();
|
|
1014
|
-
if (responseKeyId !== key.keyId) {
|
|
1015
|
-
throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1230
|
+
const key = await loadKeyFor(response.headers.get("X-Signature-KeyId"));
|
|
1018
1231
|
if (!ed255192.verifyEd25519(text, signature, key.publicKey)) {
|
|
1019
1232
|
throw new StreamsSignatureError;
|
|
1020
1233
|
}
|
|
@@ -1023,6 +1236,13 @@ function createStreamsClient(options) {
|
|
|
1023
1236
|
return JSON.parse(text);
|
|
1024
1237
|
}
|
|
1025
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);
|
|
1026
1246
|
function encodeFilters(filters) {
|
|
1027
1247
|
return filters ? JSON.stringify(filters) : undefined;
|
|
1028
1248
|
}
|
|
@@ -1070,8 +1290,10 @@ function createStreamsClient(options) {
|
|
|
1070
1290
|
...streamsFilters(params),
|
|
1071
1291
|
batchSize: params.batchSize ?? 100,
|
|
1072
1292
|
fetchEvents,
|
|
1293
|
+
fetchReorgs,
|
|
1073
1294
|
onBatch,
|
|
1074
1295
|
onReorg: params.onReorg,
|
|
1296
|
+
maxRollbackDepth: params.maxRollbackDepth,
|
|
1075
1297
|
emptyBackoffMs: params.emptyBackoffMs,
|
|
1076
1298
|
maxPages: params.maxPages,
|
|
1077
1299
|
maxEmptyPolls: params.maxEmptyPolls,
|
|
@@ -1113,10 +1335,12 @@ function createStreamsClient(options) {
|
|
|
1113
1335
|
subscribe(params) {
|
|
1114
1336
|
return subscribeStreamsEvents({
|
|
1115
1337
|
baseUrl,
|
|
1116
|
-
|
|
1338
|
+
headers: authHeaders(),
|
|
1117
1339
|
fetchImpl,
|
|
1118
1340
|
verify: verifyMode,
|
|
1119
|
-
loadKey,
|
|
1341
|
+
loadKey: loadKeyFor,
|
|
1342
|
+
reconnectDelayMs: params.reconnectDelayMs,
|
|
1343
|
+
staleAfterMs: params.staleAfterMs,
|
|
1120
1344
|
params
|
|
1121
1345
|
});
|
|
1122
1346
|
},
|
|
@@ -1124,11 +1348,11 @@ function createStreamsClient(options) {
|
|
|
1124
1348
|
const fromCursor = params.from === "genesis" ? null : params.from ?? null;
|
|
1125
1349
|
const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
|
|
1126
1350
|
const manifest = await dumps.list();
|
|
1127
|
-
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);
|
|
1128
1352
|
for (const file of files) {
|
|
1129
1353
|
if (params.signal?.aborted)
|
|
1130
1354
|
break;
|
|
1131
|
-
await params.onDumpFile(file);
|
|
1355
|
+
await params.onDumpFile(file, { from: fromCursor });
|
|
1132
1356
|
}
|
|
1133
1357
|
const seam = maxCursor(fromCursor, manifest.latest_finalized_cursor);
|
|
1134
1358
|
return consumeStreamsEvents({
|
|
@@ -1153,14 +1377,7 @@ function createStreamsClient(options) {
|
|
|
1153
1377
|
return request(`/v1/streams/blocks/${encodeURIComponent(String(heightOrHash))}/events`);
|
|
1154
1378
|
}
|
|
1155
1379
|
},
|
|
1156
|
-
reorgs: {
|
|
1157
|
-
list(params) {
|
|
1158
|
-
return request(`/v1/streams/reorgs${buildQuery({
|
|
1159
|
-
since: params.since,
|
|
1160
|
-
limit: params.limit
|
|
1161
|
-
})}`);
|
|
1162
|
-
}
|
|
1163
|
-
},
|
|
1380
|
+
reorgs: { list: listReorgs },
|
|
1164
1381
|
dumps,
|
|
1165
1382
|
canonical(height) {
|
|
1166
1383
|
return request(`/v1/streams/canonical/${height}`);
|
|
@@ -1235,5 +1452,5 @@ export {
|
|
|
1235
1452
|
AuthError
|
|
1236
1453
|
};
|
|
1237
1454
|
|
|
1238
|
-
//# debugId=
|
|
1455
|
+
//# debugId=6013F1D9BD2F6AD764756E2164756E21
|
|
1239
1456
|
//# sourceMappingURL=index.js.map
|