envio 3.15.0-subgraph → 3.16.0-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/README.md +40 -34
- package/evm.schema.json +3 -3
- package/fuel.schema.json +2 -2
- package/index.d.ts +3 -3
- package/licenses/README.md +1 -1
- package/package.json +8 -8
- package/src/Batch.res.mjs +5 -5
- package/src/BatchProcessing.res +1 -0
- package/src/BatchProcessing.res.mjs +1 -0
- package/src/ChainFetching.res +1 -6
- package/src/ChainFetching.res.mjs +1 -1
- package/src/ChainState.res +48 -7
- package/src/ChainState.res.mjs +21 -8
- package/src/Clock.res +57 -0
- package/src/Clock.res.mjs +72 -0
- package/src/Config.res +16 -4
- package/src/Config.res.mjs +5 -3
- package/src/Core.res +4 -40
- package/src/CrossChainState.res +10 -6
- package/src/CrossChainState.res.mjs +1 -1
- package/src/EffectState.res +10 -2
- package/src/EffectState.res.mjs +8 -4
- package/src/EffectState.resi +3 -1
- package/src/EntityId.res +2 -7
- package/src/EntityId.res.mjs +1 -8
- package/src/EntityTables.res +4 -1
- package/src/EntityTables.res.mjs +1 -1
- package/src/Env.res +4 -0
- package/src/Env.res.mjs +5 -0
- package/src/FetchState.res +157 -69
- package/src/FetchState.res.mjs +112 -47
- package/src/FinalizeBackfill.res +2 -4
- package/src/FinalizeBackfill.res.mjs +3 -4
- package/src/InMemoryTable.res +17 -16
- package/src/InMemoryTable.res.mjs +14 -15
- package/src/IndexerState.res +28 -4
- package/src/IndexerState.res.mjs +19 -6
- package/src/IndexerState.resi +4 -0
- package/src/Internal.res +0 -49
- package/src/Internal.res.mjs +1 -80
- package/src/LoadLayer.res +17 -10
- package/src/LoadLayer.res.mjs +11 -8
- package/src/Main.res.mjs +1 -1
- package/src/Persistence.res +3 -23
- package/src/Persistence.res.mjs +3 -3
- package/src/PreloadLookahead.res +42 -24
- package/src/PreloadLookahead.res.mjs +51 -30
- package/src/Rollback.res +0 -1
- package/src/Rollback.res.mjs +1 -1
- package/src/SafeCheckpointTracking.res +4 -4
- package/src/SafeCheckpointTracking.res.mjs +1 -1
- package/src/StagedRows.res +106 -0
- package/src/StagedRows.res.mjs +102 -0
- package/src/Staging.res +32 -14
- package/src/Staging.res.mjs +17 -20
- package/src/Storage.res +278 -973
- package/src/Storage.res.mjs +218 -720
- package/src/Supervisor.res +75 -56
- package/src/Supervisor.res.mjs +55 -26
- package/src/SyncWrites.res +24 -18
- package/src/SyncWrites.res.mjs +11 -11
- package/src/TestIndexer.res +22 -27
- package/src/TestIndexer.res.mjs +20 -17
- package/src/Throttler.res +15 -8
- package/src/Throttler.res.mjs +9 -6
- package/src/Throttler.resi +1 -1
- package/src/UserContext.res +13 -4
- package/src/UserContext.res.mjs +5 -5
- package/src/Utils.res +0 -6
- package/src/Utils.res.mjs +0 -5
- package/src/Writing.res +0 -1
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +0 -322
- package/src/bindings/ClickHouse.res.mjs +0 -338
- package/src/bindings/StorageClient.res +107 -174
- package/src/bindings/StorageClient.res.mjs +19 -8
- package/src/bindings/Vitest.res +0 -26
- package/src/bindings/Vitest.res.mjs +0 -3
- package/src/db/Table.res +24 -150
- package/src/db/Table.res.mjs +38 -272
- package/src/sources/ChainSources.res +2 -0
- package/src/sources/ChainSources.res.mjs +2 -2
- package/src/sources/EvmChain.res +3 -0
- package/src/sources/EvmChain.res.mjs +4 -2
- package/src/sources/EvmHyperSyncSource.res +2 -2
- package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
- package/src/sources/EvmRpcClient.res +13 -3
- package/src/sources/EvmRpcClient.res.mjs +4 -3
- package/src/sources/EvmRpcWs.res +4 -3
- package/src/sources/EvmRpcWs.res.mjs +2 -2
- package/src/sources/HeightFeed.res +31 -10
- package/src/sources/HeightFeed.res.mjs +10 -6
- package/src/sources/HeightStream.res +32 -17
- package/src/sources/HeightStream.res.mjs +10 -10
- package/src/sources/HyperSyncSSE.res +4 -6
- package/src/sources/HyperSyncSSE.res.mjs +2 -2
- package/src/sources/RpcSource.res +10 -1
- package/src/sources/RpcSource.res.mjs +4 -3
- package/src/sources/Source.res +3 -0
- package/src/sources/SourceManager.res +85 -104
- package/src/sources/SourceManager.res.mjs +18 -37
- package/src/sources/SourceManager.resi +3 -5
- package/src/sources/StartBlockResolver.res +1 -0
- package/src/sources/StartBlockResolver.res.mjs +4 -3
- package/src/sources/SvmHyperSyncSource.res +2 -2
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
- package/src/subgraph/graph-ts.ts +87 -28
- package/src/subgraph/runtime.ts +0 -1
- package/src/subgraph/scope.ts +0 -1
- package/svm.schema.json +3 -3
- package/src/PgWriting.res +0 -36
- package/src/PgWriting.res.mjs +0 -34
- package/src/db/IndexDefinition.res +0 -47
- package/src/db/IndexDefinition.res.mjs +0 -60
package/src/LoadLayer.res.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Clock from "./Clock.res.mjs";
|
|
3
4
|
import * as Utils from "./Utils.res.mjs";
|
|
4
5
|
import * as Logger from "./Logger.res.mjs";
|
|
5
6
|
import * as ChainId from "./ChainId.res.mjs";
|
|
@@ -28,7 +29,8 @@ function scopeKeySuffix(scope) {
|
|
|
28
29
|
|
|
29
30
|
function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, entityId) {
|
|
30
31
|
let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
|
|
31
|
-
let
|
|
32
|
+
let entityIdToKey = inMemTable.entityIdToKey;
|
|
33
|
+
let change = inMemTable.latestEntityChangeById[entityIdToKey(entityId)];
|
|
32
34
|
if (change !== undefined) {
|
|
33
35
|
return Promise.resolve(InMemoryTable.Entity.mapChangeToEntity(change));
|
|
34
36
|
}
|
|
@@ -51,13 +53,13 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
|
|
|
51
53
|
let entitiesMap = {};
|
|
52
54
|
for (let idx = 0, idx_finish = dbEntities.length; idx < idx_finish; ++idx) {
|
|
53
55
|
let entity = dbEntities[idx];
|
|
54
|
-
entitiesMap[entity.id] = entity;
|
|
56
|
+
entitiesMap[entityIdToKey(entity.id)] = entity;
|
|
55
57
|
}
|
|
56
58
|
let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
|
|
57
|
-
idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityId]));
|
|
59
|
+
idsToLoad.forEach(entityId => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entityId, entitiesMap[entityIdToKey(entityId)]));
|
|
58
60
|
return IndexerState.endStorageLoad(indexerState, timerRef, storageName, key, idsToLoad.length, dbEntities.length);
|
|
59
61
|
};
|
|
60
|
-
return LoadManager.call(loadManager, entityId, key, load,
|
|
62
|
+
return LoadManager.call(loadManager, entityId, key, load, entityIdToKey, shouldGroup, hash => hash in inMemTable.latestEntityChangeById, InMemoryTable.Entity.getUnsafe(inMemTable));
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function callEffect(effect, arg, inMemTable, timerRef, onError) {
|
|
@@ -70,7 +72,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
|
|
|
70
72
|
let promises = [];
|
|
71
73
|
let state = inMemTable.rateLimitState;
|
|
72
74
|
if (state !== undefined) {
|
|
73
|
-
let now =
|
|
75
|
+
let now = state.clock.now();
|
|
74
76
|
if (now >= state.windowStartTime + state.durationMs) {
|
|
75
77
|
state.availableCalls = state.callsPerDuration;
|
|
76
78
|
state.windowStartTime = now;
|
|
@@ -99,7 +101,7 @@ function executeWithRateLimit(effect, effectArgs, inMemTable, onError, isFromQue
|
|
|
99
101
|
nextWindowPromise = p;
|
|
100
102
|
} else {
|
|
101
103
|
millisUntilReset.contents = state.windowStartTime + state.durationMs - now | 0;
|
|
102
|
-
let p$1 =
|
|
104
|
+
let p$1 = Clock.delay(state.clock, "effect rate limit window", millisUntilReset.contents);
|
|
103
105
|
state.nextWindowPromise = p$1;
|
|
104
106
|
nextWindowPromise = p$1;
|
|
105
107
|
}
|
|
@@ -195,8 +197,9 @@ function indexFilter(inMemTable, filter, table) {
|
|
|
195
197
|
|
|
196
198
|
function getByIdInMemory(entityConfig, scope, indexerState, entityId) {
|
|
197
199
|
let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
let entityKey = inMemTable.entityIdToKey(entityId);
|
|
201
|
+
if (entityKey in inMemTable.latestEntityChangeById) {
|
|
202
|
+
return Primitive_option.some(InMemoryTable.Entity.getUnsafe(inMemTable)(entityKey));
|
|
200
203
|
}
|
|
201
204
|
}
|
|
202
205
|
|
package/src/Main.res.mjs
CHANGED
|
@@ -414,7 +414,7 @@ async function startIndexer(config, logger, persistence, resetOpt, isTestOpt, ex
|
|
|
414
414
|
Metrics.startRuntimeCollectors();
|
|
415
415
|
Server.startServer(logger, getMetrics, envioVersion, () => Persistence.getInitializedStorageOrThrow(persistence$1).dumpEffectCache().then(prim => {}), Metrics.collectRuntime, isDevelopmentMode);
|
|
416
416
|
}
|
|
417
|
-
let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined);
|
|
417
|
+
let state = IndexerState.makeFromDbState(config$2, logger, persistence$1, Persistence.getInitializedState(persistence$1), registrationsByChainId, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, undefined, undefined, Stdlib_Option.mapOr(Worker.config, false, worker => worker.holdRealtime), onError, undefined, undefined);
|
|
418
418
|
if (shouldUseTui) {
|
|
419
419
|
Tui.start(config$2, logger, () => IndexerState.toMetrics(state));
|
|
420
420
|
}
|
package/src/Persistence.res
CHANGED
|
@@ -117,8 +117,6 @@ type storage = {
|
|
|
117
117
|
// carries a matching row — storage doesn't interpret it.
|
|
118
118
|
initialize: (
|
|
119
119
|
~chainConfigs: array<Config.chain>=?,
|
|
120
|
-
~entities: array<Internal.entityConfig>=?,
|
|
121
|
-
~enums: array<Table.enumConfig<Table.enum>>=?,
|
|
122
120
|
~contractMapping: ContractMapping.t,
|
|
123
121
|
~envioInfo: JSON.t,
|
|
124
122
|
) => promise<initialState>,
|
|
@@ -129,16 +127,11 @@ type storage = {
|
|
|
129
127
|
// Brings an initialized storage a chain it doesn't have yet: its row, its
|
|
130
128
|
// partitions and its config addresses, as `initialize` would have created
|
|
131
129
|
// them. The chains already there are left untouched.
|
|
132
|
-
addChain: (
|
|
133
|
-
~chainConfig: Config.chain,
|
|
134
|
-
~entities: array<Internal.entityConfig>,
|
|
135
|
-
~contractMapping: ContractMapping.t,
|
|
136
|
-
) => promise<unit>,
|
|
130
|
+
addChain: (~chainConfig: Config.chain, ~contractMapping: ContractMapping.t) => promise<unit>,
|
|
137
131
|
// `chainIds` is what this run drives. An isolated run resumes a subset of
|
|
138
132
|
// the stored chains, and ClickHouse's resume trims past each resumed chain's
|
|
139
133
|
// checkpoint, so chains a sibling process is still writing must stay out.
|
|
140
134
|
resumeInitialState: (
|
|
141
|
-
~entities: array<Internal.entityConfig>,
|
|
142
135
|
~chainIds: array<ChainId.t>,
|
|
143
136
|
~contractMapping: ContractMapping.t,
|
|
144
137
|
) => promise<initialState>,
|
|
@@ -159,19 +152,12 @@ type storage = {
|
|
|
159
152
|
// never runs `finalizeBackfill`: an index dropped or invalidated while it was
|
|
160
153
|
// down would otherwise never be rebuilt. Best-effort, and safe to run with
|
|
161
154
|
// indexing live.
|
|
162
|
-
ensureSchemaIndexes: (
|
|
163
|
-
~entities: array<Internal.entityConfig>,
|
|
164
|
-
~chainIds: array<ChainId.t>,
|
|
165
|
-
) => promise<unit>,
|
|
155
|
+
ensureSchemaIndexes: (~chainIds: array<ChainId.t>) => promise<unit>,
|
|
166
156
|
// Creates every index the schema promises for the given chains, then stamps
|
|
167
157
|
// `ready_at` on them. Called once those chains finish backfill. The indexes
|
|
168
158
|
// are committed one at a time so a failure part way through doesn't undo the
|
|
169
159
|
// ones already built; `ready_at` is only written once they all verify.
|
|
170
|
-
finalizeBackfill: (
|
|
171
|
-
~entities: array<Internal.entityConfig>,
|
|
172
|
-
~chainIds: array<ChainId.t>,
|
|
173
|
-
~readyAt: Date.t,
|
|
174
|
-
) => promise<unit>,
|
|
160
|
+
finalizeBackfill: (~chainIds: array<ChainId.t>, ~readyAt: Date.t) => promise<unit>,
|
|
175
161
|
// This is to download cache from the database to .envio/cache
|
|
176
162
|
dumpEffectCache: unit => promise<unit>,
|
|
177
163
|
reset: unit => promise<unit>,
|
|
@@ -190,7 +176,6 @@ type storage = {
|
|
|
190
176
|
getRollbackTargetCheckpoint: (
|
|
191
177
|
~reorgChainId: ChainId.t,
|
|
192
178
|
~lastKnownValidBlockNumber: int,
|
|
193
|
-
~sequence: CheckpointSequence.t,
|
|
194
179
|
) => promise<option<Internal.checkpointId>>,
|
|
195
180
|
// Get rollback progress diff
|
|
196
181
|
getRollbackProgressDiff: (
|
|
@@ -214,7 +199,6 @@ type storage = {
|
|
|
214
199
|
~batch: Batch.t,
|
|
215
200
|
~rollback: option<rollback>,
|
|
216
201
|
~config: Config.t,
|
|
217
|
-
~allEntities: array<Internal.entityConfig>,
|
|
218
202
|
~updatedEffectsCache: array<updatedEffectCache>,
|
|
219
203
|
~updatedEntities: array<updatedEntity>,
|
|
220
204
|
// Addresses this batch registered, with the checkpoint that covers them.
|
|
@@ -339,8 +323,6 @@ let init = {
|
|
|
339
323
|
throw(Refused(Config.setUpMeanwhileMessage))
|
|
340
324
|
}
|
|
341
325
|
let initialState = await persistence.storage.initialize(
|
|
342
|
-
~entities=persistence.allEntities,
|
|
343
|
-
~enums=persistence.allEnums,
|
|
344
326
|
~chainConfigs,
|
|
345
327
|
~contractMapping,
|
|
346
328
|
~envioInfo,
|
|
@@ -397,13 +379,11 @@ let init = {
|
|
|
397
379
|
)->Array.getUnsafe(0)
|
|
398
380
|
await persistence.storage.addChain(
|
|
399
381
|
~chainConfig,
|
|
400
|
-
~entities=persistence.allEntities,
|
|
401
382
|
~contractMapping=stored.contractMapping,
|
|
402
383
|
)
|
|
403
384
|
}
|
|
404
385
|
if resume {
|
|
405
386
|
let initialState = await persistence.storage.resumeInitialState(
|
|
406
|
-
~entities=persistence.allEntities,
|
|
407
387
|
~chainIds=chainConfigs->Array.map(chain => chain.id),
|
|
408
388
|
~contractMapping=stored.contractMapping,
|
|
409
389
|
)
|
package/src/Persistence.res.mjs
CHANGED
|
@@ -94,7 +94,7 @@ async function init(persistence, logger, chainConfigs, contractMapping, envioInf
|
|
|
94
94
|
Error: new Error()
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
let initialState = await persistence.storage.initialize(chainConfigs$1,
|
|
97
|
+
let initialState = await persistence.storage.initialize(chainConfigs$1, contractMapping, envioInfo);
|
|
98
98
|
Logger.info(logger, `The indexer storage is ready. Starting indexing!`, undefined);
|
|
99
99
|
persistence.storageStatus = {
|
|
100
100
|
TAG: "Ready",
|
|
@@ -136,11 +136,11 @@ async function init(persistence, logger, chainConfigs, contractMapping, envioInf
|
|
|
136
136
|
chainId: chainConfig.id
|
|
137
137
|
});
|
|
138
138
|
let chainConfig$1 = (await StartBlockResolver.resolveAllOrThrow([chainConfig], logger, lowercaseAddresses, startBlockRetry, sourceTiming))[0];
|
|
139
|
-
await persistence.storage.addChain(chainConfig$1,
|
|
139
|
+
await persistence.storage.addChain(chainConfig$1, stored.contractMapping);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
if (resume) {
|
|
143
|
-
let initialState$1 = await persistence.storage.resumeInitialState(
|
|
143
|
+
let initialState$1 = await persistence.storage.resumeInitialState(chainConfigs.map(chain => chain.id), stored.contractMapping);
|
|
144
144
|
persistence.storageStatus = {
|
|
145
145
|
TAG: "Ready",
|
|
146
146
|
_0: initialState$1
|
package/src/PreloadLookahead.res
CHANGED
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
type t = {
|
|
27
27
|
items: array<Internal.item>,
|
|
28
28
|
indexOf: Utils.Map.t<Internal.item, int>,
|
|
29
|
-
absentReaders:
|
|
29
|
+
absentReaders: Map.t<string, array<int>>,
|
|
30
30
|
dirty: array<bool>,
|
|
31
31
|
// Milliseconds the item's preload rounds have run for, all runs together.
|
|
32
32
|
preloadMs: array<float>,
|
|
33
33
|
// By entity key, the writes items' preloads made: (item, entity or deleted).
|
|
34
|
-
overlay:
|
|
34
|
+
overlay: Map.t<string, array<(int, option<Internal.entity>)>>,
|
|
35
35
|
window: int,
|
|
36
36
|
// The last item execute finished; reads and writes at or before it are history.
|
|
37
37
|
mutable executed: int,
|
|
@@ -51,10 +51,10 @@ let start = (~items: array<Internal.item>) => {
|
|
|
51
51
|
Some({
|
|
52
52
|
items,
|
|
53
53
|
indexOf,
|
|
54
|
-
absentReaders:
|
|
54
|
+
absentReaders: Map.make(),
|
|
55
55
|
dirty: Array.make(~length=items->Array.length, false),
|
|
56
56
|
preloadMs: Array.make(~length=items->Array.length, 0.),
|
|
57
|
-
overlay:
|
|
57
|
+
overlay: Map.make(),
|
|
58
58
|
window,
|
|
59
59
|
executed: -1,
|
|
60
60
|
})
|
|
@@ -102,9 +102,9 @@ let recordAbsent = (~item: Internal.item, ~entityName, ~scope, ~entityId) =>
|
|
|
102
102
|
switch t.indexOf->Utils.Map.get(item) {
|
|
103
103
|
| Some(idx) if idx > t.executed =>
|
|
104
104
|
let key = entityKey(~entityName, ~scope, ~entityId)
|
|
105
|
-
switch t.absentReaders->
|
|
105
|
+
switch t.absentReaders->Map.get(key) {
|
|
106
106
|
| Some(readers) => readers->Array.push(idx)
|
|
107
|
-
| None => t.absentReaders->
|
|
107
|
+
| None => t.absentReaders->Map.set(key, [idx])
|
|
108
108
|
}
|
|
109
109
|
| _ => ()
|
|
110
110
|
}
|
|
@@ -113,22 +113,42 @@ let recordAbsent = (~item: Internal.item, ~entityName, ~scope, ~entityId) =>
|
|
|
113
113
|
let onSet = (~entityName, ~scope, ~entityId) =>
|
|
114
114
|
switch active.contents {
|
|
115
115
|
| None => ()
|
|
116
|
-
|
|
116
|
+
// Every write execute makes lands here, and most batches have no absent read.
|
|
117
|
+
| Some(t) if t.absentReaders->Map.size > 0 =>
|
|
117
118
|
let key = entityKey(~entityName, ~scope, ~entityId)
|
|
118
|
-
switch t.absentReaders->
|
|
119
|
+
switch t.absentReaders->Map.get(key) {
|
|
119
120
|
| None => ()
|
|
120
121
|
| Some(readers) =>
|
|
121
|
-
t.absentReaders->
|
|
122
|
+
t.absentReaders->Map.delete(key)->ignore
|
|
122
123
|
readers->Array.forEach(idx =>
|
|
123
124
|
if idx > t.executed {
|
|
124
125
|
t.dirty->Array.setUnsafe(idx, true)
|
|
125
126
|
}
|
|
126
127
|
)
|
|
127
128
|
}
|
|
129
|
+
| Some(_) => ()
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
let isActive = () => active.contents->Option.isSome
|
|
131
133
|
|
|
134
|
+
// How many of `writes` (sorted by item) are by an item before `item`. An entity
|
|
135
|
+
// every event of a batch touches (a popular holder) gathers a write per event,
|
|
136
|
+
// and a scan of them per read was quadratic in the batch.
|
|
137
|
+
let countWritesBeforeItem = (writes: array<(int, option<Internal.entity>)>, item) => {
|
|
138
|
+
let low = ref(0)
|
|
139
|
+
let high = ref(writes->Array.length)
|
|
140
|
+
while low.contents < high.contents {
|
|
141
|
+
let mid = (low.contents + high.contents) / 2
|
|
142
|
+
let (writer, _) = writes->Array.getUnsafe(mid)
|
|
143
|
+
if writer < item {
|
|
144
|
+
low := mid + 1
|
|
145
|
+
} else {
|
|
146
|
+
high := mid
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
low.contents
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
let overlaySet = (~item: Internal.item, ~entityName, ~scope, ~entityId, ~entity) =>
|
|
133
153
|
switch active.contents {
|
|
134
154
|
| None => ()
|
|
@@ -136,12 +156,15 @@ let overlaySet = (~item: Internal.item, ~entityName, ~scope, ~entityId, ~entity)
|
|
|
136
156
|
switch t.indexOf->Utils.Map.get(item) {
|
|
137
157
|
| Some(writer) =>
|
|
138
158
|
let key = entityKey(~entityName, ~scope, ~entityId)
|
|
139
|
-
|
|
140
|
-
| Some(writes) =>
|
|
141
|
-
|
|
159
|
+
switch t.overlay->Map.get(key) {
|
|
160
|
+
| Some(writes) =>
|
|
161
|
+
let at = writes->countWritesBeforeItem(writer)
|
|
162
|
+
switch writes->Array.get(at) {
|
|
163
|
+
| Some((idx, _)) if idx === writer => writes->Array.setUnsafe(at, (writer, entity))
|
|
164
|
+
| _ => writes->Array.splice(~start=at, ~remove=0, ~insert=[(writer, entity)])
|
|
165
|
+
}
|
|
166
|
+
| None => t.overlay->Map.set(key, [(writer, entity)])
|
|
142
167
|
}
|
|
143
|
-
writes->Array.push((writer, entity))
|
|
144
|
-
t.overlay->Dict.set(key, writes)
|
|
145
168
|
| None => ()
|
|
146
169
|
}
|
|
147
170
|
}
|
|
@@ -154,18 +177,13 @@ let overlayGet = (~item: Internal.item, ~entityName, ~scope, ~entityId) =>
|
|
|
154
177
|
| Some(t) =>
|
|
155
178
|
switch (
|
|
156
179
|
t.indexOf->Utils.Map.get(item),
|
|
157
|
-
t.overlay->
|
|
180
|
+
t.overlay->Map.get(entityKey(~entityName, ~scope, ~entityId)),
|
|
158
181
|
) {
|
|
159
182
|
| (Some(reader), Some(writes)) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
foundAt := writer
|
|
165
|
-
found := Some(entity)
|
|
166
|
-
}
|
|
167
|
-
)
|
|
168
|
-
found.contents
|
|
183
|
+
switch writes->Array.get(writes->countWritesBeforeItem(reader) - 1) {
|
|
184
|
+
| Some((writer, entity)) if writer > t.executed => Some(entity)
|
|
185
|
+
| _ => None
|
|
186
|
+
}
|
|
169
187
|
| _ => None
|
|
170
188
|
}
|
|
171
189
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Env from "./Env.res.mjs";
|
|
4
|
-
import * as Utils from "./Utils.res.mjs";
|
|
5
4
|
import * as ChainId from "./ChainId.res.mjs";
|
|
6
5
|
import * as Performance from "./bindings/Performance.res.mjs";
|
|
7
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
@@ -24,10 +23,10 @@ function start(items) {
|
|
|
24
23
|
active.contents = {
|
|
25
24
|
items: items,
|
|
26
25
|
indexOf: indexOf,
|
|
27
|
-
absentReaders:
|
|
26
|
+
absentReaders: new Map(),
|
|
28
27
|
dirty: Stdlib_Array.make(items.length, false),
|
|
29
28
|
preloadMs: Stdlib_Array.make(items.length, 0),
|
|
30
|
-
overlay:
|
|
29
|
+
overlay: new Map(),
|
|
31
30
|
window: Env.subgraphPreloadLookahead,
|
|
32
31
|
executed: -1
|
|
33
32
|
};
|
|
@@ -86,11 +85,11 @@ function recordAbsent(item, entityName, scope, entityId) {
|
|
|
86
85
|
return;
|
|
87
86
|
}
|
|
88
87
|
let key = entityKey(entityName, scope, entityId);
|
|
89
|
-
let readers = t.absentReaders
|
|
88
|
+
let readers = t.absentReaders.get(key);
|
|
90
89
|
if (readers !== undefined) {
|
|
91
90
|
readers.push(idx);
|
|
92
91
|
} else {
|
|
93
|
-
t.absentReaders
|
|
92
|
+
t.absentReaders.set(key, [idx]);
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -99,10 +98,13 @@ function onSet(entityName, scope, entityId) {
|
|
|
99
98
|
if (t === undefined) {
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
101
|
+
if (t.absentReaders.size <= 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
102
104
|
let key = entityKey(entityName, scope, entityId);
|
|
103
|
-
let readers = t.absentReaders
|
|
105
|
+
let readers = t.absentReaders.get(key);
|
|
104
106
|
if (readers !== undefined) {
|
|
105
|
-
|
|
107
|
+
t.absentReaders.delete(key);
|
|
106
108
|
readers.forEach(idx => {
|
|
107
109
|
if (idx > t.executed) {
|
|
108
110
|
t.dirty[idx] = true;
|
|
@@ -117,6 +119,21 @@ function isActive() {
|
|
|
117
119
|
return Stdlib_Option.isSome(active.contents);
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
function countWritesBeforeItem(writes, item) {
|
|
123
|
+
let low = 0;
|
|
124
|
+
let high = writes.length;
|
|
125
|
+
while (low < high) {
|
|
126
|
+
let mid = (low + high | 0) / 2 | 0;
|
|
127
|
+
let match = writes[mid];
|
|
128
|
+
if (match[0] < item) {
|
|
129
|
+
low = mid + 1 | 0;
|
|
130
|
+
} else {
|
|
131
|
+
high = mid;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return low;
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function overlaySet(item, entityName, scope, entityId, entity) {
|
|
121
138
|
let t = active.contents;
|
|
122
139
|
if (t === undefined) {
|
|
@@ -127,13 +144,27 @@ function overlaySet(item, entityName, scope, entityId, entity) {
|
|
|
127
144
|
return;
|
|
128
145
|
}
|
|
129
146
|
let key = entityKey(entityName, scope, entityId);
|
|
130
|
-
let writes = t.overlay
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
let writes = t.overlay.get(key);
|
|
148
|
+
if (writes !== undefined) {
|
|
149
|
+
let at = countWritesBeforeItem(writes, writer);
|
|
150
|
+
let match = writes[at];
|
|
151
|
+
if (match !== undefined && match[0] === writer) {
|
|
152
|
+
writes[at] = [
|
|
153
|
+
writer,
|
|
154
|
+
entity
|
|
155
|
+
];
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
writes.splice(at, 0, [
|
|
159
|
+
writer,
|
|
160
|
+
entity
|
|
161
|
+
]);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
t.overlay.set(key, [[
|
|
165
|
+
writer,
|
|
166
|
+
entity
|
|
167
|
+
]]);
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
function overlayGet(item, entityName, scope, entityId) {
|
|
@@ -142,28 +173,17 @@ function overlayGet(item, entityName, scope, entityId) {
|
|
|
142
173
|
return;
|
|
143
174
|
}
|
|
144
175
|
let match = t.indexOf.get(item);
|
|
145
|
-
let match$1 = t.overlay
|
|
176
|
+
let match$1 = t.overlay.get(entityKey(entityName, scope, entityId));
|
|
146
177
|
if (match === undefined) {
|
|
147
178
|
return;
|
|
148
179
|
}
|
|
149
180
|
if (match$1 === undefined) {
|
|
150
181
|
return;
|
|
151
182
|
}
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
contents: t.executed
|
|
157
|
-
};
|
|
158
|
-
match$1.forEach(param => {
|
|
159
|
-
let writer = param[0];
|
|
160
|
-
if (writer > foundAt.contents && writer < match) {
|
|
161
|
-
foundAt.contents = writer;
|
|
162
|
-
found.contents = Primitive_option.some(param[1]);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
return found.contents;
|
|
183
|
+
let match$2 = match$1[countWritesBeforeItem(match$1, match) - 1 | 0];
|
|
184
|
+
if (match$2 !== undefined && match$2[0] > t.executed) {
|
|
185
|
+
return Primitive_option.some(match$2[1]);
|
|
186
|
+
}
|
|
167
187
|
}
|
|
168
188
|
|
|
169
189
|
function afterItem(t, index, preload) {
|
|
@@ -190,6 +210,7 @@ export {
|
|
|
190
210
|
recordAbsent,
|
|
191
211
|
onSet,
|
|
192
212
|
isActive,
|
|
213
|
+
countWritesBeforeItem,
|
|
193
214
|
overlaySet,
|
|
194
215
|
overlayGet,
|
|
195
216
|
afterItem,
|
package/src/Rollback.res
CHANGED
|
@@ -130,7 +130,6 @@ and executeRollback = async (
|
|
|
130
130
|
switch await (state->IndexerState.persistence).storage.getRollbackTargetCheckpoint(
|
|
131
131
|
~reorgChainId=reorgChain,
|
|
132
132
|
~lastKnownValidBlockNumber=rollbackTargetBlockNumber,
|
|
133
|
-
~sequence=(state->IndexerState.config).checkpointSequence,
|
|
134
133
|
) {
|
|
135
134
|
| Some(checkpointId) => checkpointId
|
|
136
135
|
| None => 0n
|
package/src/Rollback.res.mjs
CHANGED
|
@@ -42,7 +42,7 @@ async function executeRollback(state, reorgChain, rollbackTargetBlockNumber, sch
|
|
|
42
42
|
Logger.info(logger, "Started rollback on reorg", undefined);
|
|
43
43
|
ChainState.setRollbackTargetBlock(IndexerState.getChainState(state, reorgChain), rollbackTargetBlockNumber);
|
|
44
44
|
await Writing.flush(state);
|
|
45
|
-
let checkpointId = await IndexerState.persistence(state).storage.getRollbackTargetCheckpoint(reorgChain, rollbackTargetBlockNumber
|
|
45
|
+
let checkpointId = await IndexerState.persistence(state).storage.getRollbackTargetCheckpoint(reorgChain, rollbackTargetBlockNumber);
|
|
46
46
|
let rollbackTargetCheckpointId = checkpointId !== undefined ? checkpointId : 0n;
|
|
47
47
|
let config = IndexerState.config(state);
|
|
48
48
|
let next = RollbackFloors.make(config.checkpointSequence, ChainMap.keys(config.chainMap), reorgChain, rollbackTargetCheckpointId, rollbackTargetBlockNumber);
|
|
@@ -16,9 +16,7 @@ let make = (
|
|
|
16
16
|
) => {
|
|
17
17
|
if maxReorgDepth > 0 && shouldRollbackOnReorg {
|
|
18
18
|
let checkpointIds = Utils.Array.jsArrayCreate(chainReorgCheckpoints->Array.length)
|
|
19
|
-
let checkpointBlockNumbers = Utils.Array.jsArrayCreate(
|
|
20
|
-
chainReorgCheckpoints->Array.length,
|
|
21
|
-
)
|
|
19
|
+
let checkpointBlockNumbers = Utils.Array.jsArrayCreate(chainReorgCheckpoints->Array.length)
|
|
22
20
|
chainReorgCheckpoints->Array.forEachWithIndex((checkpoint, idx) => {
|
|
23
21
|
checkpointIds->Array.setUnsafe(idx, checkpoint.checkpointId)
|
|
24
22
|
checkpointBlockNumbers->Array.setUnsafe(idx, checkpoint.blockNumber)
|
|
@@ -36,7 +34,9 @@ let make = (
|
|
|
36
34
|
let getSafeCheckpointId = (safeCheckpointTracking: t, ~sourceBlockNumber: int): option<
|
|
37
35
|
Internal.checkpointId,
|
|
38
36
|
> => {
|
|
39
|
-
|
|
37
|
+
// The first block reorg detection compares is itself reorgable, so the safe
|
|
38
|
+
// block is the one below it.
|
|
39
|
+
let safeBlockNumber = sourceBlockNumber - safeCheckpointTracking.maxReorgDepth - 1
|
|
40
40
|
|
|
41
41
|
switch safeCheckpointTracking.checkpointIds {
|
|
42
42
|
| [] => None
|
|
@@ -20,7 +20,7 @@ function make(maxReorgDepth, shouldRollbackOnReorg, chainReorgCheckpoints) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getSafeCheckpointId(safeCheckpointTracking, sourceBlockNumber) {
|
|
23
|
-
let safeBlockNumber = sourceBlockNumber - safeCheckpointTracking.maxReorgDepth | 0;
|
|
23
|
+
let safeBlockNumber = (sourceBlockNumber - safeCheckpointTracking.maxReorgDepth | 0) - 1 | 0;
|
|
24
24
|
let match = safeCheckpointTracking.checkpointIds;
|
|
25
25
|
if (match.length === 0) {
|
|
26
26
|
return;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Values as the addon declares each staged field has to arrive, compiled once
|
|
2
|
+
// per layout and written into a stage.
|
|
3
|
+
|
|
4
|
+
@send external getTime: unknown => float = "getTime"
|
|
5
|
+
@send external toFixed: unknown => string = "toFixed"
|
|
6
|
+
@val @scope("Array") external isArray: unknown => bool = "isArray"
|
|
7
|
+
|
|
8
|
+
// A list element as its JSON: the encoding a scalar of its type would take,
|
|
9
|
+
// except that a boolean stays one and bytes become their values.
|
|
10
|
+
%%private(
|
|
11
|
+
let elementOf = (encoding: Staging.encoding, ~column): (unknown => unknown) =>
|
|
12
|
+
switch encoding {
|
|
13
|
+
| Number =>
|
|
14
|
+
value =>
|
|
15
|
+
value->Staging.toNumber->Staging.finiteOrThrow(~column)->(Utils.magic: float => unknown)
|
|
16
|
+
| Date => value => value->getTime->(Utils.magic: float => unknown)
|
|
17
|
+
| Digits => value => value->Staging.stringOf->(Utils.magic: string => unknown)
|
|
18
|
+
| Decimal => value => value->toFixed->(Utils.magic: string => unknown)
|
|
19
|
+
| Document => value => value->Sql.canonicalDocument->(Utils.magic: string => unknown)
|
|
20
|
+
| Bytes =>
|
|
21
|
+
value =>
|
|
22
|
+
value
|
|
23
|
+
->(Utils.magic: unknown => Array.arrayLike<int>)
|
|
24
|
+
->Array.fromArrayLike
|
|
25
|
+
->(Utils.magic: array<int> => unknown)
|
|
26
|
+
| Bool | Text | List => value => value
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
%%private(
|
|
31
|
+
let rec mapList = (value: unknown, element: unknown => unknown): unknown =>
|
|
32
|
+
if value->isArray {
|
|
33
|
+
value
|
|
34
|
+
->(Utils.magic: unknown => array<unknown>)
|
|
35
|
+
->Array.map(item => item->mapList(element))
|
|
36
|
+
->(Utils.magic: array<unknown> => unknown)
|
|
37
|
+
} else if value === %raw(`undefined`) || value === %raw(`null`) {
|
|
38
|
+
%raw(`null`)
|
|
39
|
+
} else {
|
|
40
|
+
element(value)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// What a present value becomes before it is written to its slot.
|
|
45
|
+
%%private(
|
|
46
|
+
let encoderOf = ({name, encoding, element}: Staging.stagedField): (unknown => unknown) =>
|
|
47
|
+
switch encoding {
|
|
48
|
+
| Bool =>
|
|
49
|
+
value => (value->(Utils.magic: unknown => bool) ? 1. : 0.)->(Utils.magic: float => unknown)
|
|
50
|
+
| Date => value => value->getTime->(Utils.magic: float => unknown)
|
|
51
|
+
| Decimal => value => value->toFixed->(Utils.magic: string => unknown)
|
|
52
|
+
| Document => value => value->Sql.canonicalDocument->(Utils.magic: string => unknown)
|
|
53
|
+
| List =>
|
|
54
|
+
let encode = elementOf(element, ~column=name)
|
|
55
|
+
// A document can be a list itself, so a list of them is one level deep.
|
|
56
|
+
let mapList = switch element {
|
|
57
|
+
| Document =>
|
|
58
|
+
(value, encode) =>
|
|
59
|
+
value
|
|
60
|
+
->(Utils.magic: unknown => array<unknown>)
|
|
61
|
+
->Array.map(item => item->Staging.isAbsent ? %raw(`null`) : encode(item))
|
|
62
|
+
->(Utils.magic: array<unknown> => unknown)
|
|
63
|
+
| _ => mapList
|
|
64
|
+
}
|
|
65
|
+
value =>
|
|
66
|
+
value
|
|
67
|
+
->mapList(encode)
|
|
68
|
+
->(Utils.magic: unknown => JSON.t)
|
|
69
|
+
->JSON.stringify
|
|
70
|
+
->Utils.replaceLoneSurrogateEscapes
|
|
71
|
+
->(Utils.magic: string => unknown)
|
|
72
|
+
| Number | Text | Digits | Bytes => value => value
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// A layout's fields compiled once: the slot each takes and how a value of it
|
|
77
|
+
// is written there.
|
|
78
|
+
type writer = {field: Staging.stagedField, encode: unknown => unknown}
|
|
79
|
+
|
|
80
|
+
let writersOf = (fields: array<Staging.stagedField>) =>
|
|
81
|
+
fields->Array.map(field => {field, encode: encoderOf(field)})
|
|
82
|
+
|
|
83
|
+
let columnsOf = (writers: array<writer>): array<Staging.column> =>
|
|
84
|
+
writers->Array.map(({field}) => {
|
|
85
|
+
Staging.name: field.name,
|
|
86
|
+
kind: field.encoding->Staging.kindOf,
|
|
87
|
+
isNullable: field.isNullable,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// JSON `null` is a document of its own, which a column that can't be NULL
|
|
91
|
+
// stores as one. Left out, the value is no document and is refused like any
|
|
92
|
+
// other.
|
|
93
|
+
let writeField = (stage, ~writers: array<writer>, ~column, ~row, value: unknown) => {
|
|
94
|
+
let {field, encode} = writers->Array.getUnsafe(column)
|
|
95
|
+
if value->Staging.isAbsent {
|
|
96
|
+
stage->Staging.writeValue(
|
|
97
|
+
~column,
|
|
98
|
+
~row,
|
|
99
|
+
field.encoding === Staging.Document && !field.isNullable && value === %raw(`null`)
|
|
100
|
+
? "null"->(Utils.magic: string => unknown)
|
|
101
|
+
: value,
|
|
102
|
+
)
|
|
103
|
+
} else {
|
|
104
|
+
stage->Staging.writeValue(~column, ~row, encode(value))
|
|
105
|
+
}
|
|
106
|
+
}
|