@effectstream/sync 0.100.17 → 0.100.19
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/CLAUDE.md +206 -0
- package/README.md +49 -0
- package/docs/ADDING-A-SYNC-PROTOCOL.md +164 -0
- package/package.json +5 -5
- package/src/sync-protocols/avail/state.ts +11 -0
- package/src/sync-protocols/base/state.ts +37 -9
- package/src/sync-protocols/bitcoin/state.ts +13 -1
- package/src/sync-protocols/celestia/state.ts +11 -0
- package/src/sync-protocols/common/page-helpers.ts +37 -0
- package/src/sync-protocols/common/root.ts +9 -0
- package/src/sync-protocols/evm/state.ts +12 -1
- package/src/sync-protocols/midnight/state.ts +11 -0
- package/src/sync-protocols/mod.ts +4 -0
- package/src/sync-protocols/near/state.ts +11 -0
- package/src/sync-protocols/ntp/state.ts +13 -1
- package/src/sync-protocols/orchestration/merge.ts +16 -0
- package/src/sync-protocols/test/control.ts +27 -0
- package/src/sync-protocols/test/fetcher.ts +175 -0
- package/src/sync-protocols/test/state.ts +154 -0
- package/src/sync-protocols/test/types.ts +36 -0
- package/src/sync-protocols/types.ts +3 -1
- package/src/sync-protocols/utxorpc/state.ts +18 -0
- package/src/syncProtocolFactory.ts +13 -0
|
@@ -5,6 +5,43 @@ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
|
|
|
5
5
|
import type { PaginatedSyncProtocols } from "../types.ts";
|
|
6
6
|
import type { PageTypeOf } from "../base/state.ts";
|
|
7
7
|
|
|
8
|
+
// Fetch backpressure. See README "Backpressure (`maxBufferedPages`)".
|
|
9
|
+
const MAX_BUFFER_MULTIPLE = 4;
|
|
10
|
+
// Fallback chunk size for chains that fetch without an explicit stepSize
|
|
11
|
+
// (e.g. utxorpc streams one block per pass).
|
|
12
|
+
const DEFAULT_STEP_SIZE = 1000;
|
|
13
|
+
|
|
14
|
+
/** Resolved fetch-backpressure cap for a chain. See README "Backpressure". */
|
|
15
|
+
export function bufferCapFor(
|
|
16
|
+
syncProtocol: { maxBufferedPages?: number; stepSize?: number },
|
|
17
|
+
): number {
|
|
18
|
+
const step = syncProtocol.stepSize ?? DEFAULT_STEP_SIZE;
|
|
19
|
+
return Math.max(
|
|
20
|
+
syncProtocol.maxBufferedPages ?? MAX_BUFFER_MULTIPLE * step,
|
|
21
|
+
step + 1,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a chain should pause fetching because its in-memory buffer is at/over
|
|
27
|
+
* the cap. Each chain's `stateToInput` calls this first and returns `undefined`
|
|
28
|
+
* when true, so the fetch loop sleeps + retries while the merge drains. Also
|
|
29
|
+
* records backpressure metrics on `state` (for `/debug/metrics`).
|
|
30
|
+
* See README "Backpressure (`maxBufferedPages`)".
|
|
31
|
+
*/
|
|
32
|
+
export function bufferAtCap(
|
|
33
|
+
state: {
|
|
34
|
+
bufferedData: { size(): number };
|
|
35
|
+
recordBackpressure(atCap: boolean, cap: number): void;
|
|
36
|
+
},
|
|
37
|
+
syncProtocol: { maxBufferedPages?: number; stepSize?: number },
|
|
38
|
+
): boolean {
|
|
39
|
+
const cap = bufferCapFor(syncProtocol);
|
|
40
|
+
const atCap = state.bufferedData.size() >= cap;
|
|
41
|
+
state.recordBackpressure(atCap, cap);
|
|
42
|
+
return atCap;
|
|
43
|
+
}
|
|
44
|
+
|
|
8
45
|
type ConfigSubset<Page> = {
|
|
9
46
|
name: string;
|
|
10
47
|
/**
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TimestampMs,
|
|
6
6
|
} from "@effectstream/utils";
|
|
7
7
|
import type { PageRelation } from "../base/page.ts";
|
|
8
|
+
import type { LastPage } from "../base/state.ts";
|
|
8
9
|
import type {
|
|
9
10
|
ConfigSyncProtocolType,
|
|
10
11
|
FlattenSyncProtocolIOFor,
|
|
@@ -19,6 +20,14 @@ export type ChainBlock = {
|
|
|
19
20
|
block_number: BlockNumber;
|
|
20
21
|
blockHash: BlockHash;
|
|
21
22
|
}[];
|
|
23
|
+
/**
|
|
24
|
+
* Per-protocol block-accurate resume markers the runtime persists on commit.
|
|
25
|
+
* See `@effectstream/sync` CLAUDE.md, design idea #5 (restart/resume).
|
|
26
|
+
*/
|
|
27
|
+
resumePages: {
|
|
28
|
+
protocol_name: string;
|
|
29
|
+
lastPage: LastPage<unknown, unknown>;
|
|
30
|
+
}[];
|
|
22
31
|
primitives: (
|
|
23
32
|
& FlattenSyncProtocolIOFor<
|
|
24
33
|
ConfigSyncProtocolType
|
|
@@ -7,7 +7,7 @@ import { bound, type EvmBlockNumber } from "@effectstream/utils";
|
|
|
7
7
|
import { type LastPage, SyncState } from "../base/state.ts";
|
|
8
8
|
import type { RootOutput, RootPage } from "../types.ts";
|
|
9
9
|
import type { EvmFetcher } from "./fetcher.ts";
|
|
10
|
-
import { genInputRange } from "../common/page-helpers.ts";
|
|
10
|
+
import { bufferAtCap, genInputRange } from "../common/page-helpers.ts";
|
|
11
11
|
import type { Input, Output, Page } from "./types.ts";
|
|
12
12
|
import { toMsTimestamp } from "./types.ts";
|
|
13
13
|
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
@@ -53,6 +53,7 @@ export class EvmSyncState extends SyncState<
|
|
|
53
53
|
|
|
54
54
|
@bound
|
|
55
55
|
override *stateToInput(): Operation<Input | undefined> {
|
|
56
|
+
if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
|
|
56
57
|
return yield* genInputRange(
|
|
57
58
|
this as EvmSyncState,
|
|
58
59
|
this.config.syncProtocol.startBlockHeight as EvmBlockNumber,
|
|
@@ -90,6 +91,16 @@ export class EvmSyncState extends SyncState<
|
|
|
90
91
|
rootOutput.blockInfo.push(...blockInfo);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
@bound
|
|
95
|
+
override outputToLastPage(data: Output): LastPage<Page, RootPage> {
|
|
96
|
+
const block = Number(data.raw.number);
|
|
97
|
+
return {
|
|
98
|
+
own: block,
|
|
99
|
+
ownBlockNumber: block,
|
|
100
|
+
root: this.toRootPage(data),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
@bound
|
|
94
105
|
override getNamespace(): string[] {
|
|
95
106
|
return [this.config.network.name, this.config.syncProtocol.name];
|
|
@@ -10,6 +10,7 @@ import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/c
|
|
|
10
10
|
import { getPage } from "@effectstream/db";
|
|
11
11
|
import { MidnightClient } from "./MidnightClient.ts";
|
|
12
12
|
import { applyDelay } from "../common/utils.ts";
|
|
13
|
+
import { bufferAtCap } from "../common/page-helpers.ts";
|
|
13
14
|
|
|
14
15
|
type LatestBlock = {
|
|
15
16
|
block: {
|
|
@@ -72,6 +73,7 @@ export class MidnightSyncState extends SyncState<
|
|
|
72
73
|
|
|
73
74
|
@bound
|
|
74
75
|
override *stateToInput(): Operation<Input | undefined> {
|
|
76
|
+
if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
|
|
75
77
|
const latestBlockResult = yield* call(() => this.client.fetchLatestBlock());
|
|
76
78
|
const latestHeight = latestBlockResult.block.height;
|
|
77
79
|
|
|
@@ -110,6 +112,15 @@ export class MidnightSyncState extends SyncState<
|
|
|
110
112
|
rootOutput.primitives.push(...primitives);
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
@bound
|
|
116
|
+
override outputToLastPage(data: Output): LastPage<Page, RootPage> {
|
|
117
|
+
return {
|
|
118
|
+
own: { height: data.raw.height, hash: data.raw.hash },
|
|
119
|
+
ownBlockNumber: data.raw.height,
|
|
120
|
+
root: this.toRootPage(data),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
@bound
|
|
114
125
|
override getNamespace(): string[] {
|
|
115
126
|
return [this.config.network.name, this.config.syncProtocol.name];
|
|
@@ -19,6 +19,10 @@ export * from "./utxorpc/state.ts";
|
|
|
19
19
|
export * from "./celestia/fetcher.ts";
|
|
20
20
|
export * from "./celestia/state.ts";
|
|
21
21
|
|
|
22
|
+
export * from "./test/fetcher.ts";
|
|
23
|
+
export * from "./test/state.ts";
|
|
24
|
+
export * from "./test/control.ts";
|
|
25
|
+
|
|
22
26
|
export type * from "./base/fetcher.ts";
|
|
23
27
|
export type * from "./base/state.ts";
|
|
24
28
|
export * from "./orchestration/merge.ts";
|
|
@@ -10,6 +10,7 @@ import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/c
|
|
|
10
10
|
import { getPage } from "@effectstream/db";
|
|
11
11
|
import { NearClient } from "./NearClient.ts";
|
|
12
12
|
import { applyDelay } from "../common/utils.ts";
|
|
13
|
+
import { bufferAtCap } from "../common/page-helpers.ts";
|
|
13
14
|
|
|
14
15
|
export class NearSyncState extends SyncState<
|
|
15
16
|
Input,
|
|
@@ -60,6 +61,7 @@ export class NearSyncState extends SyncState<
|
|
|
60
61
|
|
|
61
62
|
@bound
|
|
62
63
|
override *stateToInput(): Operation<Input | undefined> {
|
|
64
|
+
if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
|
|
63
65
|
const latestHeight = yield* call(() =>
|
|
64
66
|
this.client.getLatestBlockHeight()
|
|
65
67
|
);
|
|
@@ -98,6 +100,15 @@ export class NearSyncState extends SyncState<
|
|
|
98
100
|
rootOutput.primitives.push(...primitives);
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
@bound
|
|
104
|
+
override outputToLastPage(data: Output): LastPage<Page, RootPage> {
|
|
105
|
+
return {
|
|
106
|
+
own: { height: data.height, hash: data.hash },
|
|
107
|
+
ownBlockNumber: data.height,
|
|
108
|
+
root: this.toRootPage(data),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
@bound
|
|
102
113
|
override getNamespace(): string[] {
|
|
103
114
|
return [this.config.network.name, this.config.syncProtocol.name];
|
|
@@ -7,7 +7,7 @@ import { bound, type NtpBlockNumber } from "@effectstream/utils";
|
|
|
7
7
|
import { type LastPage, SyncState } from "../base/state.ts";
|
|
8
8
|
import type { RootOutput, RootPage } from "../types.ts";
|
|
9
9
|
import type { NtpFetcher } from "./fetcher.ts";
|
|
10
|
-
import { genInputRange } from "../common/page-helpers.ts";
|
|
10
|
+
import { bufferAtCap, genInputRange } from "../common/page-helpers.ts";
|
|
11
11
|
import type { Input, Output, Page } from "./types.ts";
|
|
12
12
|
import { toMsTimestamp } from "./types.ts";
|
|
13
13
|
import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
|
|
@@ -48,6 +48,7 @@ export class NtpSyncState extends SyncState<
|
|
|
48
48
|
})),
|
|
49
49
|
blockNumber: Number(data.raw.blockNumber),
|
|
50
50
|
timestamp: this.toRootPage(data),
|
|
51
|
+
resumePages: [],
|
|
51
52
|
primitives: [],
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -59,6 +60,7 @@ export class NtpSyncState extends SyncState<
|
|
|
59
60
|
|
|
60
61
|
@bound
|
|
61
62
|
override *stateToInput(): Operation<Input | undefined> {
|
|
63
|
+
if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
|
|
62
64
|
return yield* genInputRange(
|
|
63
65
|
this as NtpSyncState,
|
|
64
66
|
1, // NTP Pages start from 1, and the fetcher offsets using the start page (date)
|
|
@@ -86,6 +88,16 @@ export class NtpSyncState extends SyncState<
|
|
|
86
88
|
throw new Error("Only parallel chains merge into root");
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
@bound
|
|
92
|
+
override outputToLastPage(data: Output): LastPage<Page, RootPage> {
|
|
93
|
+
const block = Number(data.raw.blockNumber);
|
|
94
|
+
return {
|
|
95
|
+
own: block,
|
|
96
|
+
ownBlockNumber: block,
|
|
97
|
+
root: this.toRootPage(data),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
@bound
|
|
90
102
|
override getNamespace(): string[] {
|
|
91
103
|
return [this.config.network.name, this.config.syncProtocol.name];
|
|
@@ -124,6 +124,11 @@ export function* mergeIntoRoot<SyncProtocol extends AllSyncProtocols>(
|
|
|
124
124
|
}
|
|
125
125
|
const output = iState.bufferedData.peekAt(0)!;
|
|
126
126
|
const newRoot = iState.toRootOutput(output.output);
|
|
127
|
+
// Resume marker: the single datum this root block is built from (CLAUDE.md #5).
|
|
128
|
+
newRoot.resumePages.push({
|
|
129
|
+
protocol_name: state.name,
|
|
130
|
+
lastPage: iState.outputToLastPage(output.output),
|
|
131
|
+
});
|
|
127
132
|
return {
|
|
128
133
|
updateCache: () => {
|
|
129
134
|
output.cleanup();
|
|
@@ -168,6 +173,17 @@ export function* mergeIntoRoot<SyncProtocol extends AllSyncProtocols>(
|
|
|
168
173
|
iState.mergeDatum,
|
|
169
174
|
);
|
|
170
175
|
|
|
176
|
+
// Resume marker: the last (highest) datum merged this iteration, still in the
|
|
177
|
+
// Deque at `cleanups.length - 1` (cleanup runs after send). Nothing consumed →
|
|
178
|
+
// no marker. See CLAUDE.md #5.
|
|
179
|
+
if (cleanups.length > 0) {
|
|
180
|
+
const lastConsumed = iState.bufferedData.peekAt(cleanups.length - 1)!;
|
|
181
|
+
rootInfo.value.resumePages.push({
|
|
182
|
+
protocol_name: state.name,
|
|
183
|
+
lastPage: iState.outputToLastPage(lastConsumed.output),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
171
187
|
return {
|
|
172
188
|
updateCache: () => {
|
|
173
189
|
state.bufferedData.remove(0, cleanups.length);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only control plane for the synthetic `test` sync protocol.
|
|
3
|
+
*
|
|
4
|
+
* The chain tip ("latest block") is held in a process-wide registry keyed by
|
|
5
|
+
* sync-protocol name, rather than in config, so a single test can advance it
|
|
6
|
+
* deterministically between simulated restarts. (Immutable config fields like
|
|
7
|
+
* `startTime` cannot change across a restart, so they can't drive this.)
|
|
8
|
+
*
|
|
9
|
+
* When no tip is registered for a protocol, the fetcher falls back to a
|
|
10
|
+
* wall-clock tip derived from `startTime`/`blockTimeMS` (see fetcher.ts).
|
|
11
|
+
*/
|
|
12
|
+
const tips = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
export const TestChainControl = {
|
|
15
|
+
/** Set the current chain tip (latest available block) for a protocol. */
|
|
16
|
+
setTip(protocolName: string, tip: number): void {
|
|
17
|
+
tips.set(protocolName, tip);
|
|
18
|
+
},
|
|
19
|
+
/** Get the manually-controlled tip, or undefined to fall back to wall-clock. */
|
|
20
|
+
getTip(protocolName: string): number | undefined {
|
|
21
|
+
return tips.get(protocolName);
|
|
22
|
+
},
|
|
23
|
+
/** Reset all tips (call between tests). */
|
|
24
|
+
clear(): void {
|
|
25
|
+
tips.clear();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseDataFetcher,
|
|
3
|
+
type DataFetched,
|
|
4
|
+
type PaginatedFetcher,
|
|
5
|
+
} from "../base/fetcher.ts";
|
|
6
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
7
|
+
import type { Operation } from "effection";
|
|
8
|
+
import { bound } from "@effectstream/utils";
|
|
9
|
+
import { blockNumberRelation } from "../common/utils.ts";
|
|
10
|
+
import type { Input, Output, Page, PrimitiveType } from "./types.ts";
|
|
11
|
+
import {
|
|
12
|
+
fetchNewestPage,
|
|
13
|
+
type PageRange,
|
|
14
|
+
type PageRequest,
|
|
15
|
+
} from "../base/page.ts";
|
|
16
|
+
import type { PrimitiveFetcher } from "../base/primitive.ts";
|
|
17
|
+
import type { OutputAndCleanup, RootConversion } from "../base/state.ts";
|
|
18
|
+
import {
|
|
19
|
+
ConfigSyncProtocolType,
|
|
20
|
+
type ConfigNetworkType,
|
|
21
|
+
type PrimitiveEntry,
|
|
22
|
+
type SyncProtocolWithNetwork,
|
|
23
|
+
} from "@effectstream/config";
|
|
24
|
+
import { TestChainControl } from "./control.ts";
|
|
25
|
+
|
|
26
|
+
type TestConfig = Extract<
|
|
27
|
+
SyncProtocolWithNetwork,
|
|
28
|
+
{ networkType: ConfigNetworkType.TEST }
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
type TestEvent = { atBlock: number; payload: Record<string, unknown> };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Synthetic chain fetcher. Blocks are computed arithmetically (no RPC):
|
|
35
|
+
* timestamp(page) = startTime + blockTimeMS * page
|
|
36
|
+
* The chain tip comes from {@link TestChainControl} when set (deterministic
|
|
37
|
+
* tests), otherwise from the wall clock. Configured `events` are emitted as
|
|
38
|
+
* primitives when their `atBlock` is reached.
|
|
39
|
+
*/
|
|
40
|
+
export class TestFetcher
|
|
41
|
+
extends BaseDataFetcher<Input, Output, RootOutput, Page, RootPage>
|
|
42
|
+
implements
|
|
43
|
+
PrimitiveFetcher<
|
|
44
|
+
Input,
|
|
45
|
+
Page,
|
|
46
|
+
{ blockNumber: number; timestamp: bigint; hash: string },
|
|
47
|
+
PrimitiveType,
|
|
48
|
+
ConfigSyncProtocolType.TEST_PARALLEL
|
|
49
|
+
>,
|
|
50
|
+
PaginatedFetcher<Page> {
|
|
51
|
+
constructor(
|
|
52
|
+
readonly config: TestConfig,
|
|
53
|
+
) {
|
|
54
|
+
super(config.syncProtocol.name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private blockOf(page: Page): Output["raw"] {
|
|
58
|
+
const timestamp = BigInt(this.config.network.startTime) +
|
|
59
|
+
BigInt(this.config.network.blockTimeMS) * BigInt(page);
|
|
60
|
+
return {
|
|
61
|
+
blockNumber: page,
|
|
62
|
+
timestamp,
|
|
63
|
+
hash: `0x${timestamp.toString(16)}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@bound
|
|
68
|
+
override *readData(
|
|
69
|
+
data: Input,
|
|
70
|
+
rootConversion: RootConversion<Output, RootOutput, RootPage>,
|
|
71
|
+
): Operation<DataFetched<Output, Page, RootPage>> {
|
|
72
|
+
// Events are declared on the (parallel) sync protocol config. The primitive
|
|
73
|
+
// instance name to tag them with is the first declared primitive's id.
|
|
74
|
+
const events: TestEvent[] =
|
|
75
|
+
(this.config.syncProtocol as { events?: TestEvent[] }).events ?? [];
|
|
76
|
+
const primitiveId = this.config.primitives[0]?.id;
|
|
77
|
+
|
|
78
|
+
const output: OutputAndCleanup<Output>[] = [];
|
|
79
|
+
for (let page = data.from; page <= data.to; page++) {
|
|
80
|
+
const raw = this.blockOf(page);
|
|
81
|
+
const primitives: PrimitiveType[] = [];
|
|
82
|
+
if (primitiveId != null) {
|
|
83
|
+
for (const ev of events) {
|
|
84
|
+
if (ev.atBlock === page) {
|
|
85
|
+
primitives.push({
|
|
86
|
+
syncProtocol: {
|
|
87
|
+
name: ConfigSyncProtocolType.TEST_PARALLEL,
|
|
88
|
+
blockNumber: page,
|
|
89
|
+
transactionHash: `0x${page.toString(16)}`,
|
|
90
|
+
contractAddress: "0x0",
|
|
91
|
+
},
|
|
92
|
+
primitive: primitiveId,
|
|
93
|
+
output: { payloadType: "test-event", payload: ev.payload },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
output.push({
|
|
99
|
+
output: { raw, primitives, blockHashes: [raw.hash] },
|
|
100
|
+
cleanup: () => {},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
output,
|
|
106
|
+
lastPage: {
|
|
107
|
+
own: Number(data.to),
|
|
108
|
+
ownBlockNumber: Number(data.to),
|
|
109
|
+
root: rootConversion.toRootPage({
|
|
110
|
+
raw: this.blockOf(Number(data.to)),
|
|
111
|
+
primitives: [],
|
|
112
|
+
blockHashes: [],
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@bound
|
|
119
|
+
groupByPage(
|
|
120
|
+
_primitives: PrimitiveType[],
|
|
121
|
+
): Record<string, PrimitiveType[]> {
|
|
122
|
+
// Primitives are attached inline in readData; not used.
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@bound
|
|
127
|
+
*readPrimitives(
|
|
128
|
+
_data: Input,
|
|
129
|
+
_pageRequest: PageRequest<Page, { timestamp: bigint; hash: string }>,
|
|
130
|
+
_primitiveEntries: Extract<
|
|
131
|
+
PrimitiveEntry,
|
|
132
|
+
{ syncProtocol: ConfigSyncProtocolType.TEST_PARALLEL }
|
|
133
|
+
>[],
|
|
134
|
+
): Operation<PrimitiveType[]> {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@bound
|
|
139
|
+
*getLatestPage(
|
|
140
|
+
knownLastPage: undefined | Page,
|
|
141
|
+
): Operation<Page> {
|
|
142
|
+
const self = this;
|
|
143
|
+
return yield* fetchNewestPage<Page>(
|
|
144
|
+
blockNumberRelation,
|
|
145
|
+
function* (): Operation<Page> {
|
|
146
|
+
const manual = TestChainControl.getTip(self.config.syncProtocol.name);
|
|
147
|
+
if (manual != null) return manual;
|
|
148
|
+
const diff = Date.now() - self.config.network.startTime;
|
|
149
|
+
return Math.floor(diff / self.config.network.blockTimeMS);
|
|
150
|
+
},
|
|
151
|
+
knownLastPage,
|
|
152
|
+
this.config.syncProtocol.pollingInterval,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@bound
|
|
157
|
+
previousInterval(nextIntervalStart: Page): PageRange<Page> {
|
|
158
|
+
return this.intervalFromStart(
|
|
159
|
+
nextIntervalStart - this.config.syncProtocol.stepSize - 1,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@bound
|
|
164
|
+
nextInterval(prevIntervalEnd: Page): PageRange<Page> {
|
|
165
|
+
return this.intervalFromStart(prevIntervalEnd + 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@bound
|
|
169
|
+
intervalFromStart(start: Page): PageRange<Page> {
|
|
170
|
+
return {
|
|
171
|
+
from: start,
|
|
172
|
+
to: start + this.config.syncProtocol.stepSize,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { applyDelay, blockNumberRelation } from "../common/utils.ts";
|
|
2
|
+
import type { Operation } from "effection";
|
|
3
|
+
import { call } from "effection";
|
|
4
|
+
import { getPage } from "@effectstream/db";
|
|
5
|
+
import type { PoolClient } from "pg";
|
|
6
|
+
import { bound, type BlockNumber } from "@effectstream/utils";
|
|
7
|
+
import { type LastPage, SyncState } from "../base/state.ts";
|
|
8
|
+
import type { RootOutput, RootPage } from "../types.ts";
|
|
9
|
+
import type { TestFetcher } from "./fetcher.ts";
|
|
10
|
+
import { bufferAtCap, genInputRange } from "../common/page-helpers.ts";
|
|
11
|
+
import type { Input, Output, Page } from "./types.ts";
|
|
12
|
+
import { toMsTimestamp } from "./types.ts";
|
|
13
|
+
import type {
|
|
14
|
+
ConfigNetworkType,
|
|
15
|
+
SyncProtocolWithNetwork,
|
|
16
|
+
} from "@effectstream/config";
|
|
17
|
+
|
|
18
|
+
type TestConfig = Extract<
|
|
19
|
+
SyncProtocolWithNetwork,
|
|
20
|
+
{ networkType: ConfigNetworkType.TEST }
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Synthetic test chain state. Implements BOTH roles:
|
|
25
|
+
* - as a TEST_MAIN clock, `toRootOutput` produces root blocks;
|
|
26
|
+
* - as a TEST_PARALLEL source, `mergeDatum` folds its primitives into the root.
|
|
27
|
+
* The merge only calls the method appropriate to the chain's position (main at
|
|
28
|
+
* index 0), so implementing both is safe.
|
|
29
|
+
*/
|
|
30
|
+
export class TestSyncState extends SyncState<
|
|
31
|
+
Input,
|
|
32
|
+
Output,
|
|
33
|
+
Page,
|
|
34
|
+
RootOutput,
|
|
35
|
+
RootPage,
|
|
36
|
+
TestFetcher
|
|
37
|
+
> {
|
|
38
|
+
constructor(
|
|
39
|
+
lastPage: LastPage<Page, RootPage>,
|
|
40
|
+
readonly config: TestConfig,
|
|
41
|
+
fetcher: TestFetcher,
|
|
42
|
+
dbConn: PoolClient,
|
|
43
|
+
) {
|
|
44
|
+
super(
|
|
45
|
+
config.syncProtocol.name,
|
|
46
|
+
lastPage,
|
|
47
|
+
fetcher,
|
|
48
|
+
blockNumberRelation,
|
|
49
|
+
dbConn,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@bound
|
|
54
|
+
override toRootOutput(data: Output): RootOutput {
|
|
55
|
+
return {
|
|
56
|
+
blockInfo: data.blockHashes.map((h) => ({
|
|
57
|
+
protocol_name: this.config.syncProtocol.name,
|
|
58
|
+
block_number: Number(data.raw.blockNumber),
|
|
59
|
+
blockHash: h,
|
|
60
|
+
})),
|
|
61
|
+
blockNumber: Number(data.raw.blockNumber),
|
|
62
|
+
timestamp: this.toRootPage(data),
|
|
63
|
+
resumePages: [],
|
|
64
|
+
primitives: data.primitives.map((p) => ({
|
|
65
|
+
...p,
|
|
66
|
+
source: this.config.syncProtocol.name,
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@bound
|
|
72
|
+
override toRootPage(data: Output): RootPage {
|
|
73
|
+
return applyDelay(
|
|
74
|
+
toMsTimestamp(data.raw.timestamp),
|
|
75
|
+
(this.config.syncProtocol as { delayMs?: number }).delayMs ?? 0,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@bound
|
|
80
|
+
override *stateToInput(): Operation<Input | undefined> {
|
|
81
|
+
if (bufferAtCap(this, this.config.syncProtocol)) return undefined;
|
|
82
|
+
return yield* genInputRange(
|
|
83
|
+
this as TestSyncState,
|
|
84
|
+
this.config.syncProtocol.startBlockHeight as BlockNumber,
|
|
85
|
+
{
|
|
86
|
+
name: this.config.syncProtocol.name,
|
|
87
|
+
startPage: this.config.syncProtocol.startBlockHeight as BlockNumber,
|
|
88
|
+
},
|
|
89
|
+
this.getNamespace(),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@bound
|
|
94
|
+
override toPage(
|
|
95
|
+
_input: Input,
|
|
96
|
+
_data: Output[],
|
|
97
|
+
): Page {
|
|
98
|
+
return Number(_input.to);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@bound
|
|
102
|
+
override mergeDatum(
|
|
103
|
+
ourOutput: Output,
|
|
104
|
+
rootOutput: RootOutput,
|
|
105
|
+
): void {
|
|
106
|
+
const primitives = ourOutput.primitives.map((p) => ({
|
|
107
|
+
...p,
|
|
108
|
+
source: this.config.syncProtocol.name,
|
|
109
|
+
}));
|
|
110
|
+
const blockInfo = ourOutput.blockHashes.map((h) => ({
|
|
111
|
+
protocol_name: this.config.syncProtocol.name,
|
|
112
|
+
block_number: Number(ourOutput.raw.blockNumber),
|
|
113
|
+
blockHash: h,
|
|
114
|
+
}));
|
|
115
|
+
rootOutput.primitives.push(...primitives);
|
|
116
|
+
rootOutput.blockInfo.push(...blockInfo);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@bound
|
|
120
|
+
override outputToLastPage(data: Output): LastPage<Page, RootPage> {
|
|
121
|
+
const block = Number(data.raw.blockNumber);
|
|
122
|
+
return {
|
|
123
|
+
own: block,
|
|
124
|
+
ownBlockNumber: block as BlockNumber,
|
|
125
|
+
root: this.toRootPage(data),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@bound
|
|
130
|
+
override getNamespace(): string[] {
|
|
131
|
+
return [this.config.network.name, this.config.syncProtocol.name];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static *restoreState(
|
|
135
|
+
dbConn: PoolClient,
|
|
136
|
+
config: TestConfig,
|
|
137
|
+
fetcher: TestFetcher,
|
|
138
|
+
): Operation<TestSyncState> {
|
|
139
|
+
const [result] = yield* call(async () =>
|
|
140
|
+
await getPage.run({
|
|
141
|
+
protocol_name: config.syncProtocol.name,
|
|
142
|
+
}, dbConn)
|
|
143
|
+
);
|
|
144
|
+
const page: any = result
|
|
145
|
+
? result.page as unknown as LastPage<Page, RootPage>
|
|
146
|
+
: undefined;
|
|
147
|
+
return new TestSyncState(
|
|
148
|
+
page,
|
|
149
|
+
config,
|
|
150
|
+
fetcher,
|
|
151
|
+
dbConn,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BlockNumber, TimestampMs } from "@effectstream/utils";
|
|
2
|
+
import type { PageSyncRange } from "../common/page-helpers.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ConfigNetworkType,
|
|
5
|
+
ConfigSyncProtocolType,
|
|
6
|
+
FlattenSyncProtocolIOFor,
|
|
7
|
+
SyncProtocolWithNetwork,
|
|
8
|
+
} from "@effectstream/config";
|
|
9
|
+
|
|
10
|
+
export type Page = BlockNumber;
|
|
11
|
+
|
|
12
|
+
export type ConfigType = Extract<
|
|
13
|
+
SyncProtocolWithNetwork,
|
|
14
|
+
{ networkType: ConfigNetworkType.TEST }
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/** Primitives a TEST_PARALLEL chain emits (one per configured event in range). */
|
|
18
|
+
export type PrimitiveType = FlattenSyncProtocolIOFor<
|
|
19
|
+
ConfigSyncProtocolType.TEST_PARALLEL
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type Input = PageSyncRange<Page>;
|
|
23
|
+
|
|
24
|
+
export type Output = {
|
|
25
|
+
raw: {
|
|
26
|
+
timestamp: bigint;
|
|
27
|
+
hash: string;
|
|
28
|
+
blockNumber: number;
|
|
29
|
+
};
|
|
30
|
+
primitives: PrimitiveType[];
|
|
31
|
+
blockHashes: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function toMsTimestamp(timestamp: bigint): TimestampMs {
|
|
35
|
+
return Number(timestamp);
|
|
36
|
+
}
|
|
@@ -10,6 +10,7 @@ import type { AvailSyncState } from "./avail/state.ts";
|
|
|
10
10
|
import type { BitcoinSyncState } from "./bitcoin/state.ts";
|
|
11
11
|
import type { CelestiaSyncState } from "./celestia/state.ts";
|
|
12
12
|
import type { NearSyncState } from "./near/state.ts";
|
|
13
|
+
import type { TestSyncState } from "./test/state.ts";
|
|
13
14
|
|
|
14
15
|
// TODO: move folders
|
|
15
16
|
export type RootOutput = ChainBlock;
|
|
@@ -24,7 +25,8 @@ export type AllSyncProtocols =
|
|
|
24
25
|
| AvailSyncState
|
|
25
26
|
| BitcoinSyncState
|
|
26
27
|
| CelestiaSyncState
|
|
27
|
-
| NearSyncState
|
|
28
|
+
| NearSyncState
|
|
29
|
+
| TestSyncState;
|
|
28
30
|
export type ISyncProtocol = UnionToIntersection<AllSyncProtocols>;
|
|
29
31
|
|
|
30
32
|
type toPaginated<T> = T extends { fetcher: PaginatedFetcher<infer Page> } ? T
|