@effectstream/sync 0.100.24 → 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.
|
|
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.
|
|
17
|
-
"@effectstream/db": "0.100.
|
|
18
|
-
"@effectstream/log": "0.100.
|
|
19
|
-
"@effectstream/utils": "0.100.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|