envio 3.5.0-alpha.0 → 3.5.0-alpha.2
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/index.d.ts +10 -6
- package/package.json +6 -6
- package/src/BatchProcessing.res +13 -0
- package/src/BatchProcessing.res.mjs +4 -0
- package/src/Bin.res +2 -2
- package/src/ChainFetching.res +5 -5
- package/src/ChainFetching.res.mjs +3 -4
- package/src/ChainState.res +44 -36
- package/src/ChainState.res.mjs +36 -31
- package/src/ChainState.resi +2 -12
- package/src/Change.res +3 -3
- package/src/Config.res +4 -4
- package/src/Config.res.mjs +1 -10
- package/src/Core.res +4 -35
- package/src/Core.res.mjs +1 -31
- package/src/CrossChainState.res +2 -20
- package/src/CrossChainState.res.mjs +2 -6
- package/src/EntityId.res +15 -0
- package/src/EntityId.res.mjs +9 -0
- package/src/Env.res +16 -0
- package/src/Env.res.mjs +6 -0
- package/src/EventConfigBuilder.res +1 -58
- package/src/EventConfigBuilder.res.mjs +1 -33
- package/src/FetchState.res +663 -484
- package/src/FetchState.res.mjs +491 -440
- package/src/HandlerRegister.res +45 -0
- package/src/HandlerRegister.res.mjs +43 -0
- package/src/InMemoryStore.res +11 -4
- package/src/InMemoryTable.res +12 -10
- package/src/InMemoryTable.res.mjs +6 -5
- package/src/IndexerState.res +59 -2
- package/src/IndexerState.res.mjs +48 -3
- package/src/IndexerState.resi +2 -0
- package/src/Internal.res +17 -8
- package/src/LogSelection.res +2 -1
- package/src/Metrics.res +37 -6
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +1 -1
- package/src/PgStorage.res +55 -22
- package/src/PgStorage.res.mjs +25 -17
- package/src/SimulateItems.res +2 -2
- package/src/TestIndexer.res +6 -5
- package/src/TestIndexer.res.mjs +3 -2
- package/src/UserContext.res +3 -3
- package/src/Writing.res +12 -2
- package/src/Writing.res.mjs +13 -2
- package/src/bindings/ClickHouse.res +5 -3
- package/src/bindings/ClickHouse.res.mjs +2 -5
- package/src/bindings/Vitest.res +22 -1
- package/src/bindings/Vitest.res.mjs +15 -0
- package/src/db/EntityHistory.res +16 -7
- package/src/db/EntityHistory.res.mjs +7 -6
- package/src/db/Table.res +40 -9
- package/src/db/Table.res.mjs +44 -6
- package/src/sources/AddressSet.res +48 -0
- package/src/sources/AddressSet.res.mjs +11 -0
- package/src/sources/AddressStore.res +152 -0
- package/src/sources/AddressStore.res.mjs +97 -0
- package/src/sources/EvmChain.res +3 -0
- package/src/sources/EvmChain.res.mjs +4 -2
- package/src/sources/EvmHyperSyncSource.res +9 -9
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcClient.res +10 -3
- package/src/sources/EvmRpcClient.res.mjs +3 -3
- package/src/sources/FuelHyperSync.res +4 -3
- package/src/sources/FuelHyperSync.res.mjs +3 -3
- package/src/sources/FuelHyperSync.resi +2 -1
- package/src/sources/FuelHyperSyncClient.res +12 -6
- package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
- package/src/sources/FuelHyperSyncSource.res +7 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
- package/src/sources/HyperSync.res +9 -5
- package/src/sources/HyperSync.res.mjs +3 -3
- package/src/sources/HyperSync.resi +3 -2
- package/src/sources/HyperSyncClient.res +24 -10
- package/src/sources/HyperSyncClient.res.mjs +4 -4
- package/src/sources/RpcSource.res +71 -67
- package/src/sources/RpcSource.res.mjs +4 -4
- package/src/sources/SimulateSource.res +26 -11
- package/src/sources/SimulateSource.res.mjs +13 -5
- package/src/sources/Source.res +8 -4
- package/src/sources/SourceManager.res +2 -3
- package/src/sources/SourceManager.res.mjs +2 -3
- package/src/sources/Svm.res +1 -2
- package/src/sources/Svm.res.mjs +1 -1
- package/src/sources/SvmHyperSyncClient.res +14 -4
- package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
- package/src/sources/SvmHyperSyncSource.res +11 -6
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/src/IndexingAddresses.res +0 -108
- package/src/IndexingAddresses.res.mjs +0 -101
- package/src/IndexingAddresses.resi +0 -34
package/src/bindings/Vitest.res
CHANGED
|
@@ -34,7 +34,6 @@ type rec expectation<'a> = {
|
|
|
34
34
|
toHavePropertyValue: 'b. (string, 'b) => unit,
|
|
35
35
|
// Exception matchers
|
|
36
36
|
toThrow: unit => unit,
|
|
37
|
-
toThrowError: string => unit,
|
|
38
37
|
// Snapshot matchers
|
|
39
38
|
toMatchSnapshot: unit => unit,
|
|
40
39
|
// Negation
|
|
@@ -151,3 +150,25 @@ module Async = {
|
|
|
151
150
|
|
|
152
151
|
@module("vitest")
|
|
153
152
|
external expect: ('a, ~message: string=?) => expectation<'a> = "expect"
|
|
153
|
+
|
|
154
|
+
// Runs `fn` and returns the thrown error's message — mirroring JS's
|
|
155
|
+
// `e instanceof Error ? e.message : String(e)` — or `None` when nothing threw.
|
|
156
|
+
let messageOfThrown: (unit => 'a) => option<string> = %raw(`function (fn) {
|
|
157
|
+
try {
|
|
158
|
+
fn();
|
|
159
|
+
return undefined;
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return e instanceof Error ? e.message : String(e);
|
|
162
|
+
}
|
|
163
|
+
}`)
|
|
164
|
+
|
|
165
|
+
// Strict counterpart to the built-in `toThrow`, which only checks that the
|
|
166
|
+
// thrown message *contains* the expected string. `toThrowErrorEqual` requires
|
|
167
|
+
// the whole message to match, so a test can pin the complete error text.
|
|
168
|
+
// A plain ReScript function (not a custom `expect.extend` matcher), so it needs
|
|
169
|
+
// no per-package vitest setup.
|
|
170
|
+
// Compared as options rather than through a "didn't throw" placeholder string,
|
|
171
|
+
// so a function that throws nothing can never match — not even when `expected`
|
|
172
|
+
// happens to equal the placeholder.
|
|
173
|
+
let toThrowErrorEqual = (t: testContext, fn: unit => 'a, ~message=?, expected: string) =>
|
|
174
|
+
t.expect(fn->messageOfThrown, ~message?).toEqual(Some(expected))
|
|
@@ -13,7 +13,22 @@ let Async = {
|
|
|
13
13
|
itSkipInClaudeCloud: itSkipInClaudeCloud
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
let messageOfThrown = (function (fn) {
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
return undefined;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return e instanceof Error ? e.message : String(e);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function toThrowErrorEqual(t, fn, message, expected) {
|
|
26
|
+
t.expect(messageOfThrown(fn), message).toEqual(expected);
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export {
|
|
17
30
|
Async,
|
|
31
|
+
messageOfThrown,
|
|
32
|
+
toThrowErrorEqual,
|
|
18
33
|
}
|
|
19
34
|
/* isClaudeCloud Not a pure module */
|
package/src/db/EntityHistory.res
CHANGED
|
@@ -30,12 +30,14 @@ let unsafeCheckpointIdSchema =
|
|
|
30
30
|
serializer: bigint => bigint->BigInt.toString,
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
let makeSetUpdateSchema: S.t<
|
|
33
|
+
let makeSetUpdateSchema = (~idSchema: S.t<EntityId.t>, entitySchema: S.t<'entity>): S.t<
|
|
34
|
+
Change.t<'entity>,
|
|
35
|
+
> => {
|
|
34
36
|
S.object(s => {
|
|
35
37
|
s.tag(changeFieldName, RowAction.SET)
|
|
36
38
|
Change.Set({
|
|
37
39
|
checkpointId: s.field(checkpointIdFieldName, unsafeCheckpointIdSchema),
|
|
38
|
-
entityId: s.field(Table.idFieldName,
|
|
40
|
+
entityId: s.field(Table.idFieldName, idSchema),
|
|
39
41
|
entity: s.flatten(entitySchema),
|
|
40
42
|
})
|
|
41
43
|
})
|
|
@@ -118,10 +120,10 @@ let pruneStaleEntityHistory = (
|
|
|
118
120
|
|
|
119
121
|
// If an entity doesn't have a history before the update
|
|
120
122
|
// we create it automatically with envio_checkpoint_id 0
|
|
121
|
-
let makeBackfillHistoryQuery = (~pgSchema, ~entityName, ~entityIndex) => {
|
|
123
|
+
let makeBackfillHistoryQuery = (~pgSchema, ~entityName, ~entityIndex, ~idPgType) => {
|
|
122
124
|
let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName, ~entityIndex)}"`
|
|
123
125
|
`WITH target_ids AS (
|
|
124
|
-
SELECT UNNEST($1::${
|
|
126
|
+
SELECT UNNEST($1::${idPgType}[]) AS id
|
|
125
127
|
),
|
|
126
128
|
missing_history AS (
|
|
127
129
|
SELECT e.*
|
|
@@ -135,11 +137,18 @@ SELECT *, 0 AS ${checkpointIdFieldName}, '${(RowAction.SET :> string)}' as ${cha
|
|
|
135
137
|
FROM missing_history;`
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
let backfillHistory = (
|
|
140
|
+
let backfillHistory = (
|
|
141
|
+
sql,
|
|
142
|
+
~pgSchema,
|
|
143
|
+
~table: Table.table,
|
|
144
|
+
~entityIndex,
|
|
145
|
+
~ids: array<EntityId.t>,
|
|
146
|
+
) => {
|
|
147
|
+
let idPgType = table->Table.getIdPgFieldType(~pgSchema)
|
|
139
148
|
sql
|
|
140
149
|
->Postgres.preparedUnsafe(
|
|
141
|
-
makeBackfillHistoryQuery(~entityName, ~entityIndex, ~pgSchema),
|
|
142
|
-
[ids]->
|
|
150
|
+
makeBackfillHistoryQuery(~entityName=table.tableName, ~entityIndex, ~pgSchema, ~idPgType),
|
|
151
|
+
[table->Table.encodeIdsToJson(ids)]->(Utils.magic: array<JSON.t> => unknown),
|
|
143
152
|
)
|
|
144
153
|
->Utils.Promise.ignoreValue
|
|
145
154
|
}
|
|
@@ -48,12 +48,12 @@ let unsafeCheckpointIdSchema = S$RescriptSchema.transform(S$RescriptSchema.setNa
|
|
|
48
48
|
s: bigint => bigint.toString()
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
|
-
function makeSetUpdateSchema(entitySchema) {
|
|
51
|
+
function makeSetUpdateSchema(idSchema, entitySchema) {
|
|
52
52
|
return S$RescriptSchema.object(s => {
|
|
53
53
|
s.tag(changeFieldName, "SET");
|
|
54
54
|
return {
|
|
55
55
|
type: "SET",
|
|
56
|
-
entityId: s.f(Table.idFieldName,
|
|
56
|
+
entityId: s.f(Table.idFieldName, idSchema),
|
|
57
57
|
entity: s.flatten(entitySchema),
|
|
58
58
|
checkpointId: s.f(checkpointIdFieldName, unsafeCheckpointIdSchema)
|
|
59
59
|
};
|
|
@@ -97,10 +97,10 @@ function pruneStaleEntityHistory(sql, entityName, entityIndex, pgSchema, safeChe
|
|
|
97
97
|
return sql.unsafe(makePruneStaleEntityHistoryQuery(entityName, entityIndex, pgSchema), [safeCheckpointId.toString()], {prepare: true});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function makeBackfillHistoryQuery(pgSchema, entityName, entityIndex) {
|
|
100
|
+
function makeBackfillHistoryQuery(pgSchema, entityName, entityIndex, idPgType) {
|
|
101
101
|
let historyTableRef = `"` + pgSchema + `"."` + historyTableName(entityName, entityIndex) + `"`;
|
|
102
102
|
return `WITH target_ids AS (
|
|
103
|
-
SELECT UNNEST($1::` +
|
|
103
|
+
SELECT UNNEST($1::` + idPgType + `[]) AS id
|
|
104
104
|
),
|
|
105
105
|
missing_history AS (
|
|
106
106
|
SELECT e.*
|
|
@@ -114,8 +114,9 @@ SELECT *, 0 AS ` + checkpointIdFieldName + `, '` + "SET" + `' as ` + changeField
|
|
|
114
114
|
FROM missing_history;`;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function backfillHistory(sql, pgSchema,
|
|
118
|
-
|
|
117
|
+
function backfillHistory(sql, pgSchema, table, entityIndex, ids) {
|
|
118
|
+
let idPgType = Table.getIdPgFieldType(table, pgSchema);
|
|
119
|
+
return sql.unsafe(makeBackfillHistoryQuery(pgSchema, table.tableName, entityIndex, idPgType), [Table.encodeIdsToJson(table, ids)], {prepare: true});
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function rollback(sql, pgSchema, entityName, entityIndex, rollbackTargetCheckpointId) {
|
package/src/db/Table.res
CHANGED
|
@@ -31,7 +31,6 @@ type fieldType =
|
|
|
31
31
|
| Json
|
|
32
32
|
| Date
|
|
33
33
|
| Enum({config: enumConfig<enum>})
|
|
34
|
-
| Entity({name: string})
|
|
35
34
|
|
|
36
35
|
type field = {
|
|
37
36
|
fieldName: string,
|
|
@@ -168,7 +167,6 @@ let getPgFieldType = (
|
|
|
168
167
|
| Date =>
|
|
169
168
|
(isNullable ? Postgres.TimestampWithTimezoneNull : Postgres.TimestampWithTimezone :> string)
|
|
170
169
|
| Enum({config}) => `"${pgSchema}".${config.name}`
|
|
171
|
-
| Entity(_) => (Postgres.Text :> string) // FIXME: Will it work correctly if id is not a text column?
|
|
172
170
|
}
|
|
173
171
|
|
|
174
172
|
// Workaround for Hasura bug https://github.com/enviodev/hyperindex/issues/788
|
|
@@ -248,6 +246,40 @@ let getDerivedFromFields = table =>
|
|
|
248
246
|
let getFieldByName = (table, fieldName) =>
|
|
249
247
|
table.fields->Array.find(field => field->getUserDefinedFieldName === fieldName)
|
|
250
248
|
|
|
249
|
+
exception NoIdField(string)
|
|
250
|
+
|
|
251
|
+
// The `id` primary-key field. Its type drives both the id column and every
|
|
252
|
+
// foreign key that references the entity, so id-typed SQL (delete-by-id,
|
|
253
|
+
// history backfill) reads the column type and value schema from here.
|
|
254
|
+
let getIdFieldOrThrow = (table): field =>
|
|
255
|
+
switch table->getFieldByName(idFieldName) {
|
|
256
|
+
| Some(Field(field)) => field
|
|
257
|
+
| _ => throw(NoIdField(table.tableName))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let getIdPgFieldType = (table, ~pgSchema) =>
|
|
261
|
+
getPgFieldType(
|
|
262
|
+
~fieldType=(table->getIdFieldOrThrow).fieldType,
|
|
263
|
+
~pgSchema,
|
|
264
|
+
~isArray=false,
|
|
265
|
+
~isNumericArrayAsText=false,
|
|
266
|
+
~isNullable=false,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Schema for a single id value, typed opaquely so id-generic code can serialize
|
|
270
|
+
// ids regardless of the underlying scalar.
|
|
271
|
+
let getIdSchema = (table): S.t<EntityId.t> =>
|
|
272
|
+
(table->getIdFieldOrThrow).fieldSchema->(Utils.magic: S.t<unknown> => S.t<EntityId.t>)
|
|
273
|
+
|
|
274
|
+
// Serializes an array of ids to the JSON form the SQL layer binds. The array
|
|
275
|
+
// schema is memoized per table so its serializer compiles once, not on every
|
|
276
|
+
// (high-frequency) delete/history write.
|
|
277
|
+
let idsArraySchema: table => S.t<array<EntityId.t>> = Utils.WeakMap.memoize(table =>
|
|
278
|
+
S.array(table->getIdSchema)
|
|
279
|
+
)
|
|
280
|
+
let encodeIdsToJson = (table, ids: array<EntityId.t>): JSON.t =>
|
|
281
|
+
ids->S.reverseConvertToJsonOrThrow(table->idsArraySchema)
|
|
282
|
+
|
|
251
283
|
// TODO: Test whether it should be passed via args and match the column type
|
|
252
284
|
|
|
253
285
|
let getFieldByApiName = (table, apiFieldName) =>
|
|
@@ -297,13 +329,12 @@ let makeRowsSchema = (table, ~rowFieldName) =>
|
|
|
297
329
|
S.array(
|
|
298
330
|
S.object(s => {
|
|
299
331
|
let dict = Dict.make()
|
|
300
|
-
table.fields->Array.forEach(
|
|
301
|
-
field
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
},
|
|
332
|
+
table.fields->Array.forEach(field =>
|
|
333
|
+
switch field {
|
|
334
|
+
| Field(field) =>
|
|
335
|
+
dict->Dict.set(field->getApiFieldName, s.field(field->rowFieldName, field.fieldSchema))
|
|
336
|
+
| DerivedFrom(_) => ()
|
|
337
|
+
}
|
|
307
338
|
)
|
|
308
339
|
dict
|
|
309
340
|
})->(Utils.magic: S.t<dict<unknown>> => S.t<unknown>),
|
package/src/db/Table.res.mjs
CHANGED
|
@@ -94,6 +94,8 @@ function getPgFieldName(fieldOrDerived) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
let idFieldName = "id";
|
|
98
|
+
|
|
97
99
|
function getPgFieldType(fieldType, pgSchema, isArray, isNumericArrayAsText, isNullable) {
|
|
98
100
|
let columnType;
|
|
99
101
|
if (typeof fieldType !== "object") {
|
|
@@ -142,9 +144,6 @@ function getPgFieldType(fieldType, pgSchema, isArray, isNumericArrayAsText, isNu
|
|
|
142
144
|
case "Enum" :
|
|
143
145
|
columnType = `"` + pgSchema + `".` + fieldType.config.name;
|
|
144
146
|
break;
|
|
145
|
-
case "Entity" :
|
|
146
|
-
columnType = "TEXT";
|
|
147
|
-
break;
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
149
|
let isNumericAsText = isArray && isNumericArrayAsText;
|
|
@@ -226,6 +225,41 @@ function getFieldByName(table, fieldName) {
|
|
|
226
225
|
return table.fields.find(field => getUserDefinedFieldName(field) === fieldName);
|
|
227
226
|
}
|
|
228
227
|
|
|
228
|
+
let NoIdField = /* @__PURE__ */Primitive_exceptions.create("Table.NoIdField");
|
|
229
|
+
|
|
230
|
+
function getIdFieldOrThrow(table) {
|
|
231
|
+
let match = getFieldByName(table, idFieldName);
|
|
232
|
+
if (match !== undefined) {
|
|
233
|
+
if (match.TAG === "Field") {
|
|
234
|
+
return match._0;
|
|
235
|
+
}
|
|
236
|
+
throw {
|
|
237
|
+
RE_EXN_ID: NoIdField,
|
|
238
|
+
_1: table.tableName,
|
|
239
|
+
Error: new Error()
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
throw {
|
|
243
|
+
RE_EXN_ID: NoIdField,
|
|
244
|
+
_1: table.tableName,
|
|
245
|
+
Error: new Error()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function getIdPgFieldType(table, pgSchema) {
|
|
250
|
+
return getPgFieldType(getIdFieldOrThrow(table).fieldType, pgSchema, false, false, false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getIdSchema(table) {
|
|
254
|
+
return getIdFieldOrThrow(table).fieldSchema;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let idsArraySchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.array(getIdFieldOrThrow(table).fieldSchema));
|
|
258
|
+
|
|
259
|
+
function encodeIdsToJson(table, ids) {
|
|
260
|
+
return S$RescriptSchema.reverseConvertToJsonOrThrow(ids, idsArraySchema(table));
|
|
261
|
+
}
|
|
262
|
+
|
|
229
263
|
function getFieldByApiName(table, apiFieldName) {
|
|
230
264
|
return table.fields.find(field => {
|
|
231
265
|
let tmp;
|
|
@@ -408,8 +442,6 @@ function getCompositeIndices(table) {
|
|
|
408
442
|
return getUnfilteredCompositeIndicesUnsafe(table).filter(ind => ind.length > 1);
|
|
409
443
|
}
|
|
410
444
|
|
|
411
|
-
let idFieldName = "id";
|
|
412
|
-
|
|
413
445
|
export {
|
|
414
446
|
makeEnumConfig,
|
|
415
447
|
mkField,
|
|
@@ -429,6 +461,12 @@ export {
|
|
|
429
461
|
getLinkedEntityFields,
|
|
430
462
|
getDerivedFromFields,
|
|
431
463
|
getFieldByName,
|
|
464
|
+
NoIdField,
|
|
465
|
+
getIdFieldOrThrow,
|
|
466
|
+
getIdPgFieldType,
|
|
467
|
+
getIdSchema,
|
|
468
|
+
idsArraySchema,
|
|
469
|
+
encodeIdsToJson,
|
|
432
470
|
getFieldByApiName,
|
|
433
471
|
queryFields,
|
|
434
472
|
makeRowsSchema,
|
|
@@ -440,4 +478,4 @@ export {
|
|
|
440
478
|
getSingleIndices,
|
|
441
479
|
getCompositeIndices,
|
|
442
480
|
}
|
|
443
|
-
/*
|
|
481
|
+
/* idsArraySchema Not a pure module */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Binding to the Rust `AddressSet` napi class: an immutable, ordered snapshot
|
|
2
|
+
// of some of a chain's indexed addresses, handed to a fetch-state partition
|
|
3
|
+
// instead of a JS address array. Sets are ordered by
|
|
4
|
+
// `(effectiveStartBlock, address)`, so the same addresses always produce the
|
|
5
|
+
// same set however they were registered or restored. Everything a query derives
|
|
6
|
+
// from one (padded topics, the routing owner index, per-contract counts) is
|
|
7
|
+
// built once inside Rust and shared by every query the partition makes.
|
|
8
|
+
type t
|
|
9
|
+
|
|
10
|
+
type startBlockGroup = {startBlock: int, count: int}
|
|
11
|
+
|
|
12
|
+
@send external size: t => int = "size"
|
|
13
|
+
|
|
14
|
+
// Live addresses of one contract, derived once per set inside Rust — so the
|
|
15
|
+
// fetch state can size and split partitions without ever walking the addresses.
|
|
16
|
+
@send external countFor: (t, string) => int = "countFor"
|
|
17
|
+
|
|
18
|
+
// The chain-wide address gate, reachable through any set of the store. Not set
|
|
19
|
+
// membership: the simulate source has no native query boundary, so it applies
|
|
20
|
+
// the same gate every real source applies while routing.
|
|
21
|
+
@send external has: (t, Address.t, string, int) => bool = "has"
|
|
22
|
+
|
|
23
|
+
@send external contractNames: t => array<string> = "contractNames"
|
|
24
|
+
|
|
25
|
+
// Distinct effective start blocks in this set, ascending — how partition
|
|
26
|
+
// creation decides where one partition ends and the next begins.
|
|
27
|
+
@send external startBlockGroups: t => array<startBlockGroup> = "startBlockGroups"
|
|
28
|
+
|
|
29
|
+
@send external slice: (t, ~offset: int, ~limit: option<int>) => t = "slice"
|
|
30
|
+
|
|
31
|
+
// Keeps only the named contracts' addresses, in set order.
|
|
32
|
+
@send external filterByContracts: (t, array<string>) => t = "filterByContracts"
|
|
33
|
+
|
|
34
|
+
// Drops addresses registered after the target block, and any the store has
|
|
35
|
+
// tombstoned since this set was built.
|
|
36
|
+
@send external filterByRegistrationBlock: (t, int) => t = "filterByRegistrationBlock"
|
|
37
|
+
|
|
38
|
+
// Union with another set of the same store, keeping set order; duplicates
|
|
39
|
+
// collapse.
|
|
40
|
+
@send external merge: (t, t) => t = "merge"
|
|
41
|
+
|
|
42
|
+
// Canonical address strings in set order. Not on a query path — queries read
|
|
43
|
+
// the set's cached slices inside Rust.
|
|
44
|
+
@send external addresses: t => array<Address.t> = "addresses"
|
|
45
|
+
|
|
46
|
+
@send external entries: t => array<Internal.indexingContract> = "entries"
|
|
47
|
+
|
|
48
|
+
let isEmpty = (set: t) => set->size === 0
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Binding to the Rust `AddressStore` napi class: the chain-wide index of every
|
|
2
|
+
// address being indexed, config-declared or dynamically registered. It owns
|
|
3
|
+
// registration bookkeeping (conflict detection, `effectiveStartBlock`
|
|
4
|
+
// derivation, reorg rollback) and hands out `AddressSet` snapshots for the
|
|
5
|
+
// fetch state's partitions. One store lives per chain on `ChainState`, shared
|
|
6
|
+
// with that chain's source clients — which read it to drop items whose emitter
|
|
7
|
+
// (or address-valued param) isn't registered at the item's block.
|
|
8
|
+
type t
|
|
9
|
+
|
|
10
|
+
// A contract the chain has events for. An address registered for anything else
|
|
11
|
+
// gets the `NoEvents` verdict: persisted so a future config that adds events
|
|
12
|
+
// picks it up, but never fetched.
|
|
13
|
+
type contract = {name: string, startBlock: option<int>}
|
|
14
|
+
|
|
15
|
+
type registration = {
|
|
16
|
+
address: Address.t,
|
|
17
|
+
contractName: string,
|
|
18
|
+
// -1 for a config address (not dynamically registered).
|
|
19
|
+
registrationBlock: int,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type verdict =
|
|
23
|
+
| Added({effectiveStartBlock: int})
|
|
24
|
+
// Contract has no events; tracked and persisted, never fetched.
|
|
25
|
+
| NoEvents({effectiveStartBlock: int})
|
|
26
|
+
// Already registered for the same contract.
|
|
27
|
+
| Duplicate({effectiveStartBlock: int, existingEffectiveStartBlock: int})
|
|
28
|
+
// Already registered for a different contract.
|
|
29
|
+
| Conflict({existingContractName: string})
|
|
30
|
+
// Not a well-formed address for the ecosystem.
|
|
31
|
+
| Invalid
|
|
32
|
+
|
|
33
|
+
type rawVerdict = {
|
|
34
|
+
kind: string,
|
|
35
|
+
effectiveStartBlock: int,
|
|
36
|
+
existingContractName: Null.t<string>,
|
|
37
|
+
existingEffectiveStartBlock: Null.t<int>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type makeSetOptions = {
|
|
41
|
+
// Only addresses whose id is at or above this. Ids are handed out in
|
|
42
|
+
// registration order, so a cursor read before a batch (`nextId`) selects
|
|
43
|
+
// exactly what that batch added.
|
|
44
|
+
minId?: int,
|
|
45
|
+
// Inclusive effectiveStartBlock bounds.
|
|
46
|
+
fromStartBlock?: int,
|
|
47
|
+
toStartBlock?: int,
|
|
48
|
+
offset?: int,
|
|
49
|
+
limit?: int,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@send
|
|
53
|
+
external newEvm: (Core.addressStoreCtor, bool, array<contract>) => t = "newEvm"
|
|
54
|
+
@send external newSvm: (Core.addressStoreCtor, array<contract>) => t = "newSvm"
|
|
55
|
+
@send external newFuel: (Core.addressStoreCtor, array<contract>) => t = "newFuel"
|
|
56
|
+
|
|
57
|
+
// The store's ecosystem is fixed here, from the chain's config, and decides how
|
|
58
|
+
// address strings are parsed and rendered back. EVM carries the chain's
|
|
59
|
+
// address-checksumming setting so entries render exactly as the sources do.
|
|
60
|
+
let make = (~ecosystem: Ecosystem.name, ~shouldChecksum: bool, ~contracts: array<contract>): t => {
|
|
61
|
+
let ctor = Core.getAddon().addressStore
|
|
62
|
+
switch ecosystem {
|
|
63
|
+
| Evm => ctor->newEvm(shouldChecksum, contracts)
|
|
64
|
+
| Svm => ctor->newSvm(contracts)
|
|
65
|
+
| Fuel => ctor->newFuel(contracts)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The contracts of a chain that have events, with the earliest block any of
|
|
70
|
+
// their events may fire at.
|
|
71
|
+
let contractsOf = (~onEventRegistrations: array<Internal.onEventRegistration>): array<contract> => {
|
|
72
|
+
let startBlocks = Dict.make()
|
|
73
|
+
let names = []
|
|
74
|
+
onEventRegistrations->Array.forEach(reg => {
|
|
75
|
+
let name = reg.eventConfig.contractName
|
|
76
|
+
switch startBlocks->Utils.Dict.dangerouslyGetNonOption(name) {
|
|
77
|
+
| None =>
|
|
78
|
+
names->Array.push(name)->ignore
|
|
79
|
+
startBlocks->Dict.set(name, reg.startBlock)
|
|
80
|
+
| Some(existing) =>
|
|
81
|
+
startBlocks->Dict.set(
|
|
82
|
+
name,
|
|
83
|
+
switch (existing, reg.startBlock) {
|
|
84
|
+
| (Some(a), Some(b)) => Some(Pervasives.min(a, b))
|
|
85
|
+
| (Some(_) as s, None) | (None, Some(_) as s) => s
|
|
86
|
+
| (None, None) => None
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
names->Array.map(name => {name, startBlock: startBlocks->Dict.getUnsafe(name)})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@send external nextId: t => int = "nextId"
|
|
95
|
+
|
|
96
|
+
// A set holding nothing — what an address-free (wildcard) partition carries, so
|
|
97
|
+
// every partition is queried through the same handle.
|
|
98
|
+
@send external emptySet: t => AddressSet.t = "emptySet"
|
|
99
|
+
|
|
100
|
+
// A set over exactly these addresses, in set order. Every set of a chain must
|
|
101
|
+
// come from that chain's one store — ids are store-scoped, so sets from
|
|
102
|
+
// different stores can't be merged.
|
|
103
|
+
@send external makeSetOf: (t, array<Address.t>) => AddressSet.t = "makeSetOf"
|
|
104
|
+
@send external registerBatchRaw: (t, array<registration>) => array<rawVerdict> = "registerBatch"
|
|
105
|
+
@send external makeSetRaw: (t, string, makeSetOptions) => AddressSet.t = "makeSet"
|
|
106
|
+
@send
|
|
107
|
+
external startBlockGroups: (t, string) => array<AddressSet.startBlockGroup> = "startBlockGroups"
|
|
108
|
+
@send external contractCount: (t, string) => int = "contractCount"
|
|
109
|
+
@send external size: t => int = "size"
|
|
110
|
+
|
|
111
|
+
// The gate routing applies, exposed for the simulate source — it has no real
|
|
112
|
+
// query boundary to gate at.
|
|
113
|
+
@send external has: (t, Address.t, string, int) => bool = "has"
|
|
114
|
+
|
|
115
|
+
// Drops every address registered after the target block, returning how many
|
|
116
|
+
// were dropped. Ids are tombstoned rather than reused, so sets built before the
|
|
117
|
+
// rollback still point at the right entries.
|
|
118
|
+
@send external rollback: (t, int) => int = "rollback"
|
|
119
|
+
|
|
120
|
+
@send external getRaw: (t, Address.t) => Null.t<Internal.indexingContract> = "get"
|
|
121
|
+
|
|
122
|
+
@send external contractAddresses: (t, string) => array<Address.t> = "contractAddresses"
|
|
123
|
+
|
|
124
|
+
@send external entries: t => array<Internal.indexingContract> = "entries"
|
|
125
|
+
|
|
126
|
+
let toVerdict = (raw: rawVerdict): verdict =>
|
|
127
|
+
switch raw.kind {
|
|
128
|
+
| "added" => Added({effectiveStartBlock: raw.effectiveStartBlock})
|
|
129
|
+
| "noEvents" => NoEvents({effectiveStartBlock: raw.effectiveStartBlock})
|
|
130
|
+
| "duplicate" =>
|
|
131
|
+
Duplicate({
|
|
132
|
+
effectiveStartBlock: raw.effectiveStartBlock,
|
|
133
|
+
existingEffectiveStartBlock: raw.existingEffectiveStartBlock->Null.getUnsafe,
|
|
134
|
+
})
|
|
135
|
+
| "conflict" => Conflict({existingContractName: raw.existingContractName->Null.getUnsafe})
|
|
136
|
+
| "invalid" => Invalid
|
|
137
|
+
| kind => JsError.throwWithMessage(`Unexpected address registration verdict "${kind}"`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Registers a batch, resolving each address against both the store and the
|
|
141
|
+
// batch's own earlier entries — so two contracts claiming one address inside a
|
|
142
|
+
// single batch conflict just as they would across batches. Verdicts come back
|
|
143
|
+
// in the batch's order.
|
|
144
|
+
let registerBatch = (store: t, registrations: array<registration>): array<verdict> =>
|
|
145
|
+
store->registerBatchRaw(registrations)->Array.map(toVerdict)
|
|
146
|
+
|
|
147
|
+
let makeSet = (store: t, ~contractName, ~options={}: makeSetOptions) =>
|
|
148
|
+
store->makeSetRaw(contractName, options)
|
|
149
|
+
|
|
150
|
+
// The entry an address is registered under, whichever contract holds it —
|
|
151
|
+
// addresses are unique chain-wide. `None` once rolled back.
|
|
152
|
+
let get = (store: t, address) => store->getRaw(address)->Null.toOption
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Core from "../Core.res.mjs";
|
|
4
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
5
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
|
+
|
|
8
|
+
function make(ecosystem, shouldChecksum, contracts) {
|
|
9
|
+
let ctor = Core.getAddon().AddressStore;
|
|
10
|
+
switch (ecosystem) {
|
|
11
|
+
case "evm" :
|
|
12
|
+
return ctor.newEvm(shouldChecksum, contracts);
|
|
13
|
+
case "fuel" :
|
|
14
|
+
return ctor.newFuel(contracts);
|
|
15
|
+
case "svm" :
|
|
16
|
+
return ctor.newSvm(contracts);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function contractsOf(onEventRegistrations) {
|
|
21
|
+
let startBlocks = {};
|
|
22
|
+
let names = [];
|
|
23
|
+
onEventRegistrations.forEach(reg => {
|
|
24
|
+
let name = reg.eventConfig.contractName;
|
|
25
|
+
let existing = startBlocks[name];
|
|
26
|
+
if (existing !== undefined) {
|
|
27
|
+
let existing$1 = Primitive_option.valFromOption(existing);
|
|
28
|
+
let match = reg.startBlock;
|
|
29
|
+
startBlocks[name] = existing$1 !== undefined ? (
|
|
30
|
+
match !== undefined ? Primitive_int.min(existing$1, match) : existing$1
|
|
31
|
+
) : (
|
|
32
|
+
match !== undefined ? match : undefined
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
names.push(name);
|
|
37
|
+
startBlocks[name] = reg.startBlock;
|
|
38
|
+
});
|
|
39
|
+
return names.map(name => ({
|
|
40
|
+
name: name,
|
|
41
|
+
startBlock: startBlocks[name]
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toVerdict(raw) {
|
|
46
|
+
let kind = raw.kind;
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case "added" :
|
|
49
|
+
return {
|
|
50
|
+
TAG: "Added",
|
|
51
|
+
effectiveStartBlock: raw.effectiveStartBlock
|
|
52
|
+
};
|
|
53
|
+
case "conflict" :
|
|
54
|
+
return {
|
|
55
|
+
TAG: "Conflict",
|
|
56
|
+
existingContractName: raw.existingContractName
|
|
57
|
+
};
|
|
58
|
+
case "duplicate" :
|
|
59
|
+
return {
|
|
60
|
+
TAG: "Duplicate",
|
|
61
|
+
effectiveStartBlock: raw.effectiveStartBlock,
|
|
62
|
+
existingEffectiveStartBlock: raw.existingEffectiveStartBlock
|
|
63
|
+
};
|
|
64
|
+
case "invalid" :
|
|
65
|
+
return "Invalid";
|
|
66
|
+
case "noEvents" :
|
|
67
|
+
return {
|
|
68
|
+
TAG: "NoEvents",
|
|
69
|
+
effectiveStartBlock: raw.effectiveStartBlock
|
|
70
|
+
};
|
|
71
|
+
default:
|
|
72
|
+
return Stdlib_JsError.throwWithMessage(`Unexpected address registration verdict "` + kind + `"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function registerBatch(store, registrations) {
|
|
77
|
+
return store.registerBatch(registrations).map(toVerdict);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeSet(store, contractName, optionsOpt) {
|
|
81
|
+
let options = optionsOpt !== undefined ? optionsOpt : ({});
|
|
82
|
+
return store.makeSet(contractName, options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function get(store, address) {
|
|
86
|
+
return Primitive_option.fromNull(store.get(address));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
make,
|
|
91
|
+
contractsOf,
|
|
92
|
+
toVerdict,
|
|
93
|
+
registerBatch,
|
|
94
|
+
makeSet,
|
|
95
|
+
get,
|
|
96
|
+
}
|
|
97
|
+
/* Core Not a pure module */
|
package/src/sources/EvmChain.res
CHANGED
|
@@ -45,6 +45,7 @@ let makeSources = (
|
|
|
45
45
|
~hyperSync,
|
|
46
46
|
~rpcs: array<rpc>,
|
|
47
47
|
~lowercaseAddresses,
|
|
48
|
+
~addressStore,
|
|
48
49
|
) => {
|
|
49
50
|
let sources = switch hyperSync {
|
|
50
51
|
| Some(endpointUrl) => [
|
|
@@ -58,6 +59,7 @@ let makeSources = (
|
|
|
58
59
|
serializationFormat: Env.hypersyncClientSerializationFormat,
|
|
59
60
|
enableQueryCaching: Env.hypersyncClientEnableQueryCaching,
|
|
60
61
|
logLevel: Env.hypersyncLogLevel,
|
|
62
|
+
addressStore,
|
|
61
63
|
}),
|
|
62
64
|
]
|
|
63
65
|
| _ => []
|
|
@@ -70,6 +72,7 @@ let makeSources = (
|
|
|
70
72
|
url,
|
|
71
73
|
onEventRegistrations,
|
|
72
74
|
lowercaseAddresses,
|
|
75
|
+
addressStore,
|
|
73
76
|
?ws,
|
|
74
77
|
?headers,
|
|
75
78
|
})
|
|
@@ -19,7 +19,7 @@ function getSyncConfig(param) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function makeSources(chain, onEventRegistrations, hyperSync, rpcs, lowercaseAddresses) {
|
|
22
|
+
function makeSources(chain, onEventRegistrations, hyperSync, rpcs, lowercaseAddresses, addressStore) {
|
|
23
23
|
let sources = hyperSync !== undefined ? [EvmHyperSyncSource.make({
|
|
24
24
|
chain: chain,
|
|
25
25
|
endpointUrl: hyperSync,
|
|
@@ -29,7 +29,8 @@ function makeSources(chain, onEventRegistrations, hyperSync, rpcs, lowercaseAddr
|
|
|
29
29
|
lowercaseAddresses: lowercaseAddresses,
|
|
30
30
|
serializationFormat: Env.hypersyncClientSerializationFormat,
|
|
31
31
|
enableQueryCaching: Env.hypersyncClientEnableQueryCaching,
|
|
32
|
-
logLevel: Env.hypersyncLogLevel
|
|
32
|
+
logLevel: Env.hypersyncLogLevel,
|
|
33
|
+
addressStore: addressStore
|
|
33
34
|
})] : [];
|
|
34
35
|
rpcs.forEach(param => {
|
|
35
36
|
let source = RpcSource.make({
|
|
@@ -39,6 +40,7 @@ function makeSources(chain, onEventRegistrations, hyperSync, rpcs, lowercaseAddr
|
|
|
39
40
|
chain: chain,
|
|
40
41
|
onEventRegistrations: onEventRegistrations,
|
|
41
42
|
lowercaseAddresses: lowercaseAddresses,
|
|
43
|
+
addressStore: addressStore,
|
|
42
44
|
ws: param.ws,
|
|
43
45
|
headers: param.headers
|
|
44
46
|
});
|