@effectstream/sync 0.100.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.
Files changed (45) hide show
  1. package/package.json +29 -0
  2. package/src/mod.ts +3 -0
  3. package/src/sync-protocols/avail/AvailClient.ts +120 -0
  4. package/src/sync-protocols/avail/fetcher.ts +174 -0
  5. package/src/sync-protocols/avail/state.ts +134 -0
  6. package/src/sync-protocols/avail/types.ts +92 -0
  7. package/src/sync-protocols/base/fetcher.ts +43 -0
  8. package/src/sync-protocols/base/page.ts +89 -0
  9. package/src/sync-protocols/base/primitive.ts +24 -0
  10. package/src/sync-protocols/base/state.ts +143 -0
  11. package/src/sync-protocols/bitcoin/fetcher.ts +549 -0
  12. package/src/sync-protocols/bitcoin/state.ts +144 -0
  13. package/src/sync-protocols/bitcoin/types.ts +92 -0
  14. package/src/sync-protocols/celestia/CelestiaClient.ts +139 -0
  15. package/src/sync-protocols/celestia/fetcher.ts +165 -0
  16. package/src/sync-protocols/celestia/state.ts +131 -0
  17. package/src/sync-protocols/celestia/types.ts +46 -0
  18. package/src/sync-protocols/common/page-helpers.ts +65 -0
  19. package/src/sync-protocols/common/root.ts +35 -0
  20. package/src/sync-protocols/common/utils.ts +29 -0
  21. package/src/sync-protocols/evm/fetcher.ts +290 -0
  22. package/src/sync-protocols/evm/state.ts +122 -0
  23. package/src/sync-protocols/evm/types.ts +53 -0
  24. package/src/sync-protocols/midnight/MidnightClient.ts +210 -0
  25. package/src/sync-protocols/midnight/fetcher.ts +252 -0
  26. package/src/sync-protocols/midnight/state.ts +147 -0
  27. package/src/sync-protocols/midnight/types.ts +128 -0
  28. package/src/sync-protocols/midnight/zswap-decoder.ts +123 -0
  29. package/src/sync-protocols/mod.ts +29 -0
  30. package/src/sync-protocols/near/NearClient.ts +178 -0
  31. package/src/sync-protocols/near/fetcher.ts +274 -0
  32. package/src/sync-protocols/near/state.ts +130 -0
  33. package/src/sync-protocols/near/types.ts +46 -0
  34. package/src/sync-protocols/ntp/fetcher.ts +199 -0
  35. package/src/sync-protocols/ntp/state.ts +118 -0
  36. package/src/sync-protocols/ntp/types.ts +40 -0
  37. package/src/sync-protocols/orchestration/merge.ts +215 -0
  38. package/src/sync-protocols/orchestration/sync.ts +74 -0
  39. package/src/sync-protocols/types.ts +32 -0
  40. package/src/sync-protocols/utxorpc/BufferedRpc.ts +169 -0
  41. package/src/sync-protocols/utxorpc/fetcher.ts +116 -0
  42. package/src/sync-protocols/utxorpc/state.ts +186 -0
  43. package/src/sync-protocols/utxorpc/types.ts +53 -0
  44. package/src/sync-protocols/utxorpc/utils.ts +80 -0
  45. package/src/syncProtocolFactory.ts +149 -0
@@ -0,0 +1,118 @@
1
+ import { 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 NtpBlockNumber } from "@effectstream/utils";
7
+ import { type LastPage, SyncState } from "../base/state.ts";
8
+ import type { RootOutput, RootPage } from "../types.ts";
9
+ import type { NtpFetcher } from "./fetcher.ts";
10
+ import { genInputRange } from "../common/page-helpers.ts";
11
+ import type { Input, Output, Page } from "./types.ts";
12
+ import { toMsTimestamp } from "./types.ts";
13
+ import type { ConfigNetworkType, SyncProtocolWithNetwork } from "@effectstream/config";
14
+
15
+ export class NtpSyncState extends SyncState<
16
+ Input,
17
+ Output,
18
+ Page,
19
+ RootOutput,
20
+ RootPage,
21
+ NtpFetcher
22
+ > {
23
+ constructor(
24
+ lastPage: LastPage<Page, RootPage>,
25
+ readonly config: Extract<
26
+ SyncProtocolWithNetwork,
27
+ { networkType: ConfigNetworkType.NTP }
28
+ >,
29
+ fetcher: NtpFetcher,
30
+ dbConn: PoolClient,
31
+ ) {
32
+ super(
33
+ config.syncProtocol.name,
34
+ lastPage,
35
+ fetcher,
36
+ blockNumberRelation,
37
+ dbConn,
38
+ );
39
+ }
40
+
41
+ @bound
42
+ override toRootOutput(data: Output): RootOutput {
43
+ return {
44
+ blockInfo: data.blockHashes.map((h) => ({
45
+ protocol_name: this.config.syncProtocol.name,
46
+ block_number: Number(data.raw.blockNumber),
47
+ blockHash: h,
48
+ })),
49
+ blockNumber: Number(data.raw.blockNumber),
50
+ timestamp: this.toRootPage(data),
51
+ primitives: [],
52
+ };
53
+ }
54
+
55
+ @bound
56
+ override toRootPage(data: Output): RootPage {
57
+ return toMsTimestamp(data.raw.timestamp);
58
+ }
59
+
60
+ @bound
61
+ override *stateToInput(): Operation<Input | undefined> {
62
+ return yield* genInputRange(
63
+ this as NtpSyncState,
64
+ 1, // NTP Pages start from 1, and the fetcher offsets using the start page (date)
65
+ {
66
+ name: this.config.syncProtocol.name,
67
+ startPage: this.config.syncProtocol.startBlockHeight as NtpBlockNumber,
68
+ },
69
+ this.getNamespace(),
70
+ );
71
+ }
72
+
73
+ @bound
74
+ override toPage(
75
+ input: Input,
76
+ data: Output[],
77
+ ): Page {
78
+ return Number(input.to);
79
+ }
80
+
81
+ @bound
82
+ override mergeDatum(
83
+ ourOutput: Output,
84
+ rootOutput: RootOutput,
85
+ ): void {
86
+ throw new Error("Only parallel chains merge into root");
87
+ }
88
+
89
+ @bound
90
+ override getNamespace(): string[] {
91
+ return [this.config.network.name, this.config.syncProtocol.name];
92
+ }
93
+
94
+ static *restoreState(
95
+ dbConn: PoolClient,
96
+ config: Extract<
97
+ SyncProtocolWithNetwork,
98
+ { networkType: ConfigNetworkType.NTP }
99
+ >,
100
+ fetcher: NtpFetcher,
101
+ ): Operation<NtpSyncState> {
102
+ // TODO: move this DB query into page-helpers?
103
+ const [result] = yield* call(async () =>
104
+ await getPage.run({
105
+ protocol_name: config.syncProtocol.name,
106
+ }, dbConn)
107
+ );
108
+ const page: any = result
109
+ ? result.page as unknown as LastPage<Page, RootPage>
110
+ : undefined;
111
+ return new NtpSyncState(
112
+ page,
113
+ config,
114
+ fetcher,
115
+ dbConn,
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,40 @@
1
+ import type {
2
+ BlockNumber,
3
+ NtpBlockHash,
4
+ NtpBlockNumber,
5
+ NtpPageJson,
6
+ TimestampMs,
7
+ } from "@effectstream/utils";
8
+ import { TypeboxHelpers } from "@effectstream/utils";
9
+ import type { PageSyncRange } from "../common/page-helpers.ts";
10
+ import { Type } from "@sinclair/typebox";
11
+ import type {
12
+ ConfigSyncProtocolType,
13
+ FlattenSyncProtocolIOFor,
14
+ } from "@effectstream/config";
15
+
16
+ export type Page = BlockNumber;
17
+ const PageJsonSchema = Type.Unsafe<
18
+ NtpPageJson
19
+ >(Type.String());
20
+ export const PageSchema = TypeboxHelpers.SerializeObjAsJson<
21
+ Page,
22
+ typeof PageJsonSchema
23
+ >();
24
+
25
+ export type PrimitiveType = FlattenSyncProtocolIOFor<
26
+ ConfigSyncProtocolType.NTP_MAIN
27
+ >;
28
+ export type Input = PageSyncRange<Page>;
29
+ export type Output = {
30
+ raw: {
31
+ timestamp: bigint;
32
+ hash: NtpBlockHash;
33
+ blockNumber: number;
34
+ };
35
+ blockHashes: NtpBlockHash[];
36
+ };
37
+
38
+ export function toMsTimestamp(timestamp: bigint): TimestampMs {
39
+ return Number(timestamp);
40
+ }
@@ -0,0 +1,215 @@
1
+ import type { Channel, Operation } from "effection";
2
+ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
3
+ import type {
4
+ AllSyncProtocols,
5
+ ISyncProtocol,
6
+ RootOutput,
7
+ RootPage,
8
+ } from "../types.ts";
9
+ import type { PageRelation } from "../base/page.ts";
10
+ import type Deque from "denque";
11
+ import type { ChainBlock } from "../common/root.ts";
12
+ import { chainPageRelation } from "../common/root.ts";
13
+ import type { CacheCleanup, OutputAndCleanup } from "../base/state.ts";
14
+
15
+ export function* startMerge(
16
+ syncProtocols: AllSyncProtocols[],
17
+ finalizedBlockStream: Channel<ChainBlock, void>,
18
+ ): Operation<ChainBlock> {
19
+ while (true) {
20
+ let newBlock: ChainBlock | undefined = undefined;
21
+
22
+ const cleanup: CacheCleanup[] = [];
23
+
24
+ for (let i = 0; i < syncProtocols.length; i++) {
25
+ const mergeInfo: MergeResult<RootOutput> = yield* mergeIntoRoot(
26
+ syncProtocols[i],
27
+ {
28
+ value: newBlock,
29
+ toPage: (block) => block.timestamp,
30
+ comparePage: chainPageRelation,
31
+ },
32
+ );
33
+ cleanup.push(mergeInfo.updateCache);
34
+ if (mergeInfo.newRoot != null) {
35
+ newBlock = mergeInfo.newRoot;
36
+ }
37
+ if (i === 0) {
38
+ log.remote(
39
+ ComponentNames.EFFECTSTREAM_SYNC,
40
+ "block-merge",
41
+ SeverityNumber.DEBUG,
42
+ (log) => log(`new block ${mergeInfo.newRoot?.blockNumber}`),
43
+ );
44
+ }
45
+ }
46
+ if (newBlock == null) continue;
47
+ log.remote(
48
+ ComponentNames.EFFECTSTREAM_SYNC,
49
+ "block-merge",
50
+ SeverityNumber.INFO,
51
+ (log) => log(`producing block ${newBlock?.blockNumber}`),
52
+ );
53
+
54
+ yield* finalizedBlockStream.send(newBlock);
55
+
56
+ for (const fn of cleanup) {
57
+ fn();
58
+ }
59
+ }
60
+ }
61
+
62
+ export type MergeResult<RootOutput> = {
63
+ updateCache: () => void;
64
+ /**
65
+ * undefined → replace whatever root might exist previously with this one.
66
+ * this is primarily useful for setting the initial root
67
+ */
68
+ newRoot: undefined | RootOutput;
69
+ };
70
+
71
+ /**
72
+ * Merge as much of the data into the root as possible\
73
+ * Note: it does *not* modify the child data.
74
+ * Instead, it returns a callback to remove elements that were merged.
75
+ * This is to help with error handling
76
+ * since you can avoid removing elements until you're sure no errors have happened
77
+ * given there is no way to recover these elements if an error happens after they get removed.
78
+ *
79
+ * @param value the data being constructed. `undefined` if this is the first piece of data in the chain
80
+ */
81
+ export function* mergeIntoRoot<SyncProtocol extends AllSyncProtocols>(
82
+ state: SyncProtocol,
83
+ rootInfo: {
84
+ value: undefined | RootOutput;
85
+ toPage: (data: RootOutput) => RootPage;
86
+ comparePage: PageRelation<RootPage>;
87
+ },
88
+ ): Operation<MergeResult<RootOutput>> {
89
+ const iState = state as ISyncProtocol;
90
+
91
+ // 1) If we've never made a single query yet, wait
92
+ while (state.lastPage == null) {
93
+ log.remote(
94
+ ComponentNames.EFFECTSTREAM_SYNC,
95
+ state.getNamespace(),
96
+ SeverityNumber.DEBUG,
97
+ (log) => log("Merge blocked (first sync): waiting for page"),
98
+ );
99
+ yield* state.newPageCondVar.wait();
100
+ log.remote(
101
+ ComponentNames.EFFECTSTREAM_SYNC,
102
+ state.getNamespace(),
103
+ SeverityNumber.DEBUG,
104
+ (log) => log("Merge unblocked (first sync): got page"),
105
+ );
106
+ }
107
+
108
+ // 2) Handle case where this is the first in the list of states to merge data from
109
+ if (rootInfo.value == null) {
110
+ while (state.bufferedData.size() === 0) {
111
+ log.remote(
112
+ ComponentNames.EFFECTSTREAM_SYNC,
113
+ state.getNamespace(),
114
+ SeverityNumber.DEBUG,
115
+ (log) => log("Merge blocked (missing main data): waiting for data"),
116
+ );
117
+ yield* state.newDataCondVar.wait();
118
+ log.remote(
119
+ ComponentNames.EFFECTSTREAM_SYNC,
120
+ state.getNamespace(),
121
+ SeverityNumber.DEBUG,
122
+ (log) => log("Merge unblocked (missing main data): got data"),
123
+ );
124
+ }
125
+ const output = iState.bufferedData.peekAt(0)!;
126
+ const newRoot = iState.toRootOutput(output.output);
127
+ return {
128
+ updateCache: () => {
129
+ output.cleanup();
130
+ state.bufferedData.remove(0, 1);
131
+ },
132
+ newRoot,
133
+ };
134
+ }
135
+
136
+ // 3) If a previous state created data, keep merging data from this state into it
137
+ // until we've exceeded the time range
138
+ while (
139
+ rootInfo.comparePage.compare(
140
+ state.lastPage.root,
141
+ rootInfo.toPage(rootInfo.value),
142
+ ) <= 0
143
+ ) {
144
+ log.remote(
145
+ ComponentNames.EFFECTSTREAM_SYNC,
146
+ state.getNamespace(),
147
+ SeverityNumber.DEBUG,
148
+ (log) => log("Merge blocked (missing parallel data): waiting for data"),
149
+ );
150
+ yield* state.newPageCondVar.wait();
151
+ log.remote(
152
+ ComponentNames.EFFECTSTREAM_SYNC,
153
+ state.getNamespace(),
154
+ SeverityNumber.DEBUG,
155
+ (log) => log("Merge unblocked (missing parallel data): got data"),
156
+ );
157
+ }
158
+ const cleanups = getFromBuffer(
159
+ {
160
+ data: iState.bufferedData,
161
+ toPage: iState.toRootPage,
162
+ },
163
+ {
164
+ data: rootInfo.value,
165
+ toPage: rootInfo.toPage,
166
+ },
167
+ rootInfo.comparePage,
168
+ iState.mergeDatum,
169
+ );
170
+
171
+ return {
172
+ updateCache: () => {
173
+ state.bufferedData.remove(0, cleanups.length);
174
+ for (const fn of cleanups) {
175
+ fn();
176
+ }
177
+ },
178
+ newRoot: undefined,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Merge as much of the data into the root as possible
184
+ * Note: it does *not* modify the child data
185
+ *
186
+ * @returns the amount of elements that can be added to the root data
187
+ */
188
+ function getFromBuffer<Output, RootOutput, RootPage>(
189
+ child: {
190
+ data: Deque<OutputAndCleanup<Output>>;
191
+ toPage: (data: Output) => RootPage;
192
+ },
193
+ root: {
194
+ data: RootOutput;
195
+ toPage: (data: RootOutput) => RootPage;
196
+ },
197
+ pageRelation: PageRelation<RootPage>,
198
+ mergeData: (child: Output, root: RootOutput) => void,
199
+ ): CacheCleanup[] {
200
+ const cleanups: CacheCleanup[] = [];
201
+ let nextElem = child.data.peekAt(cleanups.length);
202
+
203
+ while (
204
+ nextElem != null &&
205
+ pageRelation.compare(
206
+ child.toPage(nextElem.output),
207
+ root.toPage(root.data),
208
+ ) <= 0
209
+ ) {
210
+ mergeData(nextElem.output, root.data);
211
+ cleanups.push(nextElem.cleanup);
212
+ nextElem = child.data.peekAt(cleanups.length);
213
+ }
214
+ return cleanups;
215
+ }
@@ -0,0 +1,74 @@
1
+ import type { Operation } from "effection";
2
+ import { sleep, spawn } from "effection";
3
+ import { ComponentNames, log, SeverityNumber } from "@effectstream/log";
4
+ import type { AllSyncProtocols, ISyncProtocol } from "../types.ts";
5
+ import { tryYield } from "@effectstream/utils";
6
+
7
+ export function* startSync(
8
+ state: AllSyncProtocols,
9
+ ): Operation<void> {
10
+ const iState: ISyncProtocol = state as ISyncProtocol;
11
+
12
+ // spawn async task
13
+ yield* spawn(function* () {
14
+ yield* iState.startAsync();
15
+ });
16
+
17
+ // spawn polling task
18
+ yield* spawn(function* () {
19
+ while (true) {
20
+ const inputResult = yield* tryYield(iState.stateToInput());
21
+ if (inputResult.error != null) {
22
+ state.consecutiveErrors++;
23
+ state.lastErrorTimestamp = Date.now();
24
+ log.remote(
25
+ ComponentNames.EFFECTSTREAM_SYNC,
26
+ [...state.getNamespace(), "stateToInput"],
27
+ SeverityNumber.ERROR,
28
+ (log) => log(inputResult.error),
29
+ );
30
+ const { config } = state.fetcher;
31
+ if ("pollingInterval" in config.syncProtocol) {
32
+ yield* sleep(config.syncProtocol.pollingInterval);
33
+ }
34
+ continue;
35
+ }
36
+ const input = inputResult.data;
37
+ const { config } = state.fetcher;
38
+ if (input == null) {
39
+ if ("pollingInterval" in config.syncProtocol) {
40
+ yield* sleep(config.syncProtocol.pollingInterval);
41
+ }
42
+ continue;
43
+ }
44
+
45
+ const result = yield* tryYield(iState.fetcher.readData(input, iState));
46
+ if (result.error != null) {
47
+ state.consecutiveErrors++;
48
+ state.lastErrorTimestamp = Date.now();
49
+ log.remote(
50
+ ComponentNames.EFFECTSTREAM_SYNC,
51
+ [...state.getNamespace(), "readData"],
52
+ SeverityNumber.ERROR,
53
+ (l) => l(result.error),
54
+ );
55
+ if ("pollingInterval" in config.syncProtocol) {
56
+ yield* sleep(config.syncProtocol.pollingInterval);
57
+ }
58
+ continue;
59
+ }
60
+ state.consecutiveErrors = 0;
61
+ state.lastSuccessfulFetchMs = Date.now();
62
+ log.remote(
63
+ ComponentNames.EFFECTSTREAM_SYNC,
64
+ [...state.getNamespace(), "data"],
65
+ SeverityNumber.TRACE,
66
+ (log) => log(result.data),
67
+ );
68
+ yield* iState.updateState(input, result.data);
69
+ for (const datum of result.data.output) {
70
+ yield* iState.fetcher.producerChannel.send(datum.output);
71
+ }
72
+ }
73
+ });
74
+ }
@@ -0,0 +1,32 @@
1
+ import type { ChainBlock, ChainPage } from "./common/root.ts";
2
+ import type { EvmSyncState } from "./evm/state.ts";
3
+ import type { SyncState } from "./base/state.ts";
4
+ import type { UtxoRpcSyncState } from "./utxorpc/state.ts";
5
+ import type { PaginatedFetcher } from "./base/fetcher.ts";
6
+ import type { UnionToIntersection } from "@effectstream/utils";
7
+ import type { MidnightSyncState } from "./midnight/state.ts";
8
+ import type { NtpSyncState } from "./ntp/state.ts";
9
+ import type { AvailSyncState } from "./avail/state.ts";
10
+ import type { BitcoinSyncState } from "./bitcoin/state.ts";
11
+ import type { CelestiaSyncState } from "./celestia/state.ts";
12
+ import type { NearSyncState } from "./near/state.ts";
13
+
14
+ // TODO: move folders
15
+ export type RootOutput = ChainBlock;
16
+ export type RootPage = ChainPage;
17
+
18
+ // TODO: map config types to sync protocols
19
+ export type AllSyncProtocols =
20
+ | NtpSyncState
21
+ | EvmSyncState
22
+ | UtxoRpcSyncState
23
+ | MidnightSyncState
24
+ | AvailSyncState
25
+ | BitcoinSyncState
26
+ | CelestiaSyncState
27
+ | NearSyncState;
28
+ export type ISyncProtocol = UnionToIntersection<AllSyncProtocols>;
29
+
30
+ type toPaginated<T> = T extends { fetcher: PaginatedFetcher<infer Page> } ? T
31
+ : never;
32
+ export type PaginatedSyncProtocols = toPaginated<AllSyncProtocols>;
@@ -0,0 +1,169 @@
1
+ import type { CardanoSyncClient } from "@utxorpc/sdk";
2
+ import type { cardano } from "@utxorpc/spec";
3
+ import type { OutputAndCleanup } from "../base/state.ts";
4
+ import Deque from "denque";
5
+ import type { BlockNumber } from "@effectstream/utils";
6
+ import type { Operation } from "effection";
7
+ import { conditionVariable } from "@effectstream/utils";
8
+ import type { ChainPoint, Page } from "./types.ts";
9
+ import { Buffer } from "node:buffer";
10
+
11
+ export class BufferedRpc {
12
+ private readonly buffer: Deque<OutputAndCleanup<cardano.Block>> =
13
+ new Deque();
14
+ private readonly newDataCondVar = conditionVariable<void>();
15
+
16
+ constructor(
17
+ public readonly syncClient: CardanoSyncClient,
18
+ /**
19
+ * Finality should ensure that we never expose data that could be affected by `undo`
20
+ * recall: finality is in terms of blocks created (not in terms of slots)
21
+ */
22
+ private readonly finality: BlockNumber,
23
+ ) {}
24
+
25
+ static lastHeight: bigint | undefined = undefined;
26
+ static initialized = false;
27
+
28
+ public lastHeight(): bigint | undefined {
29
+ return BufferedRpc.lastHeight;
30
+ }
31
+
32
+ public async resolveOrigin(): Promise<ChainPoint> {
33
+ const blocks = await this.syncClient.fetchHistory(undefined, 1);
34
+ if (!blocks.length) throw new Error("No blocks found on chain");
35
+ const header = blocks[0].parsedBlock.header!;
36
+ return {
37
+ slot: Number(header.slot),
38
+ hash: Buffer.from(header.hash).toString("hex"),
39
+ };
40
+ }
41
+
42
+ public async start(point?: ChainPoint): Promise<void> {
43
+ if (BufferedRpc.initialized) {
44
+ // We do not want to follow the tip again.
45
+ throw new Error("BufferedRpc already initialized");
46
+ }
47
+ BufferedRpc.initialized = true;
48
+ const intersect = point ? [point] : [];
49
+ const blockEvents = this.syncClient.followTip(intersect);
50
+
51
+ let seenReset = false;
52
+
53
+ for await (const blockEvent of blockEvents) {
54
+ if (blockEvent.action === "apply") {
55
+ if (blockEvent.block.header?.height) {
56
+ BufferedRpc.lastHeight = blockEvent.block.header?.height;
57
+ }
58
+ // This works ONLY because the buffer is processed FIFO
59
+ this.buffer.push({
60
+ output: blockEvent.block,
61
+ cleanup: () => this.buffer.shift(),
62
+ });
63
+ this.newDataCondVar.wake();
64
+ } else if (blockEvent.action === "undo") {
65
+ if (blockEvent.block.header?.height) {
66
+ BufferedRpc.lastHeight = blockEvent.block.header?.height;
67
+ }
68
+ this.buffer.pop();
69
+ } else if (blockEvent.action === "reset") {
70
+ if (!seenReset) {
71
+ seenReset = true;
72
+ continue;
73
+ }
74
+
75
+ const lastBlock = this.buffer.peekBack();
76
+ const lastPoint = lastBlock ? JSON.stringify({
77
+ slot: Number(lastBlock.output.header!.slot),
78
+ hash: Buffer.from(lastBlock.output.header!.hash).toString("hex"),
79
+ }) : "<none>";
80
+ throw new Error(
81
+ `Paima node stuck on fork. Currently at point ${lastPoint}`,
82
+ );
83
+ }
84
+ }
85
+ }
86
+
87
+ public *storedBlocks(
88
+ height: undefined | BlockNumber,
89
+ ): Operation<{ from: Page; to: Page }> {
90
+ let lastElement = this.buffer.peekBack();
91
+
92
+ if (height == null) {
93
+ height = 0;
94
+ }
95
+
96
+ // if we have nothing in our buffer yet, or if we're asking for a future height
97
+ while (
98
+ this.buffer.length == 0 ||
99
+ lastElement == null ||
100
+ height >= Number(lastElement.output.header!.height) - this.finality
101
+ ) {
102
+ yield* this.newDataCondVar.wait();
103
+ lastElement = this.buffer.peekBack();
104
+ }
105
+
106
+ const endIndex = (() => {
107
+ let endIndex = this.buffer.length - 1;
108
+ const highestValid =
109
+ Number(this.buffer.peekBack()!.output.header!.height) -
110
+ this.finality;
111
+ while (
112
+ endIndex > 0 &&
113
+ Number(this.buffer.peekAt(endIndex)!.output.header!.height) >
114
+ highestValid
115
+ ) {
116
+ endIndex--;
117
+ }
118
+
119
+ return endIndex;
120
+ })();
121
+ const startIndex = (() => {
122
+ let startIndex = endIndex;
123
+ while (
124
+ startIndex > 0 &&
125
+ Number(
126
+ this.buffer.peekAt(startIndex - 1)!.output.header!.height,
127
+ ) >
128
+ height
129
+ ) {
130
+ startIndex--;
131
+ }
132
+ return startIndex;
133
+ })();
134
+ return {
135
+ from: toPage(this.buffer.peekAt(startIndex)!.output),
136
+ to: toPage(this.buffer.peekAt(endIndex)!.output),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Return an array of blocks
142
+ * This is preferred over `fetchBlock` where there may be gaps in blockHeight
143
+ * Or when you don't want a blocking call
144
+ */
145
+ public fetchBlocks(
146
+ from: BlockNumber, // (inclusive)
147
+ to: BlockNumber, // (inclusive)
148
+ ): OutputAndCleanup<cardano.Block>[] {
149
+ const blocks: OutputAndCleanup<cardano.Block>[] = [];
150
+ for (let i = 0; i < this.buffer.length; i++) {
151
+ const block = this.buffer.peekAt(i)!;
152
+ if (Number(block.output.header!.height) >= from) {
153
+ blocks.push(block);
154
+ }
155
+ if (Number(block.output.header!.height) >= to) {
156
+ break;
157
+ }
158
+ }
159
+ return blocks;
160
+ }
161
+ }
162
+
163
+ function toPage(block: cardano.Block): Page {
164
+ return {
165
+ slot: Number(block.header!.slot),
166
+ hash: Buffer.from(block.header!.hash).toString("hex"),
167
+ height: Number(block.header!.height),
168
+ };
169
+ }