@effectstream/sync 0.100.28 → 0.101.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/sync",
3
- "version": "0.100.28",
3
+ "version": "0.101.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -13,10 +13,10 @@
13
13
  },
14
14
  "exports": "./src/mod.ts",
15
15
  "dependencies": {
16
- "@effectstream/config": "0.100.28",
17
- "@effectstream/db": "0.100.28",
18
- "@effectstream/log": "0.100.28",
19
- "@effectstream/utils": "0.100.28",
16
+ "@effectstream/config": "0.101.0",
17
+ "@effectstream/db": "0.101.0",
18
+ "@effectstream/log": "0.101.0",
19
+ "@effectstream/utils": "0.101.0",
20
20
  "@midnight-ntwrk/ledger-v8": "8.0.3",
21
21
  "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
22
22
  "@midnight-ntwrk/onchain-runtime": "npm:@midnight-ntwrk/onchain-runtime-v3@3.0.0",
@@ -54,6 +54,9 @@ const CELESTIA_NODE_URL = ENV.getString(
54
54
  "http://localhost:26658",
55
55
  );
56
56
  const CELESTIA_AUTH_TOKEN = ENV.getString("CELESTIA_AUTH_TOKEN", "");
57
+ const CELESTIA_RPC_TIMEOUT_MS = parseInt(
58
+ ENV.getString("CELESTIA_RPC_TIMEOUT_MS", "15000"),
59
+ );
57
60
 
58
61
  /**
59
62
  * Converts a hex-encoded Celestia namespace to a base64-encoded 29-byte array.
@@ -80,29 +83,38 @@ export class CelestiaClient {
80
83
  }
81
84
 
82
85
  private async rpc<T>(method: string, params: unknown[]): Promise<T | null> {
83
- let res: Response;
84
- try {
85
- const headers: Record<string, string> = { "Content-Type": "application/json" };
86
- if (CELESTIA_AUTH_TOKEN) headers["Authorization"] = `Bearer ${CELESTIA_AUTH_TOKEN}`;
87
- res = await fetch(this.rpcUrl, {
88
- method: "POST",
89
- headers,
90
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
91
- });
92
- } catch (err: unknown) {
93
- const msg = err instanceof Error ? err.message : String(err);
94
- console.error(`[Celestia] RPC fetch error [${method}]: ${msg}`);
95
- return null;
96
- }
97
-
98
- const json = await res.json();
99
- if (json.error) {
100
- const errMsg: string = json.error.message ?? String(json.error);
101
- if (errMsg.includes("blob: not found")) return null;
102
- console.error(`[Celestia] RPC error [${method}]:`, errMsg);
103
- return null;
86
+ // Retry forever — a transient stall must never cause a block to be skipped.
87
+ // The only exit paths are: (a) success, (b) "blob: not found" (empty namespace).
88
+ // Everything else (network error, timeout, RPC error) loops with backoff.
89
+ let attempt = 0;
90
+ while (true) {
91
+ try {
92
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
93
+ if (CELESTIA_AUTH_TOKEN) headers["Authorization"] = `Bearer ${CELESTIA_AUTH_TOKEN}`;
94
+ const res = await fetch(this.rpcUrl, {
95
+ method: "POST",
96
+ headers,
97
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
98
+ signal: AbortSignal.timeout(CELESTIA_RPC_TIMEOUT_MS),
99
+ });
100
+ const json = await res.json();
101
+ if (json.error) {
102
+ const errMsg: string = json.error.message ?? String(json.error);
103
+ if (errMsg.includes("blob: not found")) return null;
104
+ throw new Error(`[Celestia] RPC error [${method}]: ${errMsg}`);
105
+ }
106
+ return json.result as T;
107
+ } catch (err: unknown) {
108
+ attempt++;
109
+ const backoffMs = Math.min(1000 * 2 ** Math.min(attempt - 1, 5), 30_000);
110
+ console.warn(
111
+ `[Celestia] ${method} attempt ${attempt} failed (retrying in ${backoffMs}ms): ${
112
+ err instanceof Error ? err.message : String(err)
113
+ }`,
114
+ );
115
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
116
+ }
104
117
  }
105
- return json.result as T;
106
118
  }
107
119
 
108
120
  async getLatestBlockHeight(): Promise<number> {
@@ -51,44 +51,49 @@ export class CelestiaFetcher extends BaseDataFetcher<
51
51
  (p) => p.primitive.getAllBlockHeaders,
52
52
  );
53
53
 
54
+ const heights: number[] = [];
55
+ for (let h = data.from; h <= data.to; h++) heights.push(h);
56
+
54
57
  console.log(
55
- `[Celestia] Fetching blocks from ${data.from} to ${data.to}.${
58
+ `[Celestia] Fetching blocks from ${data.from} to ${data.to} (${heights.length} blocks, concurrency=${(this.config.syncProtocol as any).concurrency ?? 1}).${
56
59
  data.isPresync ? " [presync]" : ""
57
60
  }`,
58
61
  );
59
62
 
60
- let lastFetchedOutput: Output | undefined;
61
- for (let height = data.from; height <= data.to; height++) {
62
- let extHeader;
63
- try {
64
- extHeader = yield* call(() =>
65
- this.client.getHeaderAtHeight(height)
66
- );
67
- } catch (_e) {
68
- break;
69
- }
63
+ const results: (Output | undefined)[] = new Array(heights.length);
64
+ let cursor = 0;
65
+ const concurrency: number = (this.config.syncProtocol as any).concurrency ?? 1;
66
+ const self = this;
70
67
 
71
- const blockHash = extHeader.commit.block_id.hash;
68
+ // Each worker pulls the next unstarted height from `cursor` and stores the
69
+ // result at the corresponding index so output order is always height-ascending.
70
+ // The cursor increment is cooperative-race-free in effection: it runs
71
+ // synchronously before the first yield in each worker iteration.
72
+ function* worker(): Operation<void> {
73
+ while (true) {
74
+ const i = cursor++;
75
+ if (i >= heights.length) return;
76
+ const height = heights[i];
77
+ const extHeader = yield* call(() => self.client.getHeaderAtHeight(height));
78
+ const blockHash = extHeader.commit.block_id.hash;
79
+ const primitives = yield* self.readPrimitives(height, blockHash, self.config.primitives);
80
+ results[i] = { timestamp: extHeader.header.time, height, hash: blockHash, primitives };
81
+ }
82
+ }
72
83
 
73
- const primitives = yield* this.readPrimitives(
74
- height,
75
- blockHash,
76
- this.config.primitives,
84
+ if (heights.length > 0) {
85
+ yield* all(
86
+ Array.from({ length: Math.min(concurrency, heights.length) }, () => worker()),
77
87
  );
88
+ }
78
89
 
79
- const output: Output = {
80
- timestamp: extHeader.header.time,
81
- height,
82
- hash: blockHash,
83
- primitives,
84
- };
90
+ let lastFetchedOutput: Output | undefined;
91
+ for (let i = 0; i < results.length; i++) {
92
+ const output = results[i];
93
+ if (!output) continue;
85
94
  lastFetchedOutput = output;
86
-
87
- if (fetchAllBlocks || primitives.length > 0 || height === data.to) {
88
- outputs.push({
89
- output,
90
- cleanup: () => {},
91
- });
95
+ if (fetchAllBlocks || output.primitives.length > 0 || output.height === data.to) {
96
+ outputs.push({ output, cleanup: () => {} });
92
97
  }
93
98
  }
94
99