envio 3.0.2 → 3.1.0-rc.1
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 +0 -1
- package/evm.schema.json +15 -8
- package/fuel.schema.json +19 -12
- package/index.d.ts +0 -2
- package/package.json +6 -7
- package/rescript.json +1 -1
- package/src/Batch.res +4 -214
- package/src/Batch.res.mjs +6 -165
- package/src/ChainFetcher.res +12 -28
- package/src/ChainFetcher.res.mjs +8 -17
- package/src/ChainManager.res +10 -9
- package/src/ChainManager.res.mjs +6 -10
- package/src/Config.res +9 -25
- package/src/Config.res.mjs +17 -27
- package/src/Core.res +7 -0
- package/src/Ctx.res +1 -0
- package/src/Env.res +0 -8
- package/src/Env.res.mjs +0 -6
- package/src/EventConfigBuilder.res +13 -123
- package/src/EventConfigBuilder.res.mjs +6 -73
- package/src/EventProcessing.res +5 -29
- package/src/EventProcessing.res.mjs +11 -20
- package/src/EventUtils.res +0 -27
- package/src/EventUtils.res.mjs +0 -24
- package/src/FetchState.res +2 -15
- package/src/FetchState.res.mjs +3 -18
- package/src/GlobalState.res +26 -39
- package/src/GlobalState.res.mjs +12 -40
- package/src/HandlerLoader.res +6 -5
- package/src/HandlerLoader.res.mjs +27 -9
- package/src/HandlerRegister.res +1 -12
- package/src/HandlerRegister.res.mjs +1 -6
- package/src/HandlerRegister.resi +1 -1
- package/src/Hasura.res +96 -32
- package/src/Hasura.res.mjs +93 -38
- package/src/InMemoryStore.res +205 -45
- package/src/InMemoryStore.res.mjs +157 -40
- package/src/InMemoryTable.res +165 -249
- package/src/InMemoryTable.res.mjs +156 -227
- package/src/Internal.res +10 -34
- package/src/Internal.res.mjs +9 -3
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +5 -5
- package/src/LogSelection.res +15 -19
- package/src/LogSelection.res.mjs +5 -6
- package/src/Main.res +4 -6
- package/src/Main.res.mjs +26 -15
- package/src/Persistence.res +7 -132
- package/src/Persistence.res.mjs +1 -102
- package/src/PgStorage.res +57 -40
- package/src/PgStorage.res.mjs +60 -34
- package/src/ReorgDetection.res +35 -58
- package/src/ReorgDetection.res.mjs +21 -29
- package/src/SimulateItems.res.mjs +21 -3
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +1 -1
- package/src/TableIndices.res +9 -2
- package/src/TableIndices.res.mjs +7 -1
- package/src/TestIndexer.res +53 -60
- package/src/TestIndexer.res.mjs +77 -63
- package/src/TestIndexerProxyStorage.res +4 -14
- package/src/TestIndexerProxyStorage.res.mjs +1 -5
- package/src/UserContext.res +2 -4
- package/src/UserContext.res.mjs +4 -5
- package/src/Utils.res +0 -2
- package/src/Utils.res.mjs +0 -3
- package/src/bindings/ClickHouse.res +45 -38
- package/src/bindings/ClickHouse.res.mjs +16 -17
- package/src/bindings/Vitest.res +3 -0
- package/src/db/InternalTable.res +59 -18
- package/src/db/InternalTable.res.mjs +82 -51
- package/src/db/Table.res +9 -2
- package/src/db/Table.res.mjs +10 -7
- package/src/sources/EnvioApiClient.res +15 -0
- package/src/sources/EnvioApiClient.res.mjs +24 -0
- package/src/sources/EvmChain.res +32 -10
- package/src/sources/EvmChain.res.mjs +31 -5
- package/src/sources/HyperFuelSource.res +15 -58
- package/src/sources/HyperFuelSource.res.mjs +20 -39
- package/src/sources/HyperSync.res +54 -100
- package/src/sources/HyperSync.res.mjs +67 -96
- package/src/sources/HyperSync.resi +4 -22
- package/src/sources/HyperSyncClient.res +70 -247
- package/src/sources/HyperSyncClient.res.mjs +47 -46
- package/src/sources/HyperSyncSource.res +94 -166
- package/src/sources/HyperSyncSource.res.mjs +100 -127
- package/src/sources/RpcSource.res +43 -22
- package/src/sources/RpcSource.res.mjs +50 -35
- package/src/sources/SimulateSource.res +1 -7
- package/src/sources/SimulateSource.res.mjs +1 -7
- package/src/sources/Source.res +10 -1
- package/src/sources/Source.res.mjs +3 -0
- package/src/sources/SourceManager.res +177 -8
- package/src/sources/SourceManager.res.mjs +141 -3
- package/src/sources/SourceManager.resi +19 -0
- package/src/tui/Tui.res +44 -6
- package/src/tui/Tui.res.mjs +56 -8
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +11 -4
- package/src/sources/HyperSyncJsonApi.res +0 -390
- package/src/sources/HyperSyncJsonApi.res.mjs +0 -237
package/src/PgStorage.res.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import * as Persistence from "./Persistence.res.mjs";
|
|
|
19
19
|
import * as ChainFetcher from "./ChainFetcher.res.mjs";
|
|
20
20
|
import * as EntityHistory from "./db/EntityHistory.res.mjs";
|
|
21
21
|
import * as InternalTable from "./db/InternalTable.res.mjs";
|
|
22
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
22
23
|
import * as Child_process from "child_process";
|
|
23
24
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
24
25
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
@@ -125,7 +126,8 @@ function getEntityHistory(entityConfig) {
|
|
|
125
126
|
isPrimaryKey: true,
|
|
126
127
|
isIndex: field$1.isIndex,
|
|
127
128
|
linkedEntity: field$1.linkedEntity,
|
|
128
|
-
defaultValue: field$1.defaultValue
|
|
129
|
+
defaultValue: field$1.defaultValue,
|
|
130
|
+
description: field$1.description
|
|
129
131
|
}
|
|
130
132
|
};
|
|
131
133
|
} else {
|
|
@@ -140,19 +142,20 @@ function getEntityHistory(entityConfig) {
|
|
|
140
142
|
isPrimaryKey: field$1.isPrimaryKey,
|
|
141
143
|
isIndex: false,
|
|
142
144
|
linkedEntity: field$1.linkedEntity,
|
|
143
|
-
defaultValue: field$1.defaultValue
|
|
145
|
+
defaultValue: field$1.defaultValue,
|
|
146
|
+
description: field$1.description
|
|
144
147
|
}
|
|
145
148
|
};
|
|
146
149
|
}
|
|
147
150
|
});
|
|
148
|
-
let actionField = Table.mkField(EntityHistory.changeFieldName, EntityHistory.changeFieldType, S$RescriptSchema.never, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
149
|
-
let checkpointIdField = Table.mkField(EntityHistory.checkpointIdFieldName, EntityHistory.checkpointIdFieldType, EntityHistory.unsafeCheckpointIdSchema, undefined, undefined, undefined, true, undefined, undefined);
|
|
151
|
+
let actionField = Table.mkField(EntityHistory.changeFieldName, EntityHistory.changeFieldType, S$RescriptSchema.never, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
152
|
+
let checkpointIdField = Table.mkField(EntityHistory.checkpointIdFieldName, EntityHistory.checkpointIdFieldType, EntityHistory.unsafeCheckpointIdSchema, undefined, undefined, undefined, true, undefined, undefined, undefined);
|
|
150
153
|
let entityTableName = entityConfig.table.tableName;
|
|
151
154
|
let historyTableName = EntityHistory.historyTableName(entityTableName, entityConfig.index);
|
|
152
155
|
let table = Table.mkTable(historyTableName, undefined, Belt_Array.concat(dataFields, [
|
|
153
156
|
checkpointIdField,
|
|
154
157
|
actionField
|
|
155
|
-
]));
|
|
158
|
+
]), undefined);
|
|
156
159
|
let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema);
|
|
157
160
|
let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
|
|
158
161
|
let cache$1 = {
|
|
@@ -506,7 +509,7 @@ function executeSet(sql, items, dbFunction) {
|
|
|
506
509
|
}
|
|
507
510
|
}
|
|
508
511
|
|
|
509
|
-
async function writeBatch(sql, batch, rawEvents, pgSchema,
|
|
512
|
+
async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, escapeTables) {
|
|
510
513
|
try {
|
|
511
514
|
let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold);
|
|
512
515
|
let specificError = {
|
|
@@ -514,41 +517,54 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
|
|
|
514
517
|
};
|
|
515
518
|
let setRawEvents = __x => executeSet(__x, rawEvents, (sql, items) => setOrThrow(sql, items, InternalTable.RawEvents.table, InternalTable.RawEvents.schema, pgSchema));
|
|
516
519
|
let setEntities = Belt_Array.map(updatedEntities, param => {
|
|
517
|
-
let updates = param.updates;
|
|
518
520
|
let entityConfig = param.entityConfig;
|
|
519
521
|
let entitiesToSet = [];
|
|
520
522
|
let idsToDelete = [];
|
|
521
|
-
|
|
522
|
-
|
|
523
|
+
let diffCheckpointId = Stdlib_Option.map(rollback, r => r.diffCheckpointId);
|
|
524
|
+
let batchSetUpdates = [];
|
|
525
|
+
let batchDeleteEntityIds = [];
|
|
526
|
+
let batchDeleteCheckpointIds = [];
|
|
527
|
+
let idsWithDiff = new Set();
|
|
528
|
+
let latestChangeById = {};
|
|
529
|
+
let orderedIds = [];
|
|
530
|
+
Belt_Array.forEach(param.changes, change => {
|
|
531
|
+
let entityId = change.entityId;
|
|
532
|
+
if (Stdlib_Option.isNone(latestChangeById[entityId])) {
|
|
533
|
+
orderedIds.push(entityId);
|
|
534
|
+
}
|
|
535
|
+
latestChangeById[entityId] = change;
|
|
536
|
+
if (!shouldSaveHistory) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (change.checkpointId === diffCheckpointId) {
|
|
540
|
+
idsWithDiff.add(entityId);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (change.type === "SET") {
|
|
544
|
+
batchSetUpdates.push(change);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
batchDeleteEntityIds.push(change.entityId);
|
|
548
|
+
batchDeleteCheckpointIds.push(change.checkpointId);
|
|
549
|
+
});
|
|
550
|
+
let backfillHistoryIds = new Set();
|
|
551
|
+
Belt_Array.forEach(orderedIds, entityId => {
|
|
552
|
+
let match = latestChangeById[entityId];
|
|
523
553
|
if (match.type === "SET") {
|
|
524
554
|
entitiesToSet.push(match.entity);
|
|
555
|
+
} else {
|
|
556
|
+
idsToDelete.push(match.entityId);
|
|
557
|
+
}
|
|
558
|
+
if (shouldSaveHistory && !idsWithDiff.has(entityId)) {
|
|
559
|
+
backfillHistoryIds.add(entityId);
|
|
525
560
|
return;
|
|
526
561
|
}
|
|
527
|
-
idsToDelete.push(match.entityId);
|
|
528
562
|
});
|
|
529
563
|
let shouldRemoveInvalidUtf8 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables).has(entityConfig.table) : false;
|
|
530
564
|
return async sql => {
|
|
531
565
|
try {
|
|
532
566
|
let promises = [];
|
|
533
567
|
if (shouldSaveHistory) {
|
|
534
|
-
let backfillHistoryIds = new Set();
|
|
535
|
-
let batchSetUpdates = [];
|
|
536
|
-
let batchDeleteCheckpointIds = [];
|
|
537
|
-
let batchDeleteEntityIds = [];
|
|
538
|
-
updates.forEach(update => {
|
|
539
|
-
let containsRollbackDiffChange = update.containsRollbackDiffChange;
|
|
540
|
-
update.history.forEach(change => {
|
|
541
|
-
if (!containsRollbackDiffChange) {
|
|
542
|
-
backfillHistoryIds.add(change.entityId);
|
|
543
|
-
}
|
|
544
|
-
if (change.type === "SET") {
|
|
545
|
-
batchSetUpdates.push(change);
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
batchDeleteEntityIds.push(change.entityId);
|
|
549
|
-
batchDeleteCheckpointIds.push(change.checkpointId);
|
|
550
|
-
});
|
|
551
|
-
});
|
|
552
568
|
if (backfillHistoryIds.size !== 0) {
|
|
553
569
|
await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.name, entityConfig.index, Array.from(backfillHistoryIds));
|
|
554
570
|
}
|
|
@@ -615,11 +631,17 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
|
|
|
615
631
|
}
|
|
616
632
|
};
|
|
617
633
|
});
|
|
618
|
-
let rollbackTables
|
|
634
|
+
let rollbackTables;
|
|
635
|
+
if (rollback !== undefined) {
|
|
636
|
+
let rollbackTargetCheckpointId = rollback.targetCheckpointId;
|
|
637
|
+
rollbackTables = sql => {
|
|
619
638
|
let promises = allEntities.map(entityConfig => EntityHistory.rollback(sql, pgSchema, entityConfig.name, entityConfig.index, rollbackTargetCheckpointId));
|
|
620
639
|
promises.push(InternalTable.Checkpoints.rollback(sql, pgSchema, rollbackTargetCheckpointId));
|
|
621
640
|
return Promise.all(promises);
|
|
622
|
-
}
|
|
641
|
+
};
|
|
642
|
+
} else {
|
|
643
|
+
rollbackTables = undefined;
|
|
644
|
+
}
|
|
623
645
|
try {
|
|
624
646
|
await Promise.all([
|
|
625
647
|
sql.begin(async sql => {
|
|
@@ -665,7 +687,7 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollbackTargetCheckpo
|
|
|
665
687
|
if (exn$1.RE_EXN_ID === PgEncodingError) {
|
|
666
688
|
let escapeTables$1 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables) : new Set();
|
|
667
689
|
escapeTables$1.add(exn$1.table);
|
|
668
|
-
return await writeBatch(sql, batch, rawEvents, pgSchema,
|
|
690
|
+
return await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, Primitive_option.some(escapeTables$1));
|
|
669
691
|
}
|
|
670
692
|
throw exn$1;
|
|
671
693
|
}
|
|
@@ -1020,7 +1042,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1020
1042
|
sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
|
|
1021
1043
|
sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
|
|
1022
1044
|
]);
|
|
1023
|
-
let writeBatchMethod = async (batch, rawEvents,
|
|
1045
|
+
let writeBatchMethod = async (batch, rawEvents, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities) => {
|
|
1024
1046
|
let pgUpdates = [];
|
|
1025
1047
|
let chUpdates = [];
|
|
1026
1048
|
for (let i = 0, i_finish = updatedEntities.length; i < i_finish; ++i) {
|
|
@@ -1043,7 +1065,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1043
1065
|
sinkPromise = undefined;
|
|
1044
1066
|
}
|
|
1045
1067
|
let primaryTimerRef = Hrtime.makeTimer();
|
|
1046
|
-
await writeBatch(sql, batch, rawEvents, pgSchema,
|
|
1068
|
+
await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, undefined);
|
|
1047
1069
|
return Prometheus.StorageWrite.increment("postgres", Hrtime.toSecondsFloat(Hrtime.timeSince(primaryTimerRef)));
|
|
1048
1070
|
};
|
|
1049
1071
|
let close = () => sql.end();
|
|
@@ -1103,7 +1125,11 @@ function makeStorageFromEnv(config, sqlOpt, pgSchemaOpt, isHasuraEnabledOpt) {
|
|
|
1103
1125
|
}, pgSchema, Config.getPgUserEntities(config), Env.Hasura.aggregateEntities, Env.Hasura.responseLimit, Schema.make(Belt_Array.map(config.allEntities, e => e.table))), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking tables`))) : undefined, isHasuraEnabled ? tableNames => Stdlib_Promise.$$catch(Hasura.trackTables(Env.Hasura.graphqlEndpoint, {
|
|
1104
1126
|
role: Env.Hasura.role,
|
|
1105
1127
|
secret: Env.Hasura.secret
|
|
1106
|
-
}, pgSchema, tableNames
|
|
1128
|
+
}, pgSchema, tableNames.map(tableName => ({
|
|
1129
|
+
tableName: tableName,
|
|
1130
|
+
description: undefined,
|
|
1131
|
+
columnDescriptions: {}
|
|
1132
|
+
}))), err => Promise.resolve(Logging.errorWithExn(Utils.prettifyExn(err), `Error tracking new tables`))) : undefined);
|
|
1107
1133
|
}
|
|
1108
1134
|
|
|
1109
1135
|
function makePersistenceFromConfig(config, storageOpt) {
|
package/src/ReorgDetection.res
CHANGED
|
@@ -12,11 +12,6 @@ type blockData = {
|
|
|
12
12
|
|
|
13
13
|
external generalizeBlockDataWithTimestamp: blockDataWithTimestamp => blockData = "%identity"
|
|
14
14
|
|
|
15
|
-
type reorgGuard = {
|
|
16
|
-
rangeLastBlock: blockData,
|
|
17
|
-
prevRangeLastBlock: option<blockData>,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
type reorgDetected = {
|
|
21
16
|
scannedBlock: blockData,
|
|
22
17
|
receivedBlock: blockData,
|
|
@@ -94,75 +89,57 @@ let getDataByBlockNumberCopyInThreshold = ({dataByBlockNumber, maxReorgDepth}: t
|
|
|
94
89
|
copy
|
|
95
90
|
}
|
|
96
91
|
|
|
97
|
-
/** Registers
|
|
98
|
-
*
|
|
99
|
-
|
|
92
|
+
/** Registers observed (blockNumber, blockHash) pairs from a range fetch, prunes
|
|
93
|
+
* unneeded data, and returns the updated state.
|
|
94
|
+
*
|
|
95
|
+
* Iterates the provided block hashes, skips entries outside the reorg threshold,
|
|
96
|
+
* and compares each one against the previously scanned data. Returns on the first
|
|
97
|
+
* mismatch as `ReorgDetected`.
|
|
98
|
+
*
|
|
99
|
+
* Resets internal state if shouldRollbackOnReorg is false (detect-only mode).
|
|
100
|
+
*/
|
|
100
101
|
let registerReorgGuard = (
|
|
101
102
|
{maxReorgDepth, shouldRollbackOnReorg} as self: t,
|
|
102
|
-
~
|
|
103
|
+
~blockHashes: array<blockData>,
|
|
103
104
|
~knownHeight,
|
|
104
105
|
) => {
|
|
105
106
|
let dataByBlockNumberCopyInThreshold = self->getDataByBlockNumberCopyInThreshold(~knownHeight)
|
|
107
|
+
let thresholdBlockNumber = knownHeight - maxReorgDepth
|
|
106
108
|
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
| None => None
|
|
122
|
-
| Some(prevRangeLastBlock) =>
|
|
123
|
-
switch dataByBlockNumberCopyInThreshold->Utils.Dict.dangerouslyGetNonOption(
|
|
124
|
-
prevRangeLastBlock.blockNumber->Int.toString,
|
|
125
|
-
) {
|
|
126
|
-
| Some(scannedBlock) if scannedBlock.blockHash !== prevRangeLastBlock.blockHash =>
|
|
127
|
-
Some({
|
|
128
|
-
receivedBlock: prevRangeLastBlock,
|
|
129
|
-
scannedBlock,
|
|
130
|
-
})
|
|
131
|
-
| _ => None
|
|
109
|
+
let maybeReorgDetected = ref(None)
|
|
110
|
+
let idx = ref(0)
|
|
111
|
+
while maybeReorgDetected.contents === None && idx.contents < blockHashes->Array.length {
|
|
112
|
+
let receivedBlock = blockHashes->Array.getUnsafe(idx.contents)
|
|
113
|
+
if receivedBlock.blockNumber >= thresholdBlockNumber {
|
|
114
|
+
let key = receivedBlock.blockNumber->Int.toString
|
|
115
|
+
// The working copy contains both previously scanned blocks AND blocks
|
|
116
|
+
// already written by earlier iterations of this same call, so a duplicate
|
|
117
|
+
// block number with a mismatching hash inside `blockHashes` itself is
|
|
118
|
+
// flagged as a reorg.
|
|
119
|
+
switch dataByBlockNumberCopyInThreshold->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
120
|
+
| Some(scannedBlock) if scannedBlock.blockHash !== receivedBlock.blockHash =>
|
|
121
|
+
maybeReorgDetected := Some({receivedBlock, scannedBlock})
|
|
122
|
+
| _ => dataByBlockNumberCopyInThreshold->Dict.set(key, receivedBlock)
|
|
132
123
|
}
|
|
133
124
|
}
|
|
125
|
+
idx := idx.contents + 1
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
switch maybeReorgDetected {
|
|
128
|
+
switch maybeReorgDetected.contents {
|
|
137
129
|
| Some(reorgDetected) => (
|
|
138
130
|
shouldRollbackOnReorg
|
|
139
131
|
? self
|
|
140
132
|
: make(~chainReorgCheckpoints=[], ~maxReorgDepth, ~shouldRollbackOnReorg),
|
|
141
133
|
ReorgDetected(reorgDetected),
|
|
142
134
|
)
|
|
143
|
-
| None =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
dataByBlockNumberCopyInThreshold->Dict.set(
|
|
152
|
-
prevRangeLastBlock.blockNumber->Int.toString,
|
|
153
|
-
prevRangeLastBlock,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
(
|
|
158
|
-
{
|
|
159
|
-
maxReorgDepth,
|
|
160
|
-
dataByBlockNumber: dataByBlockNumberCopyInThreshold,
|
|
161
|
-
shouldRollbackOnReorg,
|
|
162
|
-
},
|
|
163
|
-
NoReorg,
|
|
164
|
-
)
|
|
165
|
-
}
|
|
135
|
+
| None => (
|
|
136
|
+
{
|
|
137
|
+
maxReorgDepth,
|
|
138
|
+
dataByBlockNumber: dataByBlockNumberCopyInThreshold,
|
|
139
|
+
shouldRollbackOnReorg,
|
|
140
|
+
},
|
|
141
|
+
NoReorg,
|
|
142
|
+
)
|
|
166
143
|
}
|
|
167
144
|
}
|
|
168
145
|
|
|
@@ -46,47 +46,39 @@ function getDataByBlockNumberCopyInThreshold(param, knownHeight) {
|
|
|
46
46
|
return copy;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function registerReorgGuard(self,
|
|
49
|
+
function registerReorgGuard(self, blockHashes, knownHeight) {
|
|
50
50
|
let maxReorgDepth = self.maxReorgDepth;
|
|
51
51
|
let shouldRollbackOnReorg = self.shouldRollbackOnReorg;
|
|
52
52
|
let dataByBlockNumberCopyInThreshold = getDataByBlockNumberCopyInThreshold(self, knownHeight);
|
|
53
|
-
let
|
|
54
|
-
let rangeLastBlock = reorgGuard.rangeLastBlock;
|
|
55
|
-
let scannedBlock = dataByBlockNumberCopyInThreshold[rangeLastBlock.blockNumber.toString()];
|
|
53
|
+
let thresholdBlockNumber = knownHeight - maxReorgDepth | 0;
|
|
56
54
|
let maybeReorgDetected;
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
receivedBlock
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
receivedBlock: prevRangeLastBlock
|
|
72
|
-
}) : undefined;
|
|
73
|
-
} else {
|
|
74
|
-
maybeReorgDetected = undefined;
|
|
55
|
+
let idx = 0;
|
|
56
|
+
while (maybeReorgDetected === undefined && idx < blockHashes.length) {
|
|
57
|
+
let receivedBlock = blockHashes[idx];
|
|
58
|
+
if (receivedBlock.blockNumber >= thresholdBlockNumber) {
|
|
59
|
+
let key = receivedBlock.blockNumber.toString();
|
|
60
|
+
let scannedBlock = dataByBlockNumberCopyInThreshold[key];
|
|
61
|
+
if (scannedBlock !== undefined && scannedBlock.blockHash !== receivedBlock.blockHash) {
|
|
62
|
+
maybeReorgDetected = {
|
|
63
|
+
scannedBlock: scannedBlock,
|
|
64
|
+
receivedBlock: receivedBlock
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
dataByBlockNumberCopyInThreshold[key] = receivedBlock;
|
|
68
|
+
}
|
|
75
69
|
}
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
idx = idx + 1 | 0;
|
|
71
|
+
};
|
|
72
|
+
let reorgDetected = maybeReorgDetected;
|
|
73
|
+
if (reorgDetected !== undefined) {
|
|
78
74
|
return [
|
|
79
75
|
shouldRollbackOnReorg ? self : make([], maxReorgDepth, shouldRollbackOnReorg),
|
|
80
76
|
{
|
|
81
77
|
TAG: "ReorgDetected",
|
|
82
|
-
_0:
|
|
78
|
+
_0: reorgDetected
|
|
83
79
|
}
|
|
84
80
|
];
|
|
85
81
|
} else {
|
|
86
|
-
dataByBlockNumberCopyInThreshold[rangeLastBlock.blockNumber.toString()] = rangeLastBlock;
|
|
87
|
-
if (prevRangeLastBlock !== undefined) {
|
|
88
|
-
dataByBlockNumberCopyInThreshold[prevRangeLastBlock.blockNumber.toString()] = prevRangeLastBlock;
|
|
89
|
-
}
|
|
90
82
|
return [
|
|
91
83
|
{
|
|
92
84
|
shouldRollbackOnReorg: shouldRollbackOnReorg,
|
|
@@ -281,9 +281,27 @@ function patchConfig(config, processConfig) {
|
|
|
281
281
|
newrecord.startBlock = startBlock;
|
|
282
282
|
return newrecord;
|
|
283
283
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
return {
|
|
285
|
+
name: config.name,
|
|
286
|
+
description: config.description,
|
|
287
|
+
handlers: config.handlers,
|
|
288
|
+
contractHandlers: config.contractHandlers,
|
|
289
|
+
shouldRollbackOnReorg: config.shouldRollbackOnReorg,
|
|
290
|
+
shouldSaveFullHistory: config.shouldSaveFullHistory,
|
|
291
|
+
storage: config.storage,
|
|
292
|
+
chainMap: newChainMap,
|
|
293
|
+
defaultChain: config.defaultChain,
|
|
294
|
+
ecosystem: config.ecosystem,
|
|
295
|
+
enableRawEvents: config.enableRawEvents,
|
|
296
|
+
maxAddrInPartition: config.maxAddrInPartition,
|
|
297
|
+
batchSize: config.batchSize,
|
|
298
|
+
lowercaseAddresses: config.lowercaseAddresses,
|
|
299
|
+
isDev: config.isDev,
|
|
300
|
+
userEntitiesByName: config.userEntitiesByName,
|
|
301
|
+
userEntities: config.userEntities,
|
|
302
|
+
allEntities: config.allEntities,
|
|
303
|
+
allEnums: config.allEnums
|
|
304
|
+
};
|
|
287
305
|
}
|
|
288
306
|
|
|
289
307
|
export {
|
package/src/Sink.res
CHANGED
|
@@ -34,8 +34,8 @@ let makeClickHouse = (~host, ~database, ~username, ~password): t => {
|
|
|
34
34
|
},
|
|
35
35
|
writeBatch: async (~batch, ~updatedEntities) => {
|
|
36
36
|
await Promise.all(
|
|
37
|
-
updatedEntities->Belt.Array.map(({entityConfig,
|
|
38
|
-
ClickHouse.setUpdatesOrThrow(client, ~cache, ~
|
|
37
|
+
updatedEntities->Belt.Array.map(({entityConfig, changes}) => {
|
|
38
|
+
ClickHouse.setUpdatesOrThrow(client, ~cache, ~changes, ~entityConfig, ~database)
|
|
39
39
|
}),
|
|
40
40
|
)->Utils.Promise.ignoreValue
|
|
41
41
|
await ClickHouse.setCheckpointsOrThrow(client, ~batch, ~database)
|
package/src/Sink.res.mjs
CHANGED
|
@@ -21,7 +21,7 @@ function makeClickHouse(host, database, username, password) {
|
|
|
21
21
|
},
|
|
22
22
|
resume: checkpointId => ClickHouse.resume(client, database, checkpointId),
|
|
23
23
|
writeBatch: async (batch, updatedEntities) => {
|
|
24
|
-
await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.
|
|
24
|
+
await Promise.all(Belt_Array.map(updatedEntities, param => ClickHouse.setUpdatesOrThrow(client, cache, param.changes, param.entityConfig, database)));
|
|
25
25
|
return await ClickHouse.setCheckpointsOrThrow(client, batch, database);
|
|
26
26
|
}
|
|
27
27
|
};
|
package/src/TableIndices.res
CHANGED
|
@@ -69,9 +69,14 @@ module SingleIndex = {
|
|
|
69
69
|
operator,
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Lookups against the in-memory index table reconstruct this key from raw
|
|
73
|
+
// (fieldName, operator, serialized value) parts, so this is the single
|
|
74
|
+
// source of truth for the key format.
|
|
75
|
+
let toStringByParts = (~fieldName, ~operator: Operator.t, ~fieldValueHash) =>
|
|
76
|
+
`${fieldName}:${(operator :> string)}:${fieldValueHash}`
|
|
77
|
+
|
|
73
78
|
let toString = ({fieldName, fieldValue, operator}) =>
|
|
74
|
-
|
|
79
|
+
toStringByParts(~fieldName, ~operator, ~fieldValueHash=fieldValue->FieldValue.toString)
|
|
75
80
|
|
|
76
81
|
let evaluate = (self: t, ~fieldName, ~fieldValue) =>
|
|
77
82
|
self.fieldName === fieldName &&
|
|
@@ -101,6 +106,8 @@ module Index = {
|
|
|
101
106
|
| Single(index) => index->SingleIndex.toString
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
let toStringByParts = SingleIndex.toStringByParts
|
|
110
|
+
|
|
104
111
|
let evaluate = (index: t, ~fieldName, ~fieldValue) =>
|
|
105
112
|
switch index {
|
|
106
113
|
| Single(index) => SingleIndex.evaluate(index, ~fieldName, ~fieldValue)
|
package/src/TableIndices.res.mjs
CHANGED
|
@@ -81,8 +81,12 @@ function make(fieldName, fieldValue, operator) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function toStringByParts(fieldName, operator, fieldValueHash) {
|
|
85
|
+
return fieldName + `:` + operator + `:` + fieldValueHash;
|
|
86
|
+
}
|
|
87
|
+
|
|
84
88
|
function toString$2(param) {
|
|
85
|
-
return param.fieldName
|
|
89
|
+
return toStringByParts(param.fieldName, param.operator, toString$1(param.fieldValue));
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
function evaluate(self, fieldName, fieldValue) {
|
|
@@ -102,6 +106,7 @@ function evaluate(self, fieldName, fieldValue) {
|
|
|
102
106
|
|
|
103
107
|
let SingleIndex = {
|
|
104
108
|
make: make,
|
|
109
|
+
toStringByParts: toStringByParts,
|
|
105
110
|
toString: toString$2,
|
|
106
111
|
evaluate: evaluate
|
|
107
112
|
};
|
|
@@ -126,6 +131,7 @@ let Index = {
|
|
|
126
131
|
makeSingle: makeSingle,
|
|
127
132
|
getFieldName: getFieldName,
|
|
128
133
|
toString: toString$3,
|
|
134
|
+
toStringByParts: toStringByParts,
|
|
129
135
|
evaluate: evaluate$1
|
|
130
136
|
};
|
|
131
137
|
|
package/src/TestIndexer.res
CHANGED
|
@@ -122,7 +122,7 @@ let handleWriteBatch = (
|
|
|
122
122
|
// checkpointId -> entityName -> entityChange
|
|
123
123
|
let changesByCheckpoint: dict<dict<entityChange>> = Dict.make()
|
|
124
124
|
|
|
125
|
-
updatedEntities->Array.forEach(({entityName,
|
|
125
|
+
updatedEntities->Array.forEach(({entityName, changes}) => {
|
|
126
126
|
let entityDict = switch state.entities->Dict.get(entityName) {
|
|
127
127
|
| Some(dict) => dict
|
|
128
128
|
| None =>
|
|
@@ -132,68 +132,61 @@ let handleWriteBatch = (
|
|
|
132
132
|
}
|
|
133
133
|
let entityConfig = state.entityConfigs->Dict.getUnsafe(entityName)
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
176
|
-
changes
|
|
177
|
-
}
|
|
178
|
-
let entityChange = switch entityChanges->Dict.get(entityName) {
|
|
179
|
-
| Some(change) => change
|
|
180
|
-
| None =>
|
|
181
|
-
let change = {sets: [], deleted: []}
|
|
182
|
-
entityChanges->Dict.set(entityName, change)
|
|
183
|
-
change
|
|
184
|
-
}
|
|
185
|
-
entityChange.deleted->Array.push(entityId)->ignore
|
|
135
|
+
let processChange = (change: TestIndexerProxyStorage.serializableChange) => {
|
|
136
|
+
switch change {
|
|
137
|
+
| Set({entityId, entity, checkpointId}) =>
|
|
138
|
+
// Parse entity immediately to store decoded values for proper comparisons
|
|
139
|
+
// (bigint/BigDecimal need actual values, not JSON strings)
|
|
140
|
+
let parsedEntity = entity->S.parseOrThrow(entityConfig.schema)
|
|
141
|
+
|
|
142
|
+
// Update entities dict with parsed entity for load operations
|
|
143
|
+
entityDict->Dict.set(entityId, parsedEntity)
|
|
144
|
+
|
|
145
|
+
// Track change by checkpoint
|
|
146
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
147
|
+
let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
|
|
148
|
+
| Some(changes) => changes
|
|
149
|
+
| None =>
|
|
150
|
+
let changes = Dict.make()
|
|
151
|
+
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
152
|
+
changes
|
|
153
|
+
}
|
|
154
|
+
let entityChange = switch entityChanges->Dict.get(entityName) {
|
|
155
|
+
| Some(change) => change
|
|
156
|
+
| None =>
|
|
157
|
+
let change = {sets: [], deleted: []}
|
|
158
|
+
entityChanges->Dict.set(entityName, change)
|
|
159
|
+
change
|
|
160
|
+
}
|
|
161
|
+
entityChange.sets->Array.push(parsedEntity->Utils.magic)->ignore
|
|
162
|
+
|
|
163
|
+
| Delete({entityId, checkpointId}) =>
|
|
164
|
+
// Update entities dict for load operations
|
|
165
|
+
Dict.delete(entityDict->Obj.magic, entityId)
|
|
166
|
+
|
|
167
|
+
// Track change by checkpoint
|
|
168
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
169
|
+
let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
|
|
170
|
+
| Some(changes) => changes
|
|
171
|
+
| None =>
|
|
172
|
+
let changes = Dict.make()
|
|
173
|
+
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
174
|
+
changes
|
|
186
175
|
}
|
|
176
|
+
let entityChange = switch entityChanges->Dict.get(entityName) {
|
|
177
|
+
| Some(change) => change
|
|
178
|
+
| None =>
|
|
179
|
+
let change = {sets: [], deleted: []}
|
|
180
|
+
entityChanges->Dict.set(entityName, change)
|
|
181
|
+
change
|
|
182
|
+
}
|
|
183
|
+
entityChange.deleted->Array.push(entityId)->ignore
|
|
187
184
|
}
|
|
185
|
+
}
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Also include latestChange if history is empty (fallback for backwards compatibility)
|
|
193
|
-
if update.history->Array.length === 0 {
|
|
194
|
-
processChange(update.latestChange)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
187
|
+
// Every change carries its own checkpointId and each (id, checkpointId)
|
|
188
|
+
// appears at most once in the batch, so record them all into their buckets.
|
|
189
|
+
changes->Array.forEach(processChange)
|
|
197
190
|
})
|
|
198
191
|
|
|
199
192
|
// Build combined checkpoint + entity changes objects
|