@effectstream/sync 0.100.18 → 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.
@@ -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