envio 3.5.1 → 3.6.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/evm.schema.json +7 -0
- package/fuel.schema.json +7 -0
- package/index.d.ts +33 -10
- package/package.json +6 -6
- package/src/ChainState.res +22 -4
- package/src/ChainState.res.mjs +20 -3
- package/src/ChainState.resi +3 -0
- package/src/Config.res +60 -2
- package/src/Config.res.mjs +49 -13
- package/src/EffectState.res +2 -2
- package/src/EffectState.res.mjs +2 -2
- package/src/EntityTables.res +32 -0
- package/src/EntityTables.res.mjs +44 -0
- package/src/Envio.res +7 -7
- package/src/Envio.res.mjs +5 -6
- package/src/Hasura.res +37 -3
- package/src/Hasura.res.mjs +23 -5
- package/src/InMemoryStore.res +48 -8
- package/src/InMemoryStore.res.mjs +37 -7
- package/src/IndexerState.res +30 -24
- package/src/IndexerState.res.mjs +20 -35
- package/src/IndexerState.resi +8 -6
- package/src/Internal.res +29 -11
- package/src/Internal.res.mjs +27 -10
- package/src/LoadLayer.res +39 -8
- package/src/LoadLayer.res.mjs +36 -9
- package/src/LoadLayer.resi +2 -0
- package/src/Metrics.res +5 -5
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +183 -37
- package/src/PgStorage.res.mjs +149 -34
- package/src/PruneStaleHistory.res +1 -0
- package/src/PruneStaleHistory.res.mjs +2 -1
- package/src/Sink.res +3 -3
- package/src/Sink.res.mjs +2 -2
- package/src/TestIndexer.res +128 -17
- package/src/TestIndexer.res.mjs +75 -9
- package/src/UserContext.res +19 -4
- package/src/UserContext.res.mjs +15 -9
- package/src/Writing.res +8 -16
- package/src/Writing.res.mjs +10 -10
- package/src/bindings/ClickHouse.res +40 -4
- package/src/bindings/ClickHouse.res.mjs +37 -5
- package/src/db/EntityHistory.res +64 -21
- package/src/db/EntityHistory.res.mjs +43 -22
- package/src/db/InternalTable.res.mjs +31 -31
- package/src/db/Table.res +24 -1
- package/src/db/Table.res.mjs +26 -4
- package/src/sources/SourceManager.res +6 -12
- package/src/sources/SourceManager.res.mjs +8 -5
- package/svm.schema.json +7 -0
package/src/TestIndexer.res
CHANGED
|
@@ -70,6 +70,19 @@ let getIndexingAddressesByChain = (state: testIndexerState): dict<
|
|
|
70
70
|
byChain
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// Rows of a per-chain entity are keyed per (chain, id): the same id exists
|
|
74
|
+
// independently on every chain.
|
|
75
|
+
let rowKey = (~scope: Internal.chainScope, ~entityId: EntityId.t) =>
|
|
76
|
+
switch scope {
|
|
77
|
+
| CrossChain => entityId->EntityId.toKey
|
|
78
|
+
| Chain(chainId) => `${chainId->ChainId.toString}|${entityId->EntityId.toKey}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let readChainId = (entity: Internal.entity, ~field: Table.field): option<ChainId.t> =>
|
|
82
|
+
entity
|
|
83
|
+
->(Utils.magic: Internal.entity => dict<ChainId.t>)
|
|
84
|
+
->Utils.Dict.dangerouslyGetNonOption(field.fieldName)
|
|
85
|
+
|
|
73
86
|
let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFilter.t): array<
|
|
74
87
|
Internal.entity,
|
|
75
88
|
> => {
|
|
@@ -79,16 +92,28 @@ let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFi
|
|
|
79
92
|
// entityConfig.
|
|
80
93
|
switch state.entityConfigs->Dict.get(tableName) {
|
|
81
94
|
| None => []
|
|
82
|
-
| Some(
|
|
95
|
+
| Some(entityConfig) =>
|
|
83
96
|
let entityDict = state.entities->Dict.get(tableName)->Option.getOr(Dict.make())
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
let matched =
|
|
98
|
+
entityDict
|
|
99
|
+
->Dict.valuesToArray
|
|
100
|
+
->Array.filter(entity => {
|
|
101
|
+
// The store holds decoded entities and the filter carries decoded values,
|
|
102
|
+
// so compare directly (same approach as InMemoryTable) — no JSON round-trip.
|
|
103
|
+
let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
|
|
104
|
+
filter->EntityFilter.matches(~entity=entityAsDict)
|
|
105
|
+
})
|
|
106
|
+
// The chain is already fixed by the scope the load ran for, so the loaded
|
|
107
|
+
// entity is handed back in the shape the handlers see.
|
|
108
|
+
switch entityConfig.table->Table.getChainIdField {
|
|
109
|
+
| None => matched
|
|
110
|
+
| Some(field) =>
|
|
111
|
+
matched->Array.map(entity => {
|
|
112
|
+
let copy = entity->(Utils.magic: Internal.entity => dict<unknown>)->Utils.Dict.shallowCopy
|
|
113
|
+
copy->Utils.Dict.deleteInPlace(field.fieldName)
|
|
114
|
+
copy->(Utils.magic: dict<unknown> => Internal.entity)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
119
|
|
|
@@ -104,8 +129,11 @@ let handleWriteBatch = (
|
|
|
104
129
|
// checkpointId -> entityName -> entityChange
|
|
105
130
|
let changesByCheckpoint: dict<dict<entityChange>> = Dict.make()
|
|
106
131
|
|
|
107
|
-
updatedEntities->Array.forEach(({entityConfig, changes}: Persistence.updatedEntity) => {
|
|
132
|
+
updatedEntities->Array.forEach(({entityConfig, scope, changes}: Persistence.updatedEntity) => {
|
|
108
133
|
let entityName = entityConfig.name
|
|
134
|
+
// The scope is what makes a per-chain row identifiable, so it's stamped
|
|
135
|
+
// onto the stored entity the same way the Postgres write path does.
|
|
136
|
+
let chainIdField = entityConfig.table->Table.getChainIdField
|
|
109
137
|
let entityDict = switch state.entities->Dict.get(entityName) {
|
|
110
138
|
| Some(dict) => dict
|
|
111
139
|
| None =>
|
|
@@ -138,10 +166,17 @@ let handleWriteBatch = (
|
|
|
138
166
|
// The store keeps decoded entities so load comparisons (bigint /
|
|
139
167
|
// BigDecimal) work on real values. Ids are keyed by their string form
|
|
140
168
|
// since they may be string/int/bigint.
|
|
141
|
-
|
|
169
|
+
let storedEntity = switch (chainIdField, scope->Internal.chainScopeChainId) {
|
|
170
|
+
| (Some(field), Some(chainId)) =>
|
|
171
|
+
entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId)
|
|
172
|
+
| _ => entity
|
|
173
|
+
}
|
|
174
|
+
entityDict->Dict.set(rowKey(~scope, ~entityId), storedEntity)
|
|
175
|
+
// The change already carries the checkpoint's chainId, so the entity
|
|
176
|
+
// inside it stays unstamped.
|
|
142
177
|
entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
|
|
143
178
|
| Delete({entityId, checkpointId}) =>
|
|
144
|
-
Dict.delete(entityDict->Obj.magic, entityId
|
|
179
|
+
Dict.delete(entityDict->Obj.magic, rowKey(~scope, ~entityId))
|
|
145
180
|
entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
|
|
146
181
|
}
|
|
147
182
|
}
|
|
@@ -379,7 +414,10 @@ let copyEntity = (entity: Internal.entity): Internal.entity =>
|
|
|
379
414
|
->Utils.Dict.shallowCopy
|
|
380
415
|
->(Utils.magic: dict<unknown> => Internal.entity)
|
|
381
416
|
|
|
382
|
-
// Entity operations for direct manipulation outside of handlers
|
|
417
|
+
// Entity operations for direct manipulation outside of handlers. Unlike a
|
|
418
|
+
// handler, which always runs on a known chain, these are chain-agnostic — so a
|
|
419
|
+
// per-chain entity is looked up across every chain and an id present on more
|
|
420
|
+
// than one is an error rather than an arbitrary pick.
|
|
383
421
|
let getEntityFromState = (
|
|
384
422
|
~state: testIndexerState,
|
|
385
423
|
~entityConfig: Internal.entityConfig,
|
|
@@ -392,7 +430,25 @@ let getEntityFromState = (
|
|
|
392
430
|
)
|
|
393
431
|
}
|
|
394
432
|
let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
|
|
395
|
-
|
|
433
|
+
switch entityConfig.table->Table.getChainIdField {
|
|
434
|
+
| None => entityDict->Dict.get(entityId)->Option.map(copyEntity)
|
|
435
|
+
| Some(field) =>
|
|
436
|
+
let matches = entityDict->Dict.valuesToArray->Array.filter(entity => entity.id === entityId)
|
|
437
|
+
switch matches {
|
|
438
|
+
| [] => None
|
|
439
|
+
| [entity] => Some(copyEntity(entity))
|
|
440
|
+
| _ =>
|
|
441
|
+
let chains =
|
|
442
|
+
matches
|
|
443
|
+
->Array.map(entity =>
|
|
444
|
+
entity->readChainId(~field)->Option.mapOr("unknown", ChainId.toString)
|
|
445
|
+
)
|
|
446
|
+
->Array.join(", ")
|
|
447
|
+
JsError.throwWithMessage(
|
|
448
|
+
`Entity \`${entityConfig.name}\` with id \`${entityId}\` exists on multiple chains (${chains}) — use getWhere({${field.fieldName}: {_eq: ...}}) to pick one.`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
396
452
|
}
|
|
397
453
|
|
|
398
454
|
let makeEntityGet = (~state: testIndexerState, ~entityConfig: Internal.entityConfig): (
|
|
@@ -435,7 +491,23 @@ let makeEntitySet = (~state: testIndexerState, ~entityConfig: Internal.entityCon
|
|
|
435
491
|
state.entities->Dict.set(entityConfig.name, dict)
|
|
436
492
|
dict
|
|
437
493
|
}
|
|
438
|
-
|
|
494
|
+
// Outside a handler there's no chain in context, so a per-chain entity has
|
|
495
|
+
// to say which chain the row belongs to.
|
|
496
|
+
let scope = switch entityConfig.table->Table.getChainIdField {
|
|
497
|
+
| None => Internal.CrossChain
|
|
498
|
+
| Some(field) =>
|
|
499
|
+
switch entity->readChainId(~field) {
|
|
500
|
+
| Some(chainId) => Internal.Chain(chainId)
|
|
501
|
+
| None =>
|
|
502
|
+
JsError.throwWithMessage(
|
|
503
|
+
`${entityConfig.name}.set() requires a \`${field.fieldName}\` because the entity is per-chain. Pass it alongside the entity fields.`,
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
entityDict->Dict.set(
|
|
508
|
+
rowKey(~scope, ~entityId=entity.id->EntityId.unsafeOfString),
|
|
509
|
+
copyEntity(entity),
|
|
510
|
+
)
|
|
439
511
|
}
|
|
440
512
|
}
|
|
441
513
|
|
|
@@ -453,9 +525,43 @@ let makeEntityGetAll = (~state: testIndexerState, ~entityConfig: Internal.entity
|
|
|
453
525
|
}
|
|
454
526
|
}
|
|
455
527
|
|
|
528
|
+
// The same filter syntax as `context.X.getWhere` in a handler, matched against
|
|
529
|
+
// the store instead of a database. A per-chain entity's rows carry their chain
|
|
530
|
+
// id, so `{chainId: {_eq: 1}}` is what narrows an id that exists on several
|
|
531
|
+
// chains.
|
|
532
|
+
let makeEntityGetWhere = (~state: testIndexerState, ~entityConfig: Internal.entityConfig): (
|
|
533
|
+
dict<dict<unknown>> => promise<array<Internal.entity>>
|
|
534
|
+
) => {
|
|
535
|
+
filter => {
|
|
536
|
+
if state.processInProgress {
|
|
537
|
+
JsError.throwWithMessage(
|
|
538
|
+
`Cannot call ${entityConfig.name}.getWhere() while indexer.process() is running. ` ++ "Wait for process() to complete before accessing entities directly.",
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
let filters =
|
|
542
|
+
filter->EntityFilter.parseGetWhereOrThrow(
|
|
543
|
+
~entityName=entityConfig.name,
|
|
544
|
+
~table=entityConfig.table,
|
|
545
|
+
)
|
|
546
|
+
let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
|
|
547
|
+
// parseGetWhereOrThrow expands an operator group into alternatives whose
|
|
548
|
+
// matches are disjoint, so the union needs no dedup.
|
|
549
|
+
Promise.resolve(
|
|
550
|
+
entityDict
|
|
551
|
+
->Dict.valuesToArray
|
|
552
|
+
->Array.filter(entity => {
|
|
553
|
+
let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
|
|
554
|
+
filters->Array.some(filter => filter->EntityFilter.matches(~entity=entityAsDict))
|
|
555
|
+
})
|
|
556
|
+
->Array.map(copyEntity),
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
456
561
|
type entityOperations = {
|
|
457
562
|
get: string => promise<option<Internal.entity>>,
|
|
458
563
|
getAll: unit => promise<array<Internal.entity>>,
|
|
564
|
+
getWhere: dict<dict<unknown>> => promise<array<Internal.entity>>,
|
|
459
565
|
getOrThrow: (string, ~message: string=?) => promise<Internal.entity>,
|
|
460
566
|
set: Internal.entity => unit,
|
|
461
567
|
}
|
|
@@ -507,8 +613,12 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
|
|
|
507
613
|
reset: async () => (),
|
|
508
614
|
setChainMeta: async _ => Obj.magic(),
|
|
509
615
|
pruneStaleCheckpoints: async (~safeCheckpointId as _) => (),
|
|
510
|
-
pruneStaleEntityHistory: async (
|
|
511
|
-
|
|
616
|
+
pruneStaleEntityHistory: async (
|
|
617
|
+
~entityName as _,
|
|
618
|
+
~entityIndex as _,
|
|
619
|
+
~chainIdColumn as _,
|
|
620
|
+
~safeCheckpointId as _,
|
|
621
|
+
) => (),
|
|
512
622
|
getRollbackTargetCheckpoint: async (~reorgChainId as _, ~lastKnownValidBlockNumber as _) =>
|
|
513
623
|
JsError.throwWithMessage(
|
|
514
624
|
"TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable.",
|
|
@@ -625,6 +735,7 @@ let createTestIndexer = (): t<'processConfig> => {
|
|
|
625
735
|
{
|
|
626
736
|
get: makeEntityGet(~state, ~entityConfig),
|
|
627
737
|
getAll: makeEntityGetAll(~state, ~entityConfig),
|
|
738
|
+
getWhere: makeEntityGetWhere(~state, ~entityConfig),
|
|
628
739
|
getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig),
|
|
629
740
|
set: makeEntitySet(~state, ~entityConfig),
|
|
630
741
|
},
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Env from "./Env.res.mjs";
|
|
4
|
+
import * as Table from "./db/Table.res.mjs";
|
|
4
5
|
import * as Utils from "./Utils.res.mjs";
|
|
5
6
|
import * as Config from "./Config.res.mjs";
|
|
6
7
|
import * as ChainId from "./ChainId.res.mjs";
|
|
7
8
|
import * as Logging from "./Logging.res.mjs";
|
|
8
9
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
9
10
|
import * as EntityId from "./EntityId.res.mjs";
|
|
11
|
+
import * as Internal from "./Internal.res.mjs";
|
|
10
12
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
11
13
|
import * as IndexerLoop from "./IndexerLoop.res.mjs";
|
|
12
14
|
import * as Persistence from "./Persistence.res.mjs";
|
|
@@ -55,19 +57,44 @@ function getIndexingAddressesByChain(state) {
|
|
|
55
57
|
return byChain;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function rowKey(scope, entityId) {
|
|
61
|
+
if (scope === "crossChain") {
|
|
62
|
+
return EntityId.toKey(entityId);
|
|
63
|
+
} else {
|
|
64
|
+
return ChainId.toString(scope) + `|` + EntityId.toKey(entityId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readChainId(entity, field) {
|
|
69
|
+
return entity[field.fieldName];
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
function handleLoad(state, tableName, filter) {
|
|
59
|
-
let
|
|
60
|
-
if (
|
|
73
|
+
let entityConfig = state.entityConfigs[tableName];
|
|
74
|
+
if (entityConfig === undefined) {
|
|
61
75
|
return [];
|
|
62
76
|
}
|
|
63
77
|
let entityDict = Stdlib_Option.getOr(state.entities[tableName], {});
|
|
64
|
-
|
|
78
|
+
let matched = Object.values(entityDict).filter(entity => EntityFilter.matches(filter, entity));
|
|
79
|
+
let field = Table.getChainIdField(entityConfig.table);
|
|
80
|
+
if (field !== undefined) {
|
|
81
|
+
return matched.map(entity => {
|
|
82
|
+
let copy = Utils.Dict.shallowCopy(entity);
|
|
83
|
+
Utils.Dict.deleteInPlace(copy, field.fieldName);
|
|
84
|
+
return copy;
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
return matched;
|
|
88
|
+
}
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChainIds, checkpointBlockNumbers, checkpointEventsProcessed) {
|
|
68
92
|
let changesByCheckpoint = {};
|
|
69
93
|
updatedEntities.forEach(param => {
|
|
70
|
-
let
|
|
94
|
+
let scope = param.scope;
|
|
95
|
+
let entityConfig = param.entityConfig;
|
|
96
|
+
let entityName = entityConfig.name;
|
|
97
|
+
let chainIdField = Table.getChainIdField(entityConfig.table);
|
|
71
98
|
let dict = state.entities[entityName];
|
|
72
99
|
let entityDict;
|
|
73
100
|
if (dict !== undefined) {
|
|
@@ -104,12 +131,14 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
|
|
|
104
131
|
let processChange = change => {
|
|
105
132
|
if (change.type === "SET") {
|
|
106
133
|
let entity = change.entity;
|
|
107
|
-
|
|
134
|
+
let match = Internal.chainScopeChainId(scope);
|
|
135
|
+
let storedEntity = chainIdField !== undefined && match !== undefined ? Internal.stampChainId(entity, chainIdField.fieldName, Primitive_option.valFromOption(match)) : entity;
|
|
136
|
+
entityDict[rowKey(scope, change.entityId)] = storedEntity;
|
|
108
137
|
entityChangeFor(change.checkpointId).sets.push(entity);
|
|
109
138
|
return;
|
|
110
139
|
}
|
|
111
140
|
let entityId = change.entityId;
|
|
112
|
-
Stdlib_Dict.$$delete(entityDict,
|
|
141
|
+
Stdlib_Dict.$$delete(entityDict, rowKey(scope, entityId));
|
|
113
142
|
entityChangeFor(change.checkpointId).deleted.push(entityId);
|
|
114
143
|
};
|
|
115
144
|
param.changes.forEach(processChange);
|
|
@@ -265,7 +294,21 @@ function getEntityFromState(state, entityConfig, entityId, methodName) {
|
|
|
265
294
|
Stdlib_JsError.throwWithMessage(`Cannot call ` + entityConfig.name + `.` + methodName + `() while indexer.process() is running. ` + "Wait for process() to complete before accessing entities directly.");
|
|
266
295
|
}
|
|
267
296
|
let entityDict = Stdlib_Option.getOr(state.entities[entityConfig.name], {});
|
|
268
|
-
|
|
297
|
+
let field = Table.getChainIdField(entityConfig.table);
|
|
298
|
+
if (field === undefined) {
|
|
299
|
+
return Stdlib_Option.map(entityDict[entityId], copyEntity);
|
|
300
|
+
}
|
|
301
|
+
let matches = Object.values(entityDict).filter(entity => entity.id === entityId);
|
|
302
|
+
let len = matches.length;
|
|
303
|
+
if (len !== 1) {
|
|
304
|
+
if (len === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let chains = matches.map(entity => Stdlib_Option.mapOr(entity[field.fieldName], "unknown", ChainId.toString)).join(", ");
|
|
308
|
+
return Stdlib_JsError.throwWithMessage(`Entity \`` + entityConfig.name + `\` with id \`` + entityId + `\` exists on multiple chains (` + chains + `) — use getWhere({` + field.fieldName + `: {_eq: ...}}) to pick one.`);
|
|
309
|
+
}
|
|
310
|
+
let entity = matches[0];
|
|
311
|
+
return Utils.Dict.shallowCopy(entity);
|
|
269
312
|
}
|
|
270
313
|
|
|
271
314
|
function makeEntityGet(state, entityConfig) {
|
|
@@ -297,7 +340,15 @@ function makeEntitySet(state, entityConfig) {
|
|
|
297
340
|
state.entities[entityConfig.name] = dict$1;
|
|
298
341
|
entityDict = dict$1;
|
|
299
342
|
}
|
|
300
|
-
|
|
343
|
+
let field = Table.getChainIdField(entityConfig.table);
|
|
344
|
+
let scope;
|
|
345
|
+
if (field !== undefined) {
|
|
346
|
+
let chainId = entity[field.fieldName];
|
|
347
|
+
scope = chainId !== undefined ? Primitive_option.valFromOption(chainId) : Stdlib_JsError.throwWithMessage(entityConfig.name + `.set() requires a \`` + field.fieldName + `\` because the entity is per-chain. Pass it alongside the entity fields.`);
|
|
348
|
+
} else {
|
|
349
|
+
scope = "crossChain";
|
|
350
|
+
}
|
|
351
|
+
entityDict[rowKey(scope, entity.id)] = Utils.Dict.shallowCopy(entity);
|
|
301
352
|
};
|
|
302
353
|
}
|
|
303
354
|
|
|
@@ -311,6 +362,17 @@ function makeEntityGetAll(state, entityConfig) {
|
|
|
311
362
|
};
|
|
312
363
|
}
|
|
313
364
|
|
|
365
|
+
function makeEntityGetWhere(state, entityConfig) {
|
|
366
|
+
return filter => {
|
|
367
|
+
if (state.processInProgress) {
|
|
368
|
+
Stdlib_JsError.throwWithMessage(`Cannot call ` + entityConfig.name + `.getWhere() while indexer.process() is running. ` + "Wait for process() to complete before accessing entities directly.");
|
|
369
|
+
}
|
|
370
|
+
let filters = EntityFilter.parseGetWhereOrThrow(filter, entityConfig.name, entityConfig.table);
|
|
371
|
+
let entityDict = Stdlib_Option.getOr(state.entities[entityConfig.name], {});
|
|
372
|
+
return Promise.resolve(Object.values(entityDict).filter(entity => filters.some(filter => EntityFilter.matches(filter, entity))).map(copyEntity));
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
314
376
|
function makeInMemoryStorage(state) {
|
|
315
377
|
return {
|
|
316
378
|
name: "test-inmemory",
|
|
@@ -325,7 +387,7 @@ function makeInMemoryStorage(state) {
|
|
|
325
387
|
reset: async () => {},
|
|
326
388
|
setChainMeta: async param => {},
|
|
327
389
|
pruneStaleCheckpoints: async param => {},
|
|
328
|
-
pruneStaleEntityHistory: async (param, param$1, param$2) => {},
|
|
390
|
+
pruneStaleEntityHistory: async (param, param$1, param$2, param$3) => {},
|
|
329
391
|
getRollbackTargetCheckpoint: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
|
|
330
392
|
getRollbackProgressDiff: async param => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
|
|
331
393
|
getRollbackData: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable."),
|
|
@@ -407,6 +469,7 @@ function createTestIndexer() {
|
|
|
407
469
|
entityOpsDict[entityConfig.name] = {
|
|
408
470
|
get: makeEntityGet(state, entityConfig),
|
|
409
471
|
getAll: makeEntityGetAll(state, entityConfig),
|
|
472
|
+
getWhere: makeEntityGetWhere(state, entityConfig),
|
|
410
473
|
getOrThrow: makeEntityGetOrThrow(state, entityConfig),
|
|
411
474
|
set: makeEntitySet(state, entityConfig)
|
|
412
475
|
};
|
|
@@ -613,6 +676,8 @@ function createTestIndexer() {
|
|
|
613
676
|
export {
|
|
614
677
|
toIndexingAddress,
|
|
615
678
|
getIndexingAddressesByChain,
|
|
679
|
+
rowKey,
|
|
680
|
+
readChainId,
|
|
616
681
|
handleLoad,
|
|
617
682
|
handleWriteBatch,
|
|
618
683
|
makeInitialState,
|
|
@@ -626,6 +691,7 @@ export {
|
|
|
626
691
|
makeEntityGetOrThrow,
|
|
627
692
|
makeEntitySet,
|
|
628
693
|
makeEntityGetAll,
|
|
694
|
+
makeEntityGetWhere,
|
|
629
695
|
makeInMemoryStorage,
|
|
630
696
|
cloneRegistrations,
|
|
631
697
|
registrationsRef,
|
package/src/UserContext.res
CHANGED
|
@@ -60,15 +60,19 @@ external makeEffectContext: (
|
|
|
60
60
|
|
|
61
61
|
let initEffect = (params: contextParams) => {
|
|
62
62
|
let handlerChainId = params.item->Internal.getItemChainId
|
|
63
|
+
// An effect that didn't state a scope follows the config: cross-chain by
|
|
64
|
+
// default, per-chain under `disable_default_cross_chain`.
|
|
65
|
+
let isCrossChain = (effect: Internal.effect) =>
|
|
66
|
+
effect.crossChain->Option.getOr(params.config.defaultCrossChain)
|
|
63
67
|
// A chain-scoped effect always resolves against the chain of the handler that
|
|
64
68
|
// triggered the call, even several effects deep, so the chain id is captured
|
|
65
69
|
// once from the item and reused for the whole nested-call tree.
|
|
66
70
|
let rec makeCaller = (~caller: option<Internal.effect>) => {
|
|
67
71
|
(effect: Internal.effect, input: Internal.effectInput) => {
|
|
68
|
-
let scope = effect
|
|
72
|
+
let scope = effect->isCrossChain ? Internal.CrossChain : Internal.Chain(handlerChainId)
|
|
69
73
|
|
|
70
74
|
switch caller {
|
|
71
|
-
| Some(callerEffect) if callerEffect
|
|
75
|
+
| Some(callerEffect) if callerEffect->isCrossChain && !(effect->isCrossChain) =>
|
|
72
76
|
// A cross-chain effect isn't tied to a single chain, so it has no chain
|
|
73
77
|
// to resolve a chain-scoped child against. Reject before any cache work.
|
|
74
78
|
JsError.throwWithMessage(
|
|
@@ -114,6 +118,13 @@ type entityContextParams = {
|
|
|
114
118
|
entityConfig: Internal.entityConfig,
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// The handler context is always chain-scoped, so a per-chain entity resolves to
|
|
122
|
+
// the chain the handler runs on and a cross-chain one to the shared partition.
|
|
123
|
+
let entityScope = (params: entityContextParams) =>
|
|
124
|
+
params.entityConfig->InMemoryStore.entityScope(
|
|
125
|
+
~chainId=params.item->Internal.getItemChainId,
|
|
126
|
+
)
|
|
127
|
+
|
|
117
128
|
let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>) => {
|
|
118
129
|
let entityConfig = params.entityConfig
|
|
119
130
|
|
|
@@ -123,6 +134,7 @@ let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>)
|
|
|
123
134
|
~loadManager=params.loadManager,
|
|
124
135
|
~persistence=params.persistence,
|
|
125
136
|
~entityConfig,
|
|
137
|
+
~scope=params->entityScope,
|
|
126
138
|
~indexerState=params.indexerState,
|
|
127
139
|
~shouldGroup=params.isPreload,
|
|
128
140
|
~item=params.item,
|
|
@@ -164,7 +176,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
164
176
|
? noopSet
|
|
165
177
|
: (entity: Internal.entity) => {
|
|
166
178
|
params.indexerState
|
|
167
|
-
->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig)
|
|
179
|
+
->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig, ~scope=params->entityScope)
|
|
168
180
|
->InMemoryTable.Entity.set(
|
|
169
181
|
~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointId,
|
|
170
182
|
Set({
|
|
@@ -188,6 +200,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
188
200
|
~loadManager=params.loadManager,
|
|
189
201
|
~persistence=params.persistence,
|
|
190
202
|
~entityConfig=params.entityConfig,
|
|
203
|
+
~scope=params->entityScope,
|
|
191
204
|
~indexerState=params.indexerState,
|
|
192
205
|
~shouldGroup=params.isPreload,
|
|
193
206
|
~item=params.item,
|
|
@@ -220,6 +233,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
220
233
|
~loadManager=params.loadManager,
|
|
221
234
|
~persistence=params.persistence,
|
|
222
235
|
~entityConfig=params.entityConfig,
|
|
236
|
+
~scope=params->entityScope,
|
|
223
237
|
~indexerState=params.indexerState,
|
|
224
238
|
~shouldGroup=params.isPreload,
|
|
225
239
|
~item=params.item,
|
|
@@ -250,6 +264,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
250
264
|
~loadManager=params.loadManager,
|
|
251
265
|
~persistence=params.persistence,
|
|
252
266
|
~entityConfig=params.entityConfig,
|
|
267
|
+
~scope=params->entityScope,
|
|
253
268
|
~indexerState=params.indexerState,
|
|
254
269
|
~shouldGroup=params.isPreload,
|
|
255
270
|
~item=params.item,
|
|
@@ -273,7 +288,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
273
288
|
} else {
|
|
274
289
|
entityId => {
|
|
275
290
|
params.indexerState
|
|
276
|
-
->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig)
|
|
291
|
+
->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig, ~scope=params->entityScope)
|
|
277
292
|
->InMemoryTable.Entity.set(
|
|
278
293
|
~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointId,
|
|
279
294
|
Delete({
|
package/src/UserContext.res.mjs
CHANGED
|
@@ -49,9 +49,10 @@ EffectContext.prototype = effectContextPrototype;
|
|
|
49
49
|
|
|
50
50
|
function initEffect(params) {
|
|
51
51
|
let handlerChainId = Internal.getItemChainId(params.item);
|
|
52
|
+
let isCrossChain = effect => Stdlib_Option.getOr(effect.crossChain, params.config.defaultCrossChain);
|
|
52
53
|
let makeCaller = caller => ((effect, input) => {
|
|
53
|
-
let scope = effect
|
|
54
|
-
if (caller !== undefined && caller
|
|
54
|
+
let scope = isCrossChain(effect) ? "crossChain" : handlerChainId;
|
|
55
|
+
if (caller !== undefined && isCrossChain(caller) && !isCrossChain(effect)) {
|
|
55
56
|
Stdlib_JsError.throwWithMessage(`The cross-chain effect "` + caller.name + `" cannot call the chain-scoped effect "` + effect.name + `", because a cross-chain effect isn't tied to a single chain. Make "` + effect.name + `" cross-chain (\`crossChain: true\`), or make "` + caller.name + `" chain-scoped (\`crossChain: false\`).`);
|
|
56
57
|
}
|
|
57
58
|
let tmp;
|
|
@@ -70,13 +71,17 @@ function initEffect(params) {
|
|
|
70
71
|
return makeCaller(undefined);
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function entityScope(params) {
|
|
75
|
+
return InMemoryStore.entityScope(params.entityConfig, Internal.getItemChainId(params.item));
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
function getWhereHandler(params, filter) {
|
|
74
79
|
let entityConfig = params.entityConfig;
|
|
75
80
|
let filters = EntityFilter.parseGetWhereOrThrow(filter, entityConfig.name, entityConfig.table);
|
|
76
81
|
if (filters.length !== 1) {
|
|
77
|
-
return Promise.all(filters.map(filter => LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, params.indexerState, params.isPreload, params.item, params.config.ecosystem, filter))).then(results => results.flat());
|
|
82
|
+
return Promise.all(filters.map(filter => LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, filter))).then(results => results.flat());
|
|
78
83
|
} else {
|
|
79
|
-
return LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, params.indexerState, params.isPreload, params.item, params.config.ecosystem, filters[0]);
|
|
84
|
+
return LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, filters[0]);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -94,7 +99,7 @@ function throwClickHouseReadOnly(entityConfig, op) {
|
|
|
94
99
|
|
|
95
100
|
let entityTraps_get = (params, prop) => {
|
|
96
101
|
let isClickHouseOnly = !params.entityConfig.storage.postgres;
|
|
97
|
-
let set = params.isPreload ? noopSet : entity => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig), IndexerState.committedCheckpointId(params.indexerState), {
|
|
102
|
+
let set = params.isPreload ? noopSet : entity => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
98
103
|
type: "SET",
|
|
99
104
|
entityId: entity.id,
|
|
100
105
|
entity: entity,
|
|
@@ -105,7 +110,7 @@ let entityTraps_get = (params, prop) => {
|
|
|
105
110
|
if (params.isPreload) {
|
|
106
111
|
return noopDeleteUnsafe;
|
|
107
112
|
} else {
|
|
108
|
-
return entityId => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig), IndexerState.committedCheckpointId(params.indexerState), {
|
|
113
|
+
return entityId => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
109
114
|
type: "DELETE",
|
|
110
115
|
entityId: entityId,
|
|
111
116
|
checkpointId: params.checkpointId
|
|
@@ -115,13 +120,13 @@ let entityTraps_get = (params, prop) => {
|
|
|
115
120
|
if (isClickHouseOnly) {
|
|
116
121
|
return _entityId => throwClickHouseReadOnly(params.entityConfig, "get");
|
|
117
122
|
} else {
|
|
118
|
-
return entityId => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId);
|
|
123
|
+
return entityId => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId);
|
|
119
124
|
}
|
|
120
125
|
case "getOrCreate" :
|
|
121
126
|
if (isClickHouseOnly) {
|
|
122
127
|
return _entity => throwClickHouseReadOnly(params.entityConfig, "getOrCreate");
|
|
123
128
|
} else {
|
|
124
|
-
return entity => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.indexerState, params.isPreload, params.item, params.config.ecosystem, entity.id).then(storageEntity => {
|
|
129
|
+
return entity => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entity.id).then(storageEntity => {
|
|
125
130
|
if (storageEntity !== undefined) {
|
|
126
131
|
return storageEntity;
|
|
127
132
|
} else {
|
|
@@ -134,7 +139,7 @@ let entityTraps_get = (params, prop) => {
|
|
|
134
139
|
if (isClickHouseOnly) {
|
|
135
140
|
return (_entityId, param) => throwClickHouseReadOnly(params.entityConfig, "getOrThrow");
|
|
136
141
|
} else {
|
|
137
|
-
return (entityId, message) => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId).then(entity => {
|
|
142
|
+
return (entityId, message) => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId).then(entity => {
|
|
138
143
|
if (entity !== undefined) {
|
|
139
144
|
return entity;
|
|
140
145
|
} else {
|
|
@@ -210,6 +215,7 @@ export {
|
|
|
210
215
|
paramsByThis,
|
|
211
216
|
effectContextPrototype,
|
|
212
217
|
initEffect,
|
|
218
|
+
entityScope,
|
|
213
219
|
getWhereHandler,
|
|
214
220
|
noopSet,
|
|
215
221
|
noopDeleteUnsafe,
|
package/src/Writing.res
CHANGED
|
@@ -10,10 +10,8 @@ let keepLatestChangesLimit = Env.inMemoryObjectsTarget
|
|
|
10
10
|
|
|
11
11
|
let getChangesCount = (state: IndexerState.t) => {
|
|
12
12
|
let total = ref(0.)
|
|
13
|
-
state
|
|
14
|
-
|
|
15
|
-
->Array.forEach(entityConfig => {
|
|
16
|
-
total := total.contents +. (state->InMemoryStore.getInMemTable(~entityConfig)).changesCount
|
|
13
|
+
state->IndexerState.eachEntityTable((~entityConfig as _, ~scope as _, ~table) => {
|
|
14
|
+
total := total.contents +. table.changesCount
|
|
17
15
|
})
|
|
18
16
|
state
|
|
19
17
|
->IndexerState.effectState
|
|
@@ -113,14 +111,12 @@ let runOneWrite = async (state: IndexerState.t) => {
|
|
|
113
111
|
|
|
114
112
|
let rollback = state->IndexerState.takeRollback
|
|
115
113
|
|
|
116
|
-
let updatedEntities =
|
|
117
|
-
|
|
114
|
+
let updatedEntities = []
|
|
115
|
+
state->IndexerState.eachEntityTable((~entityConfig, ~scope, ~table) => {
|
|
118
116
|
let changes =
|
|
119
117
|
table->InMemoryTable.Entity.snapshotChanges(~committedCheckpointId, ~upToCheckpointId)
|
|
120
|
-
if changes->Utils.Array.
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
Some(({entityConfig, changes}: Persistence.updatedEntity))
|
|
118
|
+
if changes->Utils.Array.notEmpty {
|
|
119
|
+
updatedEntities->Array.push(({entityConfig, scope, changes}: Persistence.updatedEntity))
|
|
124
120
|
}
|
|
125
121
|
})
|
|
126
122
|
let updatedEffectsCache = snapshotEffects(state, ~cache)
|
|
@@ -221,12 +217,8 @@ let commitBatch = (state: IndexerState.t, ~batch: Batch.t) => {
|
|
|
221
217
|
// keepLoadedFromDb, entries seeded from a db read are spared.
|
|
222
218
|
let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
|
|
223
219
|
let committedCheckpointId = state->IndexerState.committedCheckpointId
|
|
224
|
-
state
|
|
225
|
-
|
|
226
|
-
->Array.forEach(entityConfig =>
|
|
227
|
-
state
|
|
228
|
-
->InMemoryStore.getInMemTable(~entityConfig)
|
|
229
|
-
->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
|
|
220
|
+
state->IndexerState.eachEntityTable((~entityConfig as _, ~scope as _, ~table) =>
|
|
221
|
+
table->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
|
|
230
222
|
)
|
|
231
223
|
state
|
|
232
224
|
->IndexerState.effectState
|
package/src/Writing.res.mjs
CHANGED
|
@@ -19,8 +19,8 @@ function getChangesCount(state) {
|
|
|
19
19
|
let total = {
|
|
20
20
|
contents: 0
|
|
21
21
|
};
|
|
22
|
-
IndexerState.
|
|
23
|
-
total.contents = total.contents +
|
|
22
|
+
IndexerState.eachEntityTable(state, (param, param$1, table) => {
|
|
23
|
+
total.contents = total.contents + table.changesCount;
|
|
24
24
|
});
|
|
25
25
|
EffectState.forEach(IndexerState.effectState(state), inMemTable => {
|
|
26
26
|
total.contents = total.contents + inMemTable.changesCount;
|
|
@@ -105,16 +105,16 @@ async function runOneWrite(state) {
|
|
|
105
105
|
let checkpointId = Utils.$$Array.last(batch.checkpointIds);
|
|
106
106
|
let upToCheckpointId = checkpointId !== undefined ? checkpointId : committedCheckpointId;
|
|
107
107
|
let rollback = IndexerState.takeRollback(state);
|
|
108
|
-
let updatedEntities =
|
|
109
|
-
|
|
108
|
+
let updatedEntities = [];
|
|
109
|
+
IndexerState.eachEntityTable(state, (entityConfig, scope, table) => {
|
|
110
110
|
let changes = InMemoryTable.Entity.snapshotChanges(table, committedCheckpointId, upToCheckpointId);
|
|
111
|
-
if (Utils.$$Array.
|
|
112
|
-
|
|
113
|
-
} else {
|
|
114
|
-
return {
|
|
111
|
+
if (Utils.$$Array.notEmpty(changes)) {
|
|
112
|
+
updatedEntities.push({
|
|
115
113
|
entityConfig: entityConfig,
|
|
114
|
+
scope: scope,
|
|
116
115
|
changes: changes
|
|
117
|
-
};
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
let updatedEffectsCache = snapshotEffects(state, cache);
|
|
@@ -179,7 +179,7 @@ function commitBatch(state, batch) {
|
|
|
179
179
|
|
|
180
180
|
function dropCommitted(state, keepLoadedFromDb) {
|
|
181
181
|
let committedCheckpointId = IndexerState.committedCheckpointId(state);
|
|
182
|
-
IndexerState.
|
|
182
|
+
IndexerState.eachEntityTable(state, (param, param$1, table) => InMemoryTable.Entity.dropCommittedChanges(table, committedCheckpointId, keepLoadedFromDb));
|
|
183
183
|
EffectState.forEach(IndexerState.effectState(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
|
|
184
184
|
}
|
|
185
185
|
|