@effectstream/sync 0.100.26 → 0.100.27
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.27",
|
|
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.27",
|
|
17
|
+
"@effectstream/db": "0.100.27",
|
|
18
|
+
"@effectstream/log": "0.100.27",
|
|
19
|
+
"@effectstream/utils": "0.100.27",
|
|
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",
|
|
@@ -11,11 +11,98 @@ import type Deque from "denque";
|
|
|
11
11
|
import type { ChainBlock } from "../common/root.ts";
|
|
12
12
|
import { chainPageRelation } from "../common/root.ts";
|
|
13
13
|
import type { CacheCleanup, OutputAndCleanup } from "../base/state.ts";
|
|
14
|
+
import { ENV } from "@effectstream/utils/node-env";
|
|
15
|
+
|
|
16
|
+
export const mergeCoalescingBoundaries = {
|
|
17
|
+
minScheduledBlockHeight: null as number | null,
|
|
18
|
+
minScheduledTimestampMs: null as number | null,
|
|
19
|
+
migrationBlockHeights: [] as number[],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isCoalescableEmpty(block: ChainBlock): boolean {
|
|
23
|
+
if (block.primitives.length > 0) return false;
|
|
24
|
+
|
|
25
|
+
const bh = block.blockNumber;
|
|
26
|
+
if (mergeCoalescingBoundaries.migrationBlockHeights.includes(bh)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
mergeCoalescingBoundaries.minScheduledBlockHeight != null &&
|
|
32
|
+
bh >= mergeCoalescingBoundaries.minScheduledBlockHeight
|
|
33
|
+
) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
mergeCoalescingBoundaries.minScheduledTimestampMs != null &&
|
|
39
|
+
block.timestamp >= mergeCoalescingBoundaries.minScheduledTimestampMs
|
|
40
|
+
) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeBlockInfo(
|
|
48
|
+
target: ChainBlock["blockInfo"],
|
|
49
|
+
source: ChainBlock["blockInfo"],
|
|
50
|
+
): ChainBlock["blockInfo"] {
|
|
51
|
+
const map = new Map<string, ChainBlock["blockInfo"][number]>();
|
|
52
|
+
for (const item of target) {
|
|
53
|
+
map.set(item.protocol_name, item);
|
|
54
|
+
}
|
|
55
|
+
for (const item of source) {
|
|
56
|
+
map.set(item.protocol_name, item);
|
|
57
|
+
}
|
|
58
|
+
return Array.from(map.values());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeResumePages(
|
|
62
|
+
target: ChainBlock["resumePages"],
|
|
63
|
+
source: ChainBlock["resumePages"],
|
|
64
|
+
): ChainBlock["resumePages"] {
|
|
65
|
+
const map = new Map<string, ChainBlock["resumePages"][number]>();
|
|
66
|
+
for (const item of target) {
|
|
67
|
+
map.set(item.protocol_name, item);
|
|
68
|
+
}
|
|
69
|
+
for (const item of source) {
|
|
70
|
+
map.set(item.protocol_name, item);
|
|
71
|
+
}
|
|
72
|
+
return Array.from(map.values());
|
|
73
|
+
}
|
|
14
74
|
|
|
15
75
|
export function* startMerge(
|
|
16
76
|
syncProtocols: AllSyncProtocols[],
|
|
17
77
|
finalizedBlockStream: Channel<ChainBlock, void>,
|
|
18
78
|
): Operation<ChainBlock> {
|
|
79
|
+
let pendingEmptyRun: {
|
|
80
|
+
endpoint: ChainBlock;
|
|
81
|
+
count: number;
|
|
82
|
+
} | null = null;
|
|
83
|
+
|
|
84
|
+
const flushPendingRun = function* (): Operation<void> {
|
|
85
|
+
if (pendingEmptyRun == null) return;
|
|
86
|
+
const { endpoint } = pendingEmptyRun;
|
|
87
|
+
endpoint.coalescedCount = pendingEmptyRun.count;
|
|
88
|
+
|
|
89
|
+
log.remote(
|
|
90
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
91
|
+
"block-merge",
|
|
92
|
+
SeverityNumber.INFO,
|
|
93
|
+
(log) =>
|
|
94
|
+
log(
|
|
95
|
+
`producing coalesced empty block run of length ${pendingEmptyRun!.count} ending at block ${endpoint.blockNumber}`,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
yield* finalizedBlockStream.send(endpoint);
|
|
100
|
+
pendingEmptyRun = null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const enabled = ENV.EFFECTSTREAM_COALESCE_EMPTY_BLOCKS;
|
|
104
|
+
const MAX_PRODUCER_COALESCE = 1000;
|
|
105
|
+
|
|
19
106
|
while (true) {
|
|
20
107
|
let newBlock: ChainBlock | undefined = undefined;
|
|
21
108
|
|
|
@@ -44,17 +131,66 @@ export function* startMerge(
|
|
|
44
131
|
}
|
|
45
132
|
}
|
|
46
133
|
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
134
|
|
|
54
|
-
|
|
135
|
+
if (enabled && isCoalescableEmpty(newBlock)) {
|
|
136
|
+
if (pendingEmptyRun == null) {
|
|
137
|
+
pendingEmptyRun = {
|
|
138
|
+
endpoint: newBlock,
|
|
139
|
+
count: 1,
|
|
140
|
+
};
|
|
141
|
+
} else {
|
|
142
|
+
pendingEmptyRun.endpoint.blockNumber = newBlock.blockNumber;
|
|
143
|
+
pendingEmptyRun.endpoint.timestamp = newBlock.timestamp;
|
|
144
|
+
pendingEmptyRun.endpoint.blockInfo = mergeBlockInfo(
|
|
145
|
+
pendingEmptyRun.endpoint.blockInfo,
|
|
146
|
+
newBlock.blockInfo,
|
|
147
|
+
);
|
|
148
|
+
pendingEmptyRun.endpoint.resumePages = mergeResumePages(
|
|
149
|
+
pendingEmptyRun.endpoint.resumePages,
|
|
150
|
+
newBlock.resumePages,
|
|
151
|
+
);
|
|
152
|
+
pendingEmptyRun.count++;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Run cleanups eagerly so the consumed datum leaves the Deque now, instead
|
|
156
|
+
// of deferring to flush. This (a) keeps the main chain's Deque from growing
|
|
157
|
+
// by one entry per coalesced block across a long run, and (b) makes the
|
|
158
|
+
// tip check below meaningful: with the consumed datum removed,
|
|
159
|
+
// bufferedData.size() === 0 means the buffer is drained and we're at the
|
|
160
|
+
// tip. Safe because, during an empty run, every cleanup is a no-op — the
|
|
161
|
+
// main (NTP) datum's cleanup is `() => {}` and parallel chains merge
|
|
162
|
+
// nothing (cleanups.length === 0). Resume durability is unaffected: the
|
|
163
|
+
// runtime is the sole writer of the persisted resume marker, which only
|
|
164
|
+
// lands when the coalesced block commits downstream (CLAUDE.md #5).
|
|
165
|
+
for (const fn of cleanup) {
|
|
166
|
+
fn();
|
|
167
|
+
}
|
|
55
168
|
|
|
56
|
-
|
|
57
|
-
|
|
169
|
+
// At the tip (steady state) the main chain produces one slot per
|
|
170
|
+
// blockTimeMS, so the Deque drains to empty after each one — flush
|
|
171
|
+
// immediately so trickle-in blocks aren't held back. While catching up the
|
|
172
|
+
// Deque stays non-empty and the run keeps accumulating until the cap.
|
|
173
|
+
const ntpState = syncProtocols[0];
|
|
174
|
+
const atTip = ntpState.bufferedData.size() === 0;
|
|
175
|
+
if (pendingEmptyRun.count >= MAX_PRODUCER_COALESCE || atTip) {
|
|
176
|
+
yield* flushPendingRun();
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if (pendingEmptyRun != null) {
|
|
180
|
+
yield* flushPendingRun();
|
|
181
|
+
}
|
|
182
|
+
log.remote(
|
|
183
|
+
ComponentNames.EFFECTSTREAM_SYNC,
|
|
184
|
+
"block-merge",
|
|
185
|
+
SeverityNumber.INFO,
|
|
186
|
+
(log) => log(`producing block ${newBlock?.blockNumber}`),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
yield* finalizedBlockStream.send(newBlock);
|
|
190
|
+
|
|
191
|
+
for (const fn of cleanup) {
|
|
192
|
+
fn();
|
|
193
|
+
}
|
|
58
194
|
}
|
|
59
195
|
}
|
|
60
196
|
}
|