envio 3.6.0 → 3.6.1-subgraph
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 +6 -6
- package/src/ChainState.res +10 -4
- package/src/ChainState.res.mjs +5 -1
- package/src/Config.res +12 -0
- package/src/Config.res.mjs +8 -2
- package/src/Core.res +15 -0
- package/src/Core.res.mjs +11 -0
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +8 -6
- package/src/HandlerLoader.res +20 -8
- package/src/HandlerLoader.res.mjs +11 -2
- package/src/Metrics.res +5 -5
- package/src/Metrics.res.mjs +5 -1
- package/src/UserContext.res +358 -51
- package/src/UserContext.res.mjs +271 -35
- package/src/sources/SourceManager.res +6 -12
- package/src/sources/SourceManager.res.mjs +8 -5
- package/src/subgraph/blocks.ts +176 -0
- package/src/subgraph/calls.ts +213 -0
- package/src/subgraph/conformance.ts +99 -0
- package/src/subgraph/division.ts +100 -0
- package/src/subgraph/errors.ts +90 -0
- package/src/subgraph/graph-ts-types/VERSION +2 -0
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
- package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
- package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
- package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
- package/src/subgraph/graph-ts-types/index.d.ts +102 -0
- package/src/subgraph/graph-ts.ts +1695 -0
- package/src/subgraph/hosts.ts +148 -0
- package/src/subgraph/runtime.ts +827 -0
- package/src/subgraph/scope.ts +63 -0
package/src/UserContext.res.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Utils from "./Utils.res.mjs";
|
|
4
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
4
5
|
import * as Logging from "./Logging.res.mjs";
|
|
5
6
|
import * as Internal from "./Internal.res.mjs";
|
|
6
7
|
import * as Ecosystem from "./Ecosystem.res.mjs";
|
|
@@ -12,8 +13,70 @@ import * as InMemoryStore from "./InMemoryStore.res.mjs";
|
|
|
12
13
|
import * as InMemoryTable from "./InMemoryTable.res.mjs";
|
|
13
14
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
14
15
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
16
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
15
17
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
16
18
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
19
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
20
|
+
|
|
21
|
+
let Suspend = /* @__PURE__ */Primitive_exceptions.create("UserContext.Suspend");
|
|
22
|
+
|
|
23
|
+
function isSuspend(exn) {
|
|
24
|
+
return exn.RE_EXN_ID === Suspend;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeSyncState() {
|
|
28
|
+
return {
|
|
29
|
+
status: "Active",
|
|
30
|
+
pending: undefined,
|
|
31
|
+
memo: undefined
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPending(sync) {
|
|
36
|
+
let pending = sync.pending;
|
|
37
|
+
if (pending !== undefined) {
|
|
38
|
+
return pending;
|
|
39
|
+
}
|
|
40
|
+
let pending$1 = [];
|
|
41
|
+
sync.pending = pending$1;
|
|
42
|
+
return pending$1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getMemo(sync) {
|
|
46
|
+
let memo = sync.memo;
|
|
47
|
+
if (memo !== undefined) {
|
|
48
|
+
return memo;
|
|
49
|
+
}
|
|
50
|
+
let memo$1 = {};
|
|
51
|
+
sync.memo = memo$1;
|
|
52
|
+
return memo$1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkStatusOrThrow(params, access) {
|
|
56
|
+
let exn = params.sync.status;
|
|
57
|
+
if (typeof exn !== "object") {
|
|
58
|
+
if (exn === "Active") {
|
|
59
|
+
return;
|
|
60
|
+
} else {
|
|
61
|
+
return ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access ` + access + ` after the handler is resolved. Make sure you didn't miss an await in the handler.`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw exn._0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scheduleAndSuspend(params, promise) {
|
|
68
|
+
getPending(params.sync).push(promise);
|
|
69
|
+
params.sync.status = {
|
|
70
|
+
TAG: "Aborted",
|
|
71
|
+
_0: {
|
|
72
|
+
RE_EXN_ID: Suspend
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
throw {
|
|
76
|
+
RE_EXN_ID: Suspend,
|
|
77
|
+
Error: new Error()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
17
80
|
|
|
18
81
|
let paramsByThis = new WeakMap();
|
|
19
82
|
|
|
@@ -47,28 +110,69 @@ var EffectContext = function(params, chainId, effectName, defaultShouldCache, ca
|
|
|
47
110
|
EffectContext.prototype = effectContextPrototype;
|
|
48
111
|
;
|
|
49
112
|
|
|
50
|
-
function
|
|
51
|
-
let handlerChainId = Internal.getItemChainId(params.item);
|
|
113
|
+
function prepareEffectCall(params, effect, input, caller) {
|
|
52
114
|
let isCrossChain = effect => Stdlib_Option.getOr(effect.crossChain, params.config.defaultCrossChain);
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
115
|
+
let scope = isCrossChain(effect) ? "crossChain" : Internal.getItemChainId(params.item);
|
|
116
|
+
if (caller !== undefined && isCrossChain(caller) && !isCrossChain(effect)) {
|
|
117
|
+
Stdlib_JsError.throwWithMessage(`The cross-chain effect "` + caller.name + `" cannot call the chain-scoped effect "` + effect.name + `", because a cross-chain effect isn't tied to a single chain. Make "` + effect.name + `" cross-chain (\`crossChain: true\`), or make "` + caller.name + `" chain-scoped (\`crossChain: false\`).`);
|
|
118
|
+
}
|
|
119
|
+
let tmp;
|
|
120
|
+
tmp = scope === "crossChain" ? undefined : Primitive_option.some(scope);
|
|
121
|
+
let effectContext = new EffectContext(params, tmp, effect.name, effect.defaultShouldCache, (nested, nestedInput) => callEffectAsync(params, nested, nestedInput, effect));
|
|
122
|
+
let effectArgs_cacheKey = Utils.Hash.makeOrThrow(S$RescriptSchema.reverseConvertOrThrow(input, effect.input));
|
|
123
|
+
let effectArgs_checkpointId = params.checkpointId;
|
|
124
|
+
let effectArgs = {
|
|
125
|
+
input: input,
|
|
126
|
+
context: effectContext,
|
|
127
|
+
cacheKey: effectArgs_cacheKey,
|
|
128
|
+
checkpointId: effectArgs_checkpointId
|
|
129
|
+
};
|
|
130
|
+
return [
|
|
131
|
+
scope,
|
|
132
|
+
effectArgs
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function callEffectAsync(params, effect, input, caller) {
|
|
137
|
+
let match = prepareEffectCall(params, effect, input, caller);
|
|
138
|
+
return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, match[1], match[0], params.indexerState, params.isPreload, params.item, params.config.ecosystem);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function initEffect(params) {
|
|
142
|
+
return (effect, input) => {
|
|
143
|
+
checkStatusOrThrow(params, "context.effect");
|
|
144
|
+
return callEffectAsync(params, effect, input, undefined);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function effectMemoKey(effect, scope, cacheKey) {
|
|
149
|
+
if (scope === "crossChain") {
|
|
150
|
+
return effect.name + `.` + cacheKey;
|
|
151
|
+
} else {
|
|
152
|
+
return effect.name + `.` + ChainId.toString(scope) + `.` + cacheKey;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function initEffectSync(params) {
|
|
157
|
+
return (effect, input) => {
|
|
158
|
+
checkStatusOrThrow(params, "context.effectSync");
|
|
159
|
+
let match = prepareEffectCall(params, effect, input, undefined);
|
|
160
|
+
let effectArgs = match[1];
|
|
161
|
+
let scope = match[0];
|
|
162
|
+
let memo = getMemo(params.sync);
|
|
163
|
+
let memoKey = effectMemoKey(effect, scope, effectArgs.cacheKey);
|
|
164
|
+
let output = memo[memoKey];
|
|
165
|
+
if (output !== undefined) {
|
|
166
|
+
return Primitive_option.valFromOption(output);
|
|
57
167
|
}
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cacheKey: effectArgs_cacheKey,
|
|
67
|
-
checkpointId: effectArgs_checkpointId
|
|
68
|
-
};
|
|
69
|
-
return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
|
|
70
|
-
});
|
|
71
|
-
return makeCaller(undefined);
|
|
168
|
+
let inMemTable = InMemoryStore.getEffectInMemTable(params.indexerState, effect, scope);
|
|
169
|
+
if (!InMemoryStore.hasEffectOutput(inMemTable, effectArgs.cacheKey)) {
|
|
170
|
+
return scheduleAndSuspend(params, LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem));
|
|
171
|
+
}
|
|
172
|
+
let output$1 = InMemoryStore.getEffectOutputUnsafe(inMemTable, effectArgs.cacheKey);
|
|
173
|
+
memo[memoKey] = output$1;
|
|
174
|
+
return output$1;
|
|
175
|
+
};
|
|
72
176
|
}
|
|
73
177
|
|
|
74
178
|
function entityScope(params) {
|
|
@@ -97,24 +201,83 @@ function throwClickHouseReadOnly(entityConfig, op) {
|
|
|
97
201
|
return Stdlib_JsError.throwWithMessage(`context.` + entityConfig.name + `.` + op + `() is unavailable: ClickHouse storage is currently write-only. Follow Envio releases to be notified when ClickHouse supports both reads and writes from handlers.`);
|
|
98
202
|
}
|
|
99
203
|
|
|
204
|
+
function getSyncHandler(params, entityId) {
|
|
205
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params));
|
|
206
|
+
if (entityId in inMemTable.latestEntityChangeById) {
|
|
207
|
+
return InMemoryTable.Entity.getUnsafe(inMemTable)(entityId);
|
|
208
|
+
} else {
|
|
209
|
+
return scheduleAndSuspend(params, LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getWhereSyncHandler(params, filter) {
|
|
214
|
+
let entityConfig = params.entityConfig;
|
|
215
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, entityConfig, entityScope(params));
|
|
216
|
+
let hasIndex = InMemoryTable.Entity.hasIndex(inMemTable);
|
|
217
|
+
let getOnIndex = InMemoryTable.Entity.getUnsafeOnIndex(inMemTable);
|
|
218
|
+
let filters = EntityFilter.parseGetWhereOrThrow(filter, entityConfig.name, entityConfig.table);
|
|
219
|
+
let missing = [];
|
|
220
|
+
let entities = [];
|
|
221
|
+
filters.forEach(filter => {
|
|
222
|
+
let filterKey = EntityFilter.toString(filter);
|
|
223
|
+
if (hasIndex(filterKey)) {
|
|
224
|
+
entities.push(...getOnIndex(filterKey));
|
|
225
|
+
} else {
|
|
226
|
+
missing.push(LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, filter));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
if (Utils.$$Array.notEmpty(missing)) {
|
|
230
|
+
let pending = getPending(params.sync);
|
|
231
|
+
missing.forEach(promise => {
|
|
232
|
+
pending.push(promise);
|
|
233
|
+
});
|
|
234
|
+
params.sync.status = {
|
|
235
|
+
TAG: "Aborted",
|
|
236
|
+
_0: {
|
|
237
|
+
RE_EXN_ID: Suspend
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
throw {
|
|
241
|
+
RE_EXN_ID: Suspend,
|
|
242
|
+
Error: new Error()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return entities;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getInBlockSyncHandler(params, entityId) {
|
|
249
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params));
|
|
250
|
+
let change = inMemTable.latestEntityChangeById[entityId];
|
|
251
|
+
if (change !== undefined && change.checkpointId === params.checkpointId) {
|
|
252
|
+
return InMemoryTable.Entity.mapChangeToEntity(change);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
100
256
|
let entityTraps_get = (params, prop) => {
|
|
257
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.` + prop);
|
|
101
258
|
let isClickHouseOnly = !params.entityConfig.storage.postgres;
|
|
102
|
-
let set = params.isPreload ? noopSet : entity =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
259
|
+
let set = params.isPreload ? noopSet : entity => {
|
|
260
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.set`);
|
|
261
|
+
InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
262
|
+
type: "SET",
|
|
263
|
+
entityId: entity.id,
|
|
264
|
+
entity: entity,
|
|
265
|
+
checkpointId: params.checkpointId
|
|
266
|
+
});
|
|
267
|
+
};
|
|
108
268
|
switch (prop) {
|
|
109
269
|
case "deleteUnsafe" :
|
|
110
270
|
if (params.isPreload) {
|
|
111
271
|
return noopDeleteUnsafe;
|
|
112
272
|
} else {
|
|
113
|
-
return entityId =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
273
|
+
return entityId => {
|
|
274
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.deleteUnsafe`);
|
|
275
|
+
InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
276
|
+
type: "DELETE",
|
|
277
|
+
entityId: entityId,
|
|
278
|
+
checkpointId: params.checkpointId
|
|
279
|
+
});
|
|
280
|
+
};
|
|
118
281
|
}
|
|
119
282
|
case "get" :
|
|
120
283
|
if (isClickHouseOnly) {
|
|
@@ -122,6 +285,8 @@ let entityTraps_get = (params, prop) => {
|
|
|
122
285
|
} else {
|
|
123
286
|
return entityId => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId);
|
|
124
287
|
}
|
|
288
|
+
case "getInBlockSync" :
|
|
289
|
+
return entityId => getInBlockSyncHandler(params, entityId);
|
|
125
290
|
case "getOrCreate" :
|
|
126
291
|
if (isClickHouseOnly) {
|
|
127
292
|
return _entity => throwClickHouseReadOnly(params.entityConfig, "getOrCreate");
|
|
@@ -147,12 +312,16 @@ let entityTraps_get = (params, prop) => {
|
|
|
147
312
|
}
|
|
148
313
|
});
|
|
149
314
|
}
|
|
315
|
+
case "getSync" :
|
|
316
|
+
return entityId => getSyncHandler(params, entityId);
|
|
150
317
|
case "getWhere" :
|
|
151
318
|
if (isClickHouseOnly) {
|
|
152
319
|
return _filter => throwClickHouseReadOnly(params.entityConfig, "getWhere");
|
|
153
320
|
} else {
|
|
154
321
|
return filter => getWhereHandler(params, filter);
|
|
155
322
|
}
|
|
323
|
+
case "getWhereSync" :
|
|
324
|
+
return filter => getWhereSyncHandler(params, filter);
|
|
156
325
|
case "set" :
|
|
157
326
|
return set;
|
|
158
327
|
default:
|
|
@@ -164,16 +333,63 @@ let entityTraps = {
|
|
|
164
333
|
get: entityTraps_get
|
|
165
334
|
};
|
|
166
335
|
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
336
|
+
async function runSyncRound(params, fn, round) {
|
|
337
|
+
if (round > 10000) {
|
|
338
|
+
Stdlib_JsError.throwWithMessage(`The handler suspended on a synchronous read too many times: gave up after ` + (10000).toString() + ` rounds. This usually means the code isn't deterministic across reruns.`);
|
|
339
|
+
}
|
|
340
|
+
params.sync.status = "Active";
|
|
341
|
+
params.sync.pending = undefined;
|
|
342
|
+
let suspended;
|
|
343
|
+
try {
|
|
344
|
+
fn();
|
|
345
|
+
suspended = false;
|
|
346
|
+
} catch (raw_exn) {
|
|
347
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
348
|
+
if (exn.RE_EXN_ID === Suspend) {
|
|
349
|
+
suspended = true;
|
|
350
|
+
} else {
|
|
351
|
+
let pending = params.sync.pending;
|
|
352
|
+
if (pending !== undefined) {
|
|
353
|
+
params.sync.pending = undefined;
|
|
354
|
+
pending.forEach(promise => {
|
|
355
|
+
Utils.$$Promise.silentCatch(promise);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
throw exn;
|
|
359
|
+
}
|
|
170
360
|
}
|
|
361
|
+
let pending$1 = params.sync.pending;
|
|
362
|
+
if (pending$1 === undefined) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
params.sync.pending = undefined;
|
|
366
|
+
if (suspended) {
|
|
367
|
+
let errors = [];
|
|
368
|
+
await Promise.all(pending$1.map(promise => Stdlib_Promise.$$catch(promise, exn => {
|
|
369
|
+
errors.push(exn);
|
|
370
|
+
return Promise.resolve();
|
|
371
|
+
})));
|
|
372
|
+
let exn$1 = errors[0];
|
|
373
|
+
if (exn$1 !== undefined) {
|
|
374
|
+
throw exn$1;
|
|
375
|
+
}
|
|
376
|
+
return await runSyncRound(params, fn, round + 1 | 0);
|
|
377
|
+
}
|
|
378
|
+
pending$1.forEach(promise => {
|
|
379
|
+
Utils.$$Promise.silentCatch(promise);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let handlerTraps_get = (params, prop) => {
|
|
384
|
+
checkStatusOrThrow(params, `context.` + prop);
|
|
171
385
|
switch (prop) {
|
|
172
386
|
case "chain" :
|
|
173
387
|
let chainId = Internal.getItemChainId(params.item);
|
|
174
388
|
return params.chains[chainId];
|
|
175
389
|
case "effect" :
|
|
176
390
|
return initEffect(params);
|
|
391
|
+
case "effectSync" :
|
|
392
|
+
return initEffectSync(params);
|
|
177
393
|
case "isPreload" :
|
|
178
394
|
return params.isPreload;
|
|
179
395
|
case "log" :
|
|
@@ -182,6 +398,8 @@ let handlerTraps_get = (params, prop) => {
|
|
|
182
398
|
} else {
|
|
183
399
|
return Ecosystem.getItemUserLogger(params.item, params.config.ecosystem);
|
|
184
400
|
}
|
|
401
|
+
case "runSync" :
|
|
402
|
+
return fn => runSyncRound(params, fn, 1);
|
|
185
403
|
default:
|
|
186
404
|
let entityConfig = params.config.userEntitiesByName[prop];
|
|
187
405
|
if (entityConfig !== undefined) {
|
|
@@ -194,7 +412,7 @@ let handlerTraps_get = (params, prop) => {
|
|
|
194
412
|
isPreload: params.isPreload,
|
|
195
413
|
chains: params.chains,
|
|
196
414
|
config: params.config,
|
|
197
|
-
|
|
415
|
+
sync: params.sync,
|
|
198
416
|
entityConfig: entityConfig
|
|
199
417
|
}, entityTraps);
|
|
200
418
|
} else {
|
|
@@ -211,16 +429,34 @@ function getHandlerContext(params) {
|
|
|
211
429
|
return new Proxy(params, handlerTraps);
|
|
212
430
|
}
|
|
213
431
|
|
|
432
|
+
let maxSyncRounds = 10000;
|
|
433
|
+
|
|
214
434
|
export {
|
|
435
|
+
Suspend,
|
|
436
|
+
isSuspend,
|
|
437
|
+
makeSyncState,
|
|
438
|
+
getPending,
|
|
439
|
+
getMemo,
|
|
440
|
+
checkStatusOrThrow,
|
|
441
|
+
scheduleAndSuspend,
|
|
215
442
|
paramsByThis,
|
|
216
443
|
effectContextPrototype,
|
|
444
|
+
prepareEffectCall,
|
|
445
|
+
callEffectAsync,
|
|
217
446
|
initEffect,
|
|
447
|
+
effectMemoKey,
|
|
448
|
+
initEffectSync,
|
|
218
449
|
entityScope,
|
|
219
450
|
getWhereHandler,
|
|
220
451
|
noopSet,
|
|
221
452
|
noopDeleteUnsafe,
|
|
222
453
|
throwClickHouseReadOnly,
|
|
454
|
+
getSyncHandler,
|
|
455
|
+
getWhereSyncHandler,
|
|
456
|
+
getInBlockSyncHandler,
|
|
223
457
|
entityTraps,
|
|
458
|
+
maxSyncRounds,
|
|
459
|
+
runSyncRound,
|
|
224
460
|
handlerTraps,
|
|
225
461
|
getHandlerContext,
|
|
226
462
|
}
|
|
@@ -468,16 +468,16 @@ let getSourceNewHeight = async (
|
|
|
468
468
|
// head on every (re)connect; waking the wait loop on a height we already
|
|
469
469
|
// know spins it and leaks fallback pollers (#1270).
|
|
470
470
|
if newHeight > sourceState.knownHeight {
|
|
471
|
+
sourceState->recordRequestStats([{Source.method: "heightPush", seconds: 0.}])
|
|
471
472
|
sourceState.knownHeight = newHeight
|
|
472
473
|
let resolvers = sourceState.pendingHeightResolvers
|
|
473
474
|
sourceState.pendingHeightResolvers = []
|
|
474
475
|
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
476
|
+
} else {
|
|
477
|
+
sourceState->recordRequestStats([{Source.method: "heightPushIgnored", seconds: 0.}])
|
|
475
478
|
}
|
|
476
479
|
})
|
|
477
480
|
sourceState.unsubscribe = Some(unsubscribe)
|
|
478
|
-
// Count a subscription (re)start rather than every pushed height —
|
|
479
|
-
// there's no request/response to time here.
|
|
480
|
-
sourceState->recordRequestStats([{Source.method: "heightSubscription", seconds: 0.}])
|
|
481
481
|
| _ =>
|
|
482
482
|
// Slowdown polling when the chain isn't progressing
|
|
483
483
|
let pollingInterval = if reducedPolling {
|
|
@@ -724,9 +724,7 @@ let executeQuery = async (
|
|
|
724
724
|
) {
|
|
725
725
|
| Some(s) =>
|
|
726
726
|
if s.source !== sourceManager.activeSource {
|
|
727
|
-
let logger = Logging.createChild(
|
|
728
|
-
~params={"chainId": sourceManager.activeSource.chainId},
|
|
729
|
-
)
|
|
727
|
+
let logger = Logging.createChild(~params={"chainId": sourceManager.activeSource.chainId})
|
|
730
728
|
logger->Logging.childInfo({
|
|
731
729
|
"msg": "Switching data-source",
|
|
732
730
|
"source": s.source.name,
|
|
@@ -736,9 +734,7 @@ let executeQuery = async (
|
|
|
736
734
|
}
|
|
737
735
|
s
|
|
738
736
|
| None =>
|
|
739
|
-
let logger = Logging.createChild(
|
|
740
|
-
~params={"chainId": sourceManager.activeSource.chainId},
|
|
741
|
-
)
|
|
737
|
+
let logger = Logging.createChild(~params={"chainId": sourceManager.activeSource.chainId})
|
|
742
738
|
%raw(`null`)->ErrorHandling.mkLogAndRaise(~logger, ~msg=noSourcesError)
|
|
743
739
|
}
|
|
744
740
|
sourceManager.activeSource = sourceState.source
|
|
@@ -895,9 +891,7 @@ let getBlockHashes = async (sourceManager: t, ~blockNumbers: array<int>, ~isReal
|
|
|
895
891
|
let sourceState = switch sourceManager->getNextSource(~isRealtime) {
|
|
896
892
|
| Some(s) => s
|
|
897
893
|
| None =>
|
|
898
|
-
let logger = Logging.createChild(
|
|
899
|
-
~params={"chainId": sourceManager.activeSource.chainId},
|
|
900
|
-
)
|
|
894
|
+
let logger = Logging.createChild(~params={"chainId": sourceManager.activeSource.chainId})
|
|
901
895
|
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
902
896
|
~logger,
|
|
903
897
|
~msg="No data-sources available for fetching block hashes.",
|
|
@@ -388,18 +388,21 @@ async function getSourceNewHeight(sourceManager, sourceState, knownHeight, stall
|
|
|
388
388
|
if (createSubscription !== undefined && isRealtime) {
|
|
389
389
|
let unsubscribe = createSubscription(newHeight => {
|
|
390
390
|
if (newHeight <= sourceState.knownHeight) {
|
|
391
|
-
return
|
|
391
|
+
return recordRequestStats(sourceState, [{
|
|
392
|
+
method: "heightPushIgnored",
|
|
393
|
+
seconds: 0
|
|
394
|
+
}]);
|
|
392
395
|
}
|
|
396
|
+
recordRequestStats(sourceState, [{
|
|
397
|
+
method: "heightPush",
|
|
398
|
+
seconds: 0
|
|
399
|
+
}]);
|
|
393
400
|
sourceState.knownHeight = newHeight;
|
|
394
401
|
let resolvers = sourceState.pendingHeightResolvers;
|
|
395
402
|
sourceState.pendingHeightResolvers = [];
|
|
396
403
|
resolvers.forEach(resolve => resolve(newHeight));
|
|
397
404
|
});
|
|
398
405
|
sourceState.unsubscribe = unsubscribe;
|
|
399
|
-
recordRequestStats(sourceState, [{
|
|
400
|
-
method: "heightSubscription",
|
|
401
|
-
seconds: 0
|
|
402
|
-
}]);
|
|
403
406
|
} else {
|
|
404
407
|
exit = 1;
|
|
405
408
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block handler's `block.timestamp`.
|
|
3
|
+
*
|
|
4
|
+
* envio hands a block handler only the block number, so the timestamp has to be
|
|
5
|
+
* fetched. Every handler invocation in a batch asks within the same microtask,
|
|
6
|
+
* so the requests are collected and answered by a single HyperSync range query
|
|
7
|
+
* rather than one round trip per block. An RPC endpoint, if one is configured,
|
|
8
|
+
* is the fallback for whatever HyperSync couldn't answer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createPublicClient, fallback, http } from "viem";
|
|
12
|
+
|
|
13
|
+
const API_TOKEN_ENV_VAR = "ENVIO_API_TOKEN";
|
|
14
|
+
|
|
15
|
+
const hypersyncUrl = (chainId: number) => `https://${chainId}.hypersync.xyz/query`;
|
|
16
|
+
|
|
17
|
+
type Waiter = {
|
|
18
|
+
resolve: (timestamp: bigint) => void;
|
|
19
|
+
reject: (error: unknown) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Blocks asked for since the last flush, by chain then block number. */
|
|
23
|
+
let pending = new Map<number, Map<number, Waiter[]>>();
|
|
24
|
+
let flushScheduled = false;
|
|
25
|
+
|
|
26
|
+
let rpcUrls: string[] = [];
|
|
27
|
+
let client: ReturnType<typeof createPublicClient> | null = null;
|
|
28
|
+
|
|
29
|
+
export function configureBlockTimestamps(urls: string[]) {
|
|
30
|
+
rpcUrls = urls;
|
|
31
|
+
client = null;
|
|
32
|
+
pending = new Map();
|
|
33
|
+
flushScheduled = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function requestBlockTimestamp(chainId: number, blockNumber: number): Promise<bigint> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let blocks = pending.get(chainId);
|
|
39
|
+
if (!blocks) {
|
|
40
|
+
blocks = new Map();
|
|
41
|
+
pending.set(chainId, blocks);
|
|
42
|
+
}
|
|
43
|
+
const waiters = blocks.get(blockNumber);
|
|
44
|
+
if (waiters) {
|
|
45
|
+
waiters.push({ resolve, reject });
|
|
46
|
+
} else {
|
|
47
|
+
blocks.set(blockNumber, [{ resolve, reject }]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!flushScheduled) {
|
|
51
|
+
flushScheduled = true;
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
flushScheduled = false;
|
|
54
|
+
const collected = pending;
|
|
55
|
+
pending = new Map();
|
|
56
|
+
for (const [chain, blocksForChain] of collected) {
|
|
57
|
+
void answer(chain, blocksForChain);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function answer(chainId: number, blocks: Map<number, Waiter[]>) {
|
|
65
|
+
let timestamps = new Map<number, bigint>();
|
|
66
|
+
|
|
67
|
+
// A batch can hold thousands of blocks, and one argument per block would
|
|
68
|
+
// overflow the call stack inside the try, where it would read as a HyperSync
|
|
69
|
+
// failure and fall back to one round trip per block.
|
|
70
|
+
let lowest = Infinity;
|
|
71
|
+
let highest = -Infinity;
|
|
72
|
+
for (const blockNumber of blocks.keys()) {
|
|
73
|
+
if (blockNumber < lowest) lowest = blockNumber;
|
|
74
|
+
if (blockNumber > highest) highest = blockNumber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
timestamps = await fromHyperSync(chainId, lowest, highest);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (rpcUrls.length === 0) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
for (const waiters of blocks.values()) {
|
|
83
|
+
for (const waiter of waiters) {
|
|
84
|
+
waiter.reject(
|
|
85
|
+
new Error(
|
|
86
|
+
`Envio Subgraph couldn't read a block timestamp from HyperSync: ${message}\n` +
|
|
87
|
+
`Set ${API_TOKEN_ENV_VAR} in .env or the environment — create one at\n` +
|
|
88
|
+
`https://envio.dev/app/api-tokens — or set ENVIO_SUBGRAPH_RPC to fall back to RPC.`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [blockNumber, waiters] of blocks) {
|
|
98
|
+
const known = timestamps.get(blockNumber);
|
|
99
|
+
if (known !== undefined) {
|
|
100
|
+
for (const waiter of waiters) waiter.resolve(known);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const timestamp = await fromRpc(blockNumber);
|
|
105
|
+
for (const waiter of waiters) waiter.resolve(timestamp);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type HyperSyncResponse = {
|
|
113
|
+
data: { blocks?: { number: number; timestamp: string }[] }[];
|
|
114
|
+
next_block: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
async function fromHyperSync(
|
|
118
|
+
chainId: number,
|
|
119
|
+
fromBlock: number,
|
|
120
|
+
toBlock: number,
|
|
121
|
+
): Promise<Map<number, bigint>> {
|
|
122
|
+
const token = process.env[API_TOKEN_ENV_VAR];
|
|
123
|
+
const timestamps = new Map<number, bigint>();
|
|
124
|
+
|
|
125
|
+
let cursor = fromBlock;
|
|
126
|
+
// HyperSync answers as much of the range as one response holds and points at
|
|
127
|
+
// where to resume, so a wide range takes more than one round trip.
|
|
128
|
+
while (cursor <= toBlock) {
|
|
129
|
+
const response = await fetch(hypersyncUrl(chainId), {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"content-type": "application/json",
|
|
133
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
from_block: cursor,
|
|
137
|
+
to_block: toBlock + 1,
|
|
138
|
+
include_all_blocks: true,
|
|
139
|
+
field_selection: { block: ["number", "timestamp"] },
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`HyperSync returned ${response.status} ${await response.text()}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const body = (await response.json()) as HyperSyncResponse;
|
|
148
|
+
for (const page of body.data ?? []) {
|
|
149
|
+
for (const block of page.blocks ?? []) {
|
|
150
|
+
timestamps.set(block.number, BigInt(block.timestamp));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!body.next_block || body.next_block <= cursor) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
cursor = body.next_block;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return timestamps;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fromRpc(blockNumber: number): Promise<bigint> {
|
|
164
|
+
if (rpcUrls.length === 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Envio Subgraph couldn't read the timestamp of block ${blockNumber}: HyperSync didn't\n` +
|
|
167
|
+
`return it, and no RPC fallback is configured. Set ENVIO_SUBGRAPH_RPC in .env or the\n` +
|
|
168
|
+
`environment.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
client ??= createPublicClient({
|
|
172
|
+
transport: fallback(rpcUrls.map((url) => http(url, { retryCount: 0 }))),
|
|
173
|
+
});
|
|
174
|
+
const block = await client.getBlock({ blockNumber: BigInt(blockNumber) });
|
|
175
|
+
return block.timestamp;
|
|
176
|
+
}
|