@effectstream/sync 0.100.25 → 0.100.26

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.25",
3
+ "version": "0.100.26",
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.25",
17
- "@effectstream/db": "0.100.25",
18
- "@effectstream/log": "0.100.25",
19
- "@effectstream/utils": "0.100.25",
16
+ "@effectstream/config": "0.100.26",
17
+ "@effectstream/db": "0.100.26",
18
+ "@effectstream/log": "0.100.26",
19
+ "@effectstream/utils": "0.100.26",
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",
@@ -1,4 +1,3 @@
1
- import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
2
1
  import type { Block } from "./types.ts";
3
2
  import type { ExecutionResult } from "graphql-ws/client";
4
3
 
@@ -67,24 +66,16 @@ export interface BlockFetchOptions {
67
66
  tokenMints?: boolean;
68
67
  }
69
68
 
70
- type PublicDataProvider = ReturnType<typeof indexerPublicDataProvider>;
71
69
  export class MidnightClient {
72
70
  private readonly queryURL: string;
73
- private readonly subscriptionURL: string;
74
- private publicDataProvider: PublicDataProvider;
75
71
  private readonly networkId?: string;
76
72
  /** Timeout in milliseconds for individual HTTP requests to the indexer. */
77
73
  private readonly requestTimeoutMs: number;
78
74
 
79
- constructor(queryURL: string, subscriptionURL: string, networkId?: string, requestTimeoutMs = 30_000) {
75
+ constructor(queryURL: string, networkId?: string, requestTimeoutMs = 30_000) {
80
76
  this.queryURL = queryURL;
81
- this.subscriptionURL = subscriptionURL;
82
77
  this.networkId = networkId;
83
78
  this.requestTimeoutMs = requestTimeoutMs;
84
- this.publicDataProvider = indexerPublicDataProvider(
85
- queryURL,
86
- subscriptionURL,
87
- );
88
79
  console.log(
89
80
  `[MidnightClient] Using indexer ${queryURL} (network: ${
90
81
  networkId ?? "unknown"
@@ -92,48 +83,6 @@ export class MidnightClient {
92
83
  );
93
84
  }
94
85
 
95
- /**
96
- * Recreate the publicDataProvider to recover from stale internal state.
97
- * The Midnight JS library can get into a state where all queryContractState
98
- * calls fail with {"json":{}} until the provider is recreated.
99
- */
100
- resetProvider(): void {
101
- console.log(
102
- `[MidnightClient] Resetting publicDataProvider for ${this.queryURL}`,
103
- );
104
- this.publicDataProvider = indexerPublicDataProvider(
105
- this.queryURL,
106
- this.subscriptionURL,
107
- );
108
- }
109
-
110
- async fetchContractState(contractAddress: string, blockHeight: number) {
111
- try {
112
- const state = await Promise.race([
113
- this.publicDataProvider.queryContractState(
114
- contractAddress,
115
- {
116
- type: "blockHeight",
117
- blockHeight,
118
- },
119
- ),
120
- new Promise<never>((_, reject) => {
121
- setTimeout(
122
- () => reject(new Error(`fetchContractState timed out after ${this.requestTimeoutMs}ms (contract=${contractAddress}, height=${blockHeight})`)),
123
- this.requestTimeoutMs,
124
- );
125
- }),
126
- ]);
127
- return state;
128
- } catch (error) {
129
- // The Midnight JS publicDataProvider can enter a broken state where all
130
- // subsequent calls fail (e.g. {"json":{}}). Recreate it so the next
131
- // retry starts with a fresh connection.
132
- this.resetProvider();
133
- throw error;
134
- }
135
- }
136
-
137
86
  async gqlQuery(query: string, signal?: AbortSignal): Promise<any> {
138
87
  const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
139
88
  const combinedSignal = signal
@@ -35,16 +35,9 @@ export class MidnightFetcher extends BaseDataFetcher<
35
35
  ) {
36
36
  super(config.syncProtocol.name);
37
37
  const indexerHttp = config.syncProtocol.indexer;
38
- const indexerWs =
39
- (config.syncProtocol as any).indexerWS ??
40
- (config.syncProtocol as any).indexerWs;
41
- if (!indexerHttp || !indexerWs) {
38
+ if (!indexerHttp) {
42
39
  throw new Error(
43
- `Midnight sync protocol "${
44
- config.syncProtocol.name
45
- }" requires both indexer and indexerWS URLs. Received indexer=${
46
- indexerHttp ?? "undefined"
47
- }, indexerWS=${indexerWs ?? "undefined"}.`,
40
+ `Midnight sync protocol "${config.syncProtocol.name}" requires an indexer URL.`,
48
41
  );
49
42
  }
50
43
  this.networkId = config.network?.networkId ??
@@ -65,7 +58,6 @@ export class MidnightFetcher extends BaseDataFetcher<
65
58
 
66
59
  this.client = new MidnightClient(
67
60
  indexerHttp,
68
- indexerWs,
69
61
  this.networkId,
70
62
  );
71
63
  }
@@ -385,19 +377,17 @@ export class MidnightFetcher extends BaseDataFetcher<
385
377
  @bound
386
378
  *fetchContractState(
387
379
  height: number,
388
- client: MidnightClient,
380
+ _client: MidnightClient,
389
381
  primitiveEntry: PrimitiveEntryType,
390
382
  block: MidnightGqlBlockState,
391
383
  ): Operation<PrimitiveType[]> {
392
384
  const contractAddress = primitiveEntry.primitive.contractAddress!;
393
- const blockFinalState = yield* call(() =>
394
- client.fetchContractState(contractAddress, height)
395
- );
396
- // The state returns null if the contract was not called in the block height
397
- if (!blockFinalState) {
398
- return [];
399
- }
400
385
 
386
+ // Determine whether the contract was called in this block by inspecting
387
+ // the contractActions already present in the fetched block data — no extra
388
+ // network round-trip needed. The per-transaction state is also read from
389
+ // contractActions below, so queryContractState would be redundant and its
390
+ // accumulating internal WS subscriptions caused saturation during catch-up.
401
391
  const transactions = block.block.transactions.filter((t) => {
402
392
  return (t.contractActions ?? []).some((c) => {
403
393
  const longest = Math.max(contractAddress.length, c.address.length);
@@ -406,6 +396,9 @@ export class MidnightFetcher extends BaseDataFetcher<
406
396
  });
407
397
  });
408
398
 
399
+ if (transactions.length === 0) {
400
+ return [];
401
+ }
409
402
 
410
403
  return transactions.map(t => {
411
404
  // TODO: What does it mean if t.contractActions has more than one element?
@@ -12,11 +12,6 @@ import { MidnightClient } from "./MidnightClient.ts";
12
12
  import { applyDelay } from "../common/utils.ts";
13
13
  import { bufferAtCap } from "../common/page-helpers.ts";
14
14
 
15
- type LatestBlock = {
16
- block: {
17
- height: number;
18
- };
19
- };
20
15
 
21
16
  export class MidnightSyncState extends SyncState<
22
17
  Input,
@@ -27,6 +22,13 @@ export class MidnightSyncState extends SyncState<
27
22
  MidnightFetcher
28
23
  > {
29
24
  private readonly url: string;
25
+ private cachedLatestHeight: number | undefined;
26
+ private cacheTimestampMs: number = 0;
27
+ // Midnight produces ~1 block every 6 s. Re-query the real tip once this many
28
+ // estimated chain blocks have elapsed, giving periodic lag info without a
29
+ // round-trip on every batch.
30
+ private static readonly CHAIN_BLOCK_TIME_MS = 6_000;
31
+ private static readonly REFRESH_EVERY_CHAIN_BLOCKS = 100;
30
32
 
31
33
  constructor(
32
34
  lastPage: LastPage<Page, RootPage> | undefined,
@@ -74,12 +76,29 @@ export class MidnightSyncState extends SyncState<
74
76
  @bound
75
77
  override *stateToInput(): Operation<Input | undefined> {
76
78
  if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
77
- const latestBlockResult = yield* call(() => this.client.fetchLatestBlock());
78
- const latestHeight = latestBlockResult.block.height;
79
79
 
80
80
  const startHeight = this.lastPage?.own.height ??
81
81
  this.config.syncProtocol.startBlockHeight - 1;
82
82
 
83
+ // Extrapolate how many new chain blocks have likely arrived since the last
84
+ // real query, then re-query only when that estimate exceeds the threshold.
85
+ // This bounds indexer round-trips to O(catch-up_time / (REFRESH * block_time))
86
+ // rather than one per batch, while still refreshing periodically so lag
87
+ // metrics stay accurate.
88
+ const estimatedNewBlocks = this.cachedLatestHeight !== undefined
89
+ ? Math.floor((Date.now() - this.cacheTimestampMs) / MidnightSyncState.CHAIN_BLOCK_TIME_MS)
90
+ : 0;
91
+ if (
92
+ this.cachedLatestHeight === undefined ||
93
+ startHeight >= this.cachedLatestHeight ||
94
+ estimatedNewBlocks >= MidnightSyncState.REFRESH_EVERY_CHAIN_BLOCKS
95
+ ) {
96
+ const latestBlockResult = yield* call(() => this.client.fetchLatestBlock());
97
+ this.cachedLatestHeight = latestBlockResult.block.height;
98
+ this.cacheTimestampMs = Date.now();
99
+ }
100
+ const latestHeight = this.cachedLatestHeight;
101
+
83
102
  if (startHeight >= latestHeight) {
84
103
  return undefined;
85
104
  }