envio 3.9.0 → 3.10.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.
- package/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Utils from "../Utils.res.mjs";
|
|
4
|
+
import * as Source from "./Source.res.mjs";
|
|
5
|
+
import * as Logging from "../Logging.res.mjs";
|
|
6
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
9
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
11
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
|
|
13
|
+
function make(source, recordRequestStats, getHeightRetryInterval) {
|
|
14
|
+
return {
|
|
15
|
+
source: source,
|
|
16
|
+
logger: Logging.createChild({
|
|
17
|
+
chainId: source.chainId,
|
|
18
|
+
source: source.name
|
|
19
|
+
}),
|
|
20
|
+
recordRequestStats: recordRequestStats,
|
|
21
|
+
getHeightRetryInterval: getHeightRetryInterval,
|
|
22
|
+
knownHeight: 0,
|
|
23
|
+
waiters: [],
|
|
24
|
+
stream: "NeverEnabled",
|
|
25
|
+
generation: 0,
|
|
26
|
+
closeStream: undefined,
|
|
27
|
+
polling: false,
|
|
28
|
+
sleeping: undefined,
|
|
29
|
+
pollRetry: 0,
|
|
30
|
+
connects: 0,
|
|
31
|
+
disconnects: {},
|
|
32
|
+
stopped: false,
|
|
33
|
+
unreadableWarned: false,
|
|
34
|
+
subscribeRejectedWarned: false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function knownHeight(feed) {
|
|
39
|
+
return feed.knownHeight;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sample(feed) {
|
|
43
|
+
let match = feed.stream;
|
|
44
|
+
if (typeof match !== "object" && match === "NeverEnabled") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
connectCount: feed.connects,
|
|
49
|
+
disconnectsByReason: Object.entries(feed.disconnects).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldPoll(feed) {
|
|
54
|
+
if (feed.waiters.length === 0) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
let match = feed.stream;
|
|
58
|
+
if (typeof match !== "object" || !match.proven) {
|
|
59
|
+
return true;
|
|
60
|
+
} else {
|
|
61
|
+
return feed.waiters.some(waiter => waiter.distrustsStream);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function wakeCadence(feed) {
|
|
66
|
+
let match = feed.sleeping;
|
|
67
|
+
if (match === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let match$1 = match.kind;
|
|
71
|
+
if (match$1 === "Cadence") {
|
|
72
|
+
return match.wake();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function clearBackoff(feed) {
|
|
77
|
+
feed.pollRetry = 0;
|
|
78
|
+
let sleeping = feed.sleeping;
|
|
79
|
+
if (sleeping !== undefined) {
|
|
80
|
+
sleeping.kind = "Cadence";
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fireWaiters(feed, height) {
|
|
86
|
+
let isSatisfied = waiter => height > waiter.knownHeight;
|
|
87
|
+
let satisfied = feed.waiters.filter(isSatisfied);
|
|
88
|
+
if (satisfied.length !== 0) {
|
|
89
|
+
feed.waiters = feed.waiters.filter(waiter => height <= waiter.knownHeight);
|
|
90
|
+
satisfied.forEach(waiter => {
|
|
91
|
+
try {
|
|
92
|
+
return waiter.onHeight(height);
|
|
93
|
+
} catch (raw_exn) {
|
|
94
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
95
|
+
return Logging.childError(feed.logger, {
|
|
96
|
+
msg: "A height waiter threw. Dropping it rather than losing the height for the others.",
|
|
97
|
+
err: Utils.prettifyExn(exn)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return wakeCadence(feed);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function trustStreamAgain(feed) {
|
|
106
|
+
feed.waiters.forEach(waiter => {
|
|
107
|
+
waiter.distrustsStream = false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recordHeight(feed, height) {
|
|
112
|
+
if (height > feed.knownHeight) {
|
|
113
|
+
feed.knownHeight = height;
|
|
114
|
+
return fireWaiters(feed, height);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function recordAnswer(feed, generation, res) {
|
|
119
|
+
feed.recordRequestStats(res.requestStats);
|
|
120
|
+
clearBackoff(feed);
|
|
121
|
+
let match = feed.stream;
|
|
122
|
+
if (typeof match !== "object") {
|
|
123
|
+
match === "NeverEnabled";
|
|
124
|
+
} else if (generation === feed.generation) {
|
|
125
|
+
feed.stream = {
|
|
126
|
+
TAG: "Connected",
|
|
127
|
+
proven: true
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
recordHeight(feed, res.height);
|
|
131
|
+
wakeCadence(feed);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function recordLateAnswer(feed, res) {
|
|
135
|
+
feed.recordRequestStats(res.requestStats);
|
|
136
|
+
recordHeight(feed, res.height);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function heightWithin(feed) {
|
|
140
|
+
let timeoutId = {
|
|
141
|
+
contents: undefined
|
|
142
|
+
};
|
|
143
|
+
let timedOut = {
|
|
144
|
+
contents: false
|
|
145
|
+
};
|
|
146
|
+
let request = feed.source.getHeightOrThrow();
|
|
147
|
+
Stdlib_Promise.$$catch(request.then(res => {
|
|
148
|
+
if (timedOut.contents) {
|
|
149
|
+
return recordLateAnswer(feed, res);
|
|
150
|
+
}
|
|
151
|
+
}), param => Promise.resolve());
|
|
152
|
+
return Promise.race([
|
|
153
|
+
Stdlib_Promise.$$catch(request.then(res => ({
|
|
154
|
+
TAG: "Answered",
|
|
155
|
+
_0: res
|
|
156
|
+
})), exn => Promise.resolve({
|
|
157
|
+
TAG: "Failed",
|
|
158
|
+
_0: exn
|
|
159
|
+
})),
|
|
160
|
+
new Promise((resolve, _reject) => {
|
|
161
|
+
timeoutId.contents = Primitive_option.some(setTimeout(() => {
|
|
162
|
+
timedOut.contents = true;
|
|
163
|
+
resolve("TimedOut");
|
|
164
|
+
}, 60000));
|
|
165
|
+
})
|
|
166
|
+
]).then(outcome => {
|
|
167
|
+
Utils.clearTimeoutRef(timeoutId);
|
|
168
|
+
return outcome;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sleep(feed, kind, millis) {
|
|
173
|
+
return new Promise((resolve, _reject) => {
|
|
174
|
+
let timeoutId = setTimeout(() => {
|
|
175
|
+
feed.sleeping = undefined;
|
|
176
|
+
resolve();
|
|
177
|
+
}, millis);
|
|
178
|
+
feed.sleeping = {
|
|
179
|
+
kind: kind,
|
|
180
|
+
wake: () => {
|
|
181
|
+
clearTimeout(timeoutId);
|
|
182
|
+
feed.sleeping = undefined;
|
|
183
|
+
resolve();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function currentInterval(feed) {
|
|
190
|
+
return Stdlib_Option.getOr(Stdlib_Array.reduce(feed.waiters, undefined, (shortest, waiter) => Utils.$$Math.minOptInt(shortest, waiter.interval())), feed.source.pollingInterval);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function nextRetryInterval(feed) {
|
|
194
|
+
let retryInterval = feed.getHeightRetryInterval(feed.pollRetry);
|
|
195
|
+
feed.pollRetry = feed.pollRetry + 1 | 0;
|
|
196
|
+
return retryInterval;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function backOff(feed, what, exn) {
|
|
200
|
+
let retryInterval = nextRetryInterval(feed);
|
|
201
|
+
Logging.childTrace(feed.logger, {
|
|
202
|
+
msg: `Height retrieval from ` + feed.source.name + ` source ` + what + `. Retrying in ` + retryInterval.toString() + `ms.`,
|
|
203
|
+
err: Stdlib_Option.map(exn, Utils.prettifyExn)
|
|
204
|
+
});
|
|
205
|
+
return [
|
|
206
|
+
retryInterval,
|
|
207
|
+
"Backoff"
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function pollOnce(feed) {
|
|
212
|
+
let generation = feed.generation;
|
|
213
|
+
let res = await heightWithin(feed);
|
|
214
|
+
if (typeof res !== "object") {
|
|
215
|
+
return backOff(feed, `did not answer within ` + (60).toString() + `s`, undefined);
|
|
216
|
+
}
|
|
217
|
+
if (res.TAG !== "Answered") {
|
|
218
|
+
return backOff(feed, "failed", res._0);
|
|
219
|
+
}
|
|
220
|
+
recordAnswer(feed, generation, res._0);
|
|
221
|
+
return [
|
|
222
|
+
currentInterval(feed),
|
|
223
|
+
"Cadence"
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function runPollLoop(feed) {
|
|
228
|
+
while (shouldPoll(feed)) {
|
|
229
|
+
let match;
|
|
230
|
+
try {
|
|
231
|
+
match = await pollOnce(feed);
|
|
232
|
+
} catch (raw_exn) {
|
|
233
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
234
|
+
Logging.childError(feed.logger, {
|
|
235
|
+
msg: "The height poll cycle threw. Backing off and carrying on rather than leaving the source uncovered.",
|
|
236
|
+
err: Utils.prettifyExn(exn)
|
|
237
|
+
});
|
|
238
|
+
match = [
|
|
239
|
+
nextRetryInterval(feed),
|
|
240
|
+
"Backoff"
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
if (shouldPoll(feed)) {
|
|
244
|
+
await sleep(feed, match[1], match[0]);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
feed.polling = false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function syncPolling(feed) {
|
|
251
|
+
if (shouldPoll(feed) && !feed.polling) {
|
|
252
|
+
feed.polling = true;
|
|
253
|
+
runPollLoop(feed);
|
|
254
|
+
return;
|
|
255
|
+
} else {
|
|
256
|
+
return wakeCadence(feed);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function catchUpWhileIdle(feed) {
|
|
261
|
+
let generation = feed.generation;
|
|
262
|
+
let res = await heightWithin(feed);
|
|
263
|
+
if (typeof res !== "object" || res.TAG !== "Answered") {
|
|
264
|
+
return;
|
|
265
|
+
} else {
|
|
266
|
+
return recordAnswer(feed, generation, res._0);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function handlePushedHeight(feed, height) {
|
|
271
|
+
let advances = height > feed.knownHeight;
|
|
272
|
+
feed.recordRequestStats([{
|
|
273
|
+
method: advances ? "heightPush" : "heightPushIgnored",
|
|
274
|
+
seconds: 0
|
|
275
|
+
}]);
|
|
276
|
+
clearBackoff(feed);
|
|
277
|
+
let match = feed.stream;
|
|
278
|
+
if (typeof match !== "object") {
|
|
279
|
+
match === "NeverEnabled";
|
|
280
|
+
} else if (advances) {
|
|
281
|
+
feed.stream = {
|
|
282
|
+
TAG: "Connected",
|
|
283
|
+
proven: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
trustStreamAgain(feed);
|
|
287
|
+
recordHeight(feed, height);
|
|
288
|
+
wakeCadence(feed);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function markStreamDown(feed, reason) {
|
|
292
|
+
let match = feed.stream;
|
|
293
|
+
let hadConnection;
|
|
294
|
+
hadConnection = typeof match === "object";
|
|
295
|
+
feed.stream = "Disconnected";
|
|
296
|
+
if (hadConnection) {
|
|
297
|
+
Utils.Dict.incrementBy(feed.disconnects, Source.downReasonLabel(reason), 1);
|
|
298
|
+
trustStreamAgain(feed);
|
|
299
|
+
return syncPolling(feed);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function handleStatus(feed, status) {
|
|
304
|
+
if (typeof status !== "object") {
|
|
305
|
+
let match = feed.stream;
|
|
306
|
+
if (typeof match === "object") {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
match === "NeverEnabled";
|
|
310
|
+
feed.stream = {
|
|
311
|
+
TAG: "Connected",
|
|
312
|
+
proven: false
|
|
313
|
+
};
|
|
314
|
+
feed.generation = feed.generation + 1 | 0;
|
|
315
|
+
feed.connects = feed.connects + 1 | 0;
|
|
316
|
+
syncPolling(feed);
|
|
317
|
+
if (!feed.polling) {
|
|
318
|
+
catchUpWhileIdle(feed);
|
|
319
|
+
return;
|
|
320
|
+
} else {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let reason = status.reason;
|
|
325
|
+
markStreamDown(feed, reason);
|
|
326
|
+
let log;
|
|
327
|
+
if (typeof reason !== "object") {
|
|
328
|
+
switch (reason) {
|
|
329
|
+
case "Unreadable" :
|
|
330
|
+
if (feed.unreadableWarned) {
|
|
331
|
+
log = Logging.childTrace;
|
|
332
|
+
} else {
|
|
333
|
+
feed.unreadableWarned = true;
|
|
334
|
+
log = Logging.childWarn;
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
case "SubscribeRejected" :
|
|
338
|
+
if (!feed.subscribeRejectedWarned && feed.connects === 0) {
|
|
339
|
+
feed.subscribeRejectedWarned = true;
|
|
340
|
+
log = Logging.childWarn;
|
|
341
|
+
} else {
|
|
342
|
+
log = Logging.childTrace;
|
|
343
|
+
}
|
|
344
|
+
break;
|
|
345
|
+
default:
|
|
346
|
+
log = Logging.childTrace;
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
log = Logging.childTrace;
|
|
350
|
+
}
|
|
351
|
+
let label = Source.downReasonLabel(reason);
|
|
352
|
+
log(feed.logger, {
|
|
353
|
+
msg: `Height subscription for ` + feed.source.name + ` source went down (` + label + `). Polling for the height until it reconnects.`,
|
|
354
|
+
reason: label,
|
|
355
|
+
detail: status.detail
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function enableStream(feed) {
|
|
360
|
+
let match = feed.source.createHeightSubscription;
|
|
361
|
+
let match$1 = feed.closeStream;
|
|
362
|
+
if (match === undefined) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (match$1 !== undefined) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (feed.stopped) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
let match$2 = feed.stream;
|
|
372
|
+
if (typeof match$2 !== "object" && match$2 === "NeverEnabled") {
|
|
373
|
+
feed.stream = "Disconnected";
|
|
374
|
+
}
|
|
375
|
+
feed.closeStream = match(height => handlePushedHeight(feed, height), status => handleStatus(feed, status));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function stop(feed) {
|
|
379
|
+
feed.stopped = true;
|
|
380
|
+
let closeStream = feed.closeStream;
|
|
381
|
+
if (closeStream !== undefined) {
|
|
382
|
+
closeStream();
|
|
383
|
+
feed.closeStream = undefined;
|
|
384
|
+
return markStreamDown(feed, "Unsubscribed");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function onHeightAbove(feed, knownHeight, interval, onHeight) {
|
|
389
|
+
if (feed.knownHeight > knownHeight) {
|
|
390
|
+
onHeight(feed.knownHeight);
|
|
391
|
+
return {
|
|
392
|
+
unsubscribe: () => {},
|
|
393
|
+
distrustStream: () => {}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
let waiter = {
|
|
397
|
+
knownHeight: knownHeight,
|
|
398
|
+
onHeight: onHeight,
|
|
399
|
+
interval: interval,
|
|
400
|
+
distrustsStream: false
|
|
401
|
+
};
|
|
402
|
+
feed.waiters.push(waiter);
|
|
403
|
+
syncPolling(feed);
|
|
404
|
+
return {
|
|
405
|
+
unsubscribe: () => {
|
|
406
|
+
feed.waiters = feed.waiters.filter(w => w !== waiter);
|
|
407
|
+
wakeCadence(feed);
|
|
408
|
+
},
|
|
409
|
+
distrustStream: () => {
|
|
410
|
+
let match = feed.stream;
|
|
411
|
+
if (typeof match !== "object" || waiter.distrustsStream) {
|
|
412
|
+
return;
|
|
413
|
+
} else {
|
|
414
|
+
waiter.distrustsStream = true;
|
|
415
|
+
return syncPolling(feed);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let pollTimeoutMillis = 60000;
|
|
422
|
+
|
|
423
|
+
export {
|
|
424
|
+
make,
|
|
425
|
+
knownHeight,
|
|
426
|
+
sample,
|
|
427
|
+
shouldPoll,
|
|
428
|
+
wakeCadence,
|
|
429
|
+
clearBackoff,
|
|
430
|
+
fireWaiters,
|
|
431
|
+
trustStreamAgain,
|
|
432
|
+
recordHeight,
|
|
433
|
+
recordAnswer,
|
|
434
|
+
recordLateAnswer,
|
|
435
|
+
pollTimeoutMillis,
|
|
436
|
+
heightWithin,
|
|
437
|
+
sleep,
|
|
438
|
+
currentInterval,
|
|
439
|
+
nextRetryInterval,
|
|
440
|
+
backOff,
|
|
441
|
+
pollOnce,
|
|
442
|
+
runPollLoop,
|
|
443
|
+
syncPolling,
|
|
444
|
+
catchUpWhileIdle,
|
|
445
|
+
handlePushedHeight,
|
|
446
|
+
markStreamDown,
|
|
447
|
+
handleStatus,
|
|
448
|
+
enableStream,
|
|
449
|
+
stop,
|
|
450
|
+
onHeightAbove,
|
|
451
|
+
}
|
|
452
|
+
/* Utils Not a pure module */
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Reconnect driver shared by every height subscription transport. A transport
|
|
3
|
+
supplies `connect`, which wires its socket to the driver callbacks and returns a
|
|
4
|
+
close function; the driver owns retries, staleness detection and status
|
|
5
|
+
reporting so SSE and WebSocket streams behave identically.
|
|
6
|
+
|
|
7
|
+
Nothing here logs. A height stream failing is not something an operator has to
|
|
8
|
+
act on — the indexer polls for the height instead — so a failure travels out
|
|
9
|
+
through `onStatus`, where its reason becomes a metric label and its detail the
|
|
10
|
+
one log line that says what the provider actually sent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type driver = {
|
|
14
|
+
// The stream is usable: for SSE the connection opened, for WebSocket the
|
|
15
|
+
// subscription was confirmed.
|
|
16
|
+
onConnected: unit => unit,
|
|
17
|
+
onKeepAlive: unit => unit,
|
|
18
|
+
onHeight: int => unit,
|
|
19
|
+
// A message the transport couldn't read. Not a failure by itself, but from
|
|
20
|
+
// then on only a height we can read counts as the connection still being
|
|
21
|
+
// useful, so a stream of them goes quiet and fails as "unreadable", carrying
|
|
22
|
+
// the frame that started it.
|
|
23
|
+
onUnreadable: (~detail: string) => unit,
|
|
24
|
+
onFailure: (~reason: Source.heightSubscriptionDownReason, ~detail: string=?) => unit,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// While a stream is down its consumer polls instead, so the retry delay is what
|
|
28
|
+
// that fallback costs: a load balancer rotating a connection heals on the first
|
|
29
|
+
// retry, and every polling interval spent waiting for it is an avoidable
|
|
30
|
+
// request. Small first step, with the doubling to protect an endpoint that is
|
|
31
|
+
// genuinely in trouble.
|
|
32
|
+
let baseRetryMillis = 250
|
|
33
|
+
let maxRetryMillis = 60_000
|
|
34
|
+
// Floor for how long a connection has to serve to have been worth making, as a
|
|
35
|
+
// fraction of the window we would wait before calling it dead. Both transports
|
|
36
|
+
// deliver something the moment they connect — HyperSync sends the head, a
|
|
37
|
+
// WebSocket confirms the subscription — so an endpoint that accepts and drops
|
|
38
|
+
// produces everything a working one would have by then, and only staying up
|
|
39
|
+
// tells them apart. A fixed floor can't do it: one low enough to clear on a
|
|
40
|
+
// healthy connection is one a connection that dies seconds in clears too.
|
|
41
|
+
let provenStaleFraction = 4
|
|
42
|
+
// A frame nobody could read ends up in a log line, so cap what a provider can
|
|
43
|
+
// put there.
|
|
44
|
+
let maxDetailLength = 200
|
|
45
|
+
|
|
46
|
+
// The wait that follows `count` consecutive failures. Zero for a first
|
|
47
|
+
// connection, which nothing preceded. Deterministic: the bar a connection has
|
|
48
|
+
// to clear to count as proven is derived from it, and a jittered bar would make
|
|
49
|
+
// the rotated/closed split depend on a coin flip.
|
|
50
|
+
let retryDelayFor = count =>
|
|
51
|
+
count <= 0
|
|
52
|
+
? 0
|
|
53
|
+
: Utils.expBackoff(~base=baseRetryMillis, ~exp=count - 1, ~maxMillis=maxRetryMillis)
|
|
54
|
+
|
|
55
|
+
let truncateDetail = detail =>
|
|
56
|
+
detail->String.length > maxDetailLength
|
|
57
|
+
? detail->String.slice(~start=0, ~end=maxDetailLength) ++ "..."
|
|
58
|
+
: detail
|
|
59
|
+
|
|
60
|
+
let subscribe = (
|
|
61
|
+
~staleTimeout: int,
|
|
62
|
+
~onHeight: int => unit,
|
|
63
|
+
~onStatus: Source.heightSubscriptionStatus => unit,
|
|
64
|
+
// Must not allocate anything it can then throw past: a throw leaves the driver
|
|
65
|
+
// without the close function, so only the transport can release what it took.
|
|
66
|
+
~connect: driver => unit => unit,
|
|
67
|
+
): (unit => unit) => {
|
|
68
|
+
let closeConnectionRef = ref(None)
|
|
69
|
+
let failureCount = ref(0)
|
|
70
|
+
// Bumped on every failure, reconnect and unsubscribe. Callbacks from a
|
|
71
|
+
// superseded connection compare against it and no-op, so a socket that
|
|
72
|
+
// reports an error and then a close only counts as one failure, and nothing a
|
|
73
|
+
// connection reports after unsubscribing is heard.
|
|
74
|
+
let generation = ref(0)
|
|
75
|
+
// A connection is either waiting for traffic or waiting to be retried, never
|
|
76
|
+
// both, so a single slot holds whichever timer is pending.
|
|
77
|
+
let timeoutId = ref(None)
|
|
78
|
+
// The frame that stopped this connection being readable, kept for the failure
|
|
79
|
+
// it will eventually be named after.
|
|
80
|
+
let unreadableDetail = ref(None)
|
|
81
|
+
// When the current connection reported itself live, if it ever did. Time
|
|
82
|
+
// before that is not service: a handshake that takes seconds to fail, or a
|
|
83
|
+
// socket that is accepted and then hangs until the staleness timer, would
|
|
84
|
+
// otherwise be indistinguishable from a connection that worked.
|
|
85
|
+
let liveAt = ref(None)
|
|
86
|
+
let minProvenMillis = staleTimeout / provenStaleFraction
|
|
87
|
+
|
|
88
|
+
let clearPendingTimeout = () => timeoutId->Utils.clearTimeoutRef
|
|
89
|
+
|
|
90
|
+
// A transport that throws while closing must not be able to stop the retry
|
|
91
|
+
// that follows, nor escape a timer callback and take the process down. Every
|
|
92
|
+
// close goes through here, including the one for a connection superseded
|
|
93
|
+
// while connect was still running.
|
|
94
|
+
let closeSafely = close =>
|
|
95
|
+
try close() catch {
|
|
96
|
+
| _ => ()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let closeConnection = () => {
|
|
100
|
+
switch closeConnectionRef.contents {
|
|
101
|
+
| Some(close) => closeSafely(close)
|
|
102
|
+
| None => ()
|
|
103
|
+
}
|
|
104
|
+
closeConnectionRef := None
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let rec armStaleTimeout = () => {
|
|
108
|
+
clearPendingTimeout()
|
|
109
|
+
timeoutId := Some(setTimeout(() => {
|
|
110
|
+
timeoutId := None
|
|
111
|
+
// Distinguishes a provider whose message shape we don't understand
|
|
112
|
+
// from a chain that simply has nothing to report.
|
|
113
|
+
switch unreadableDetail.contents {
|
|
114
|
+
| Some(detail) => fail(~reason=Source.Unreadable, ~detail)
|
|
115
|
+
| None => fail(~reason=Source.Stale)
|
|
116
|
+
}
|
|
117
|
+
}, staleTimeout))
|
|
118
|
+
}
|
|
119
|
+
and fail = (~reason: Source.heightSubscriptionDownReason, ~detail=?) => {
|
|
120
|
+
generation := generation.contents + 1
|
|
121
|
+
clearPendingTimeout()
|
|
122
|
+
closeConnection()
|
|
123
|
+
|
|
124
|
+
// A connection that served for longer than the wait it cost was worth
|
|
125
|
+
// making, whatever ended it, so the backoff starts over. That covers a load
|
|
126
|
+
// balancer rotating connections and a chain whose blocks are further apart
|
|
127
|
+
// than the stale timeout alike, without either having to be recognised. The
|
|
128
|
+
// bar rises with the backoff, so an endpoint that always dies at the same
|
|
129
|
+
// age keeps escalating rather than settling into a loop at a fixed delay.
|
|
130
|
+
let servedMillis = switch liveAt.contents {
|
|
131
|
+
| Some(since) => (Performance.now() -. since)->Float.toInt
|
|
132
|
+
| None => 0
|
|
133
|
+
}
|
|
134
|
+
// Against the schedule, not against the jittered wait actually served. The
|
|
135
|
+
// spread exists to keep indexers from retrying in lockstep, and letting it
|
|
136
|
+
// reach this would put a coin flip in the rotated/closed split an operator
|
|
137
|
+
// reads. Judging a connection by the step of the ramp it was born on is
|
|
138
|
+
// conservative in the same direction every time.
|
|
139
|
+
let provenMillis = Pervasives.max(retryDelayFor(failureCount.contents), minProvenMillis)
|
|
140
|
+
failureCount := if servedMillis >= provenMillis {
|
|
141
|
+
1
|
|
142
|
+
} else {
|
|
143
|
+
failureCount.contents + 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let retryMillis = retryDelayFor(failureCount.contents)->Utils.jitter
|
|
147
|
+
|
|
148
|
+
// A clean end to a connection that had proven itself is a rotation: routine,
|
|
149
|
+
// and worth telling apart from the same clean end arriving early, which is a
|
|
150
|
+
// server dropping connections it should be serving. Neither transport can
|
|
151
|
+
// tell which it was, so the driver decides from how long the connection
|
|
152
|
+
// lasted — the same measure the backoff already trusts.
|
|
153
|
+
let reason: Source.heightSubscriptionDownReason = switch reason {
|
|
154
|
+
| Closed if servedMillis >= provenMillis => Rotated
|
|
155
|
+
| reason => reason
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Scheduled before reporting, so a consumer that throws can't be what makes
|
|
159
|
+
// the stream give up.
|
|
160
|
+
timeoutId := Some(setTimeout(() => {
|
|
161
|
+
timeoutId := None
|
|
162
|
+
start()
|
|
163
|
+
}, retryMillis))
|
|
164
|
+
|
|
165
|
+
onStatus(Down({reason, detail: ?(detail->Option.map(truncateDetail))}))
|
|
166
|
+
}
|
|
167
|
+
and start = () => {
|
|
168
|
+
generation := generation.contents + 1
|
|
169
|
+
unreadableDetail := None
|
|
170
|
+
liveAt := None
|
|
171
|
+
let connectionGeneration = generation.contents
|
|
172
|
+
let isCurrent = () => generation.contents === connectionGeneration
|
|
173
|
+
|
|
174
|
+
// Armed before connecting, so it covers a connect that never reports
|
|
175
|
+
// anything, and so a synchronous failure inside connect replaces it with
|
|
176
|
+
// the retry timeout rather than the other way around.
|
|
177
|
+
armStaleTimeout()
|
|
178
|
+
|
|
179
|
+
// A height is proof the stream is delivering, so it reports Live as well as
|
|
180
|
+
// onConnected does. Without that, a transport that never reports the
|
|
181
|
+
// connection usable would leave its consumer polling at full rate next to a
|
|
182
|
+
// stream that works.
|
|
183
|
+
let goLive = () => {
|
|
184
|
+
armStaleTimeout()
|
|
185
|
+
if liveAt.contents->Option.isNone {
|
|
186
|
+
liveAt := Some(Performance.now())
|
|
187
|
+
onStatus(Live)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let driver = {
|
|
192
|
+
onConnected: () =>
|
|
193
|
+
if isCurrent() {
|
|
194
|
+
goLive()
|
|
195
|
+
},
|
|
196
|
+
onKeepAlive: () =>
|
|
197
|
+
// Stops counting once anything unreadable has arrived, on the first one
|
|
198
|
+
// rather than on some run of them: a stream whose heights are all
|
|
199
|
+
// malformed is indistinguishable from one that has sent a single
|
|
200
|
+
// garbled frame until a readable height arrives, and the keep-alives
|
|
201
|
+
// between malformed heights would otherwise hold the connection open
|
|
202
|
+
// indefinitely, never failing and so never reporting anything, while
|
|
203
|
+
// its consumer sat on the staleness backstop. Being wrong about a lone
|
|
204
|
+
// glitch costs one reconnect.
|
|
205
|
+
if isCurrent() && unreadableDetail.contents->Option.isNone {
|
|
206
|
+
armStaleTimeout()
|
|
207
|
+
},
|
|
208
|
+
onHeight: height =>
|
|
209
|
+
if isCurrent() {
|
|
210
|
+
// A height we could read proves the shape is fine again. A keep-alive
|
|
211
|
+
// deliberately doesn't: on a stream whose heights are all malformed,
|
|
212
|
+
// the pings between them would clear this and the staleness that
|
|
213
|
+
// followed would look like a chain with nothing to report.
|
|
214
|
+
unreadableDetail := None
|
|
215
|
+
goLive()
|
|
216
|
+
onHeight(height)
|
|
217
|
+
},
|
|
218
|
+
onUnreadable: (~detail) =>
|
|
219
|
+
if isCurrent() {
|
|
220
|
+
unreadableDetail := Some(detail)
|
|
221
|
+
},
|
|
222
|
+
onFailure: (~reason, ~detail=?) =>
|
|
223
|
+
if isCurrent() {
|
|
224
|
+
fail(~reason, ~detail?)
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A transport constructor can throw on a malformed url. Left to escape it
|
|
229
|
+
// would reach a timer callback on the next retry and take the process down.
|
|
230
|
+
// What it says is the only account of why the url can't be connected to, and
|
|
231
|
+
// this retries forever, so it goes out as the failure's detail.
|
|
232
|
+
let connected = try Ok(connect(driver)) catch {
|
|
233
|
+
| exn => Error(exn)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch connected {
|
|
237
|
+
| Ok(closeCurrentConnection) =>
|
|
238
|
+
if isCurrent() {
|
|
239
|
+
closeConnectionRef := Some(closeCurrentConnection)
|
|
240
|
+
} else {
|
|
241
|
+
closeSafely(closeCurrentConnection)
|
|
242
|
+
}
|
|
243
|
+
| Error(exn) =>
|
|
244
|
+
if isCurrent() {
|
|
245
|
+
fail(~reason=ConnectFailed, ~detail=?exn->Utils.exnMessage)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
start()
|
|
251
|
+
|
|
252
|
+
() => {
|
|
253
|
+
generation := generation.contents + 1
|
|
254
|
+
clearPendingTimeout()
|
|
255
|
+
closeConnection()
|
|
256
|
+
}
|
|
257
|
+
}
|