envio 3.15.0-subgraph → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -34
- package/evm.schema.json +3 -3
- package/fuel.schema.json +2 -2
- package/index.d.ts +3 -3
- package/licenses/README.md +1 -1
- package/package.json +9 -10
- package/src/Batch.res.mjs +5 -5
- package/src/BatchProcessing.res +1 -0
- package/src/BatchProcessing.res.mjs +1 -0
- package/src/Bin.res +0 -7
- package/src/Bin.res.mjs +0 -8
- package/src/ChainFetching.res +17 -456
- package/src/ChainFetching.res.mjs +33 -404
- package/src/ChainState.res +48 -83
- package/src/ChainState.res.mjs +83 -115
- package/src/ChainState.resi +0 -26
- package/src/Clock.res +57 -0
- package/src/Clock.res.mjs +72 -0
- package/src/Config.res +7 -16
- package/src/Config.res.mjs +4 -11
- package/src/ContractRegisterContext.res +6 -3
- package/src/ContractRegisterContext.res.mjs +2 -4
- package/src/Core.res +5 -136
- package/src/Core.res.mjs +1 -90
- package/src/CrossChainState.res +2 -8
- package/src/CrossChainState.res.mjs +3 -3
- package/src/Ecosystem.res +4 -7
- package/src/Ecosystem.res.mjs +8 -8
- package/src/EffectState.res +10 -2
- package/src/EffectState.res.mjs +8 -4
- package/src/EffectState.resi +3 -1
- package/src/EntityId.res +2 -7
- package/src/EntityId.res.mjs +1 -8
- package/src/EntityTables.res +4 -1
- package/src/EntityTables.res.mjs +1 -1
- package/src/Env.res +0 -16
- package/src/Env.res.mjs +0 -11
- package/src/EventProcessing.res +1 -38
- package/src/EventProcessing.res.mjs +1 -28
- package/src/FetchState.res +160 -319
- package/src/FetchState.res.mjs +182 -292
- package/src/FinalizeBackfill.res +2 -4
- package/src/FinalizeBackfill.res.mjs +3 -4
- package/src/HandlerLoader.res +15 -39
- package/src/HandlerLoader.res.mjs +8 -27
- package/src/HandlerRegister.res +0 -4
- package/src/HandlerRegister.res.mjs +2 -5
- package/src/HandlerRegister.resi +0 -2
- package/src/InMemoryTable.res +17 -16
- package/src/InMemoryTable.res.mjs +14 -15
- package/src/IndexerState.res +28 -4
- package/src/IndexerState.res.mjs +19 -6
- package/src/IndexerState.resi +4 -0
- package/src/Internal.res +0 -110
- package/src/Internal.res.mjs +1 -115
- package/src/LoadLayer.res +21 -47
- package/src/LoadLayer.res.mjs +47 -72
- package/src/LoadLayer.resi +0 -26
- package/src/Main.res +1 -14
- package/src/Main.res.mjs +3 -4
- package/src/Persistence.res +3 -23
- package/src/Persistence.res.mjs +3 -3
- package/src/Rollback.res +0 -1
- package/src/Rollback.res.mjs +1 -1
- package/src/RunScope.res +0 -3
- package/src/SafeCheckpointTracking.res +4 -4
- package/src/SafeCheckpointTracking.res.mjs +1 -1
- package/src/SimulateItems.res +26 -42
- package/src/SimulateItems.res.mjs +16 -40
- package/src/StagedRows.res +106 -0
- package/src/StagedRows.res.mjs +102 -0
- package/src/Staging.res +32 -14
- package/src/Staging.res.mjs +17 -20
- package/src/Storage.res +277 -998
- package/src/Storage.res.mjs +216 -731
- package/src/Supervisor.res +75 -56
- package/src/Supervisor.res.mjs +55 -26
- package/src/TestIndexer.res +22 -27
- package/src/TestIndexer.res.mjs +20 -17
- package/src/Throttler.res +15 -8
- package/src/Throttler.res.mjs +9 -6
- package/src/Throttler.resi +1 -1
- package/src/UserContext.res +46 -410
- package/src/UserContext.res.mjs +30 -318
- package/src/Utils.res +0 -23
- package/src/Utils.res.mjs +1 -17
- package/src/Writing.res +0 -1
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +0 -322
- package/src/bindings/ClickHouse.res.mjs +0 -338
- package/src/bindings/StorageClient.res +107 -175
- package/src/bindings/StorageClient.res.mjs +19 -8
- package/src/bindings/Vitest.res +0 -26
- package/src/bindings/Vitest.res.mjs +0 -3
- package/src/db/Table.res +24 -150
- package/src/db/Table.res.mjs +38 -272
- package/src/sources/AddressSet.res +0 -2
- package/src/sources/AddressStore.res +0 -3
- package/src/sources/ChainSources.res +2 -0
- package/src/sources/ChainSources.res.mjs +2 -2
- package/src/sources/EvmChain.res +4 -7
- package/src/sources/EvmChain.res.mjs +5 -4
- package/src/sources/EvmHyperSyncSource.res +2 -2
- package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
- package/src/sources/EvmRpcClient.res +13 -3
- package/src/sources/EvmRpcClient.res.mjs +4 -3
- package/src/sources/EvmRpcWs.res +4 -3
- package/src/sources/EvmRpcWs.res.mjs +2 -2
- package/src/sources/HeightFeed.res +31 -10
- package/src/sources/HeightFeed.res.mjs +10 -6
- package/src/sources/HeightStream.res +32 -17
- package/src/sources/HeightStream.res.mjs +10 -10
- package/src/sources/HyperSync.res +1 -2
- package/src/sources/HyperSync.res.mjs +1 -2
- package/src/sources/HyperSyncSSE.res +4 -6
- package/src/sources/HyperSyncSSE.res.mjs +2 -2
- package/src/sources/RpcSource.res +10 -1
- package/src/sources/RpcSource.res.mjs +4 -3
- package/src/sources/Source.res +3 -0
- package/src/sources/SourceManager.res +85 -104
- package/src/sources/SourceManager.res.mjs +18 -37
- package/src/sources/SourceManager.resi +3 -5
- package/src/sources/StartBlockResolver.res +1 -0
- package/src/sources/StartBlockResolver.res.mjs +4 -3
- package/src/sources/SvmHyperSyncSource.res +2 -2
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
- package/svm.schema.json +3 -3
- package/src/EffectCacheLookup.res +0 -98
- package/src/EffectCacheLookup.res.mjs +0 -116
- package/src/PgWriting.res +0 -36
- package/src/PgWriting.res.mjs +0 -34
- package/src/PreloadLookahead.res +0 -184
- package/src/PreloadLookahead.res.mjs +0 -197
- package/src/SubgraphLog.res +0 -19
- package/src/SubgraphLog.res.mjs +0 -26
- package/src/SyncWrites.res +0 -101
- package/src/SyncWrites.res.mjs +0 -98
- package/src/db/IndexDefinition.res +0 -47
- package/src/db/IndexDefinition.res.mjs +0 -60
- package/src/sources/CallSource.res +0 -274
- package/src/sources/CallSource.res.mjs +0 -196
- package/src/subgraph/abi-types.ts +0 -84
- package/src/subgraph/aggregations.ts +0 -745
- package/src/subgraph/as-package.ts +0 -143
- package/src/subgraph/assemblyscript-regex.ts +0 -67
- package/src/subgraph/assemblyscript.ts +0 -731
- package/src/subgraph/blocks.ts +0 -398
- package/src/subgraph/calls-source.ts +0 -702
- package/src/subgraph/calls.ts +0 -258
- package/src/subgraph/conformance.ts +0 -106
- package/src/subgraph/errors.ts +0 -85
- package/src/subgraph/graft.ts +0 -73
- package/src/subgraph/graph-cli-worker.mjs +0 -35
- package/src/subgraph/graph-cli.ts +0 -900
- package/src/subgraph/graph-node-decimal.ts +0 -199
- package/src/subgraph/graph-ts-enums.ts +0 -28
- package/src/subgraph/graph-ts-types/VERSION +0 -2
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +0 -70
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +0 -327
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +0 -233
- package/src/subgraph/graph-ts-types/chain/near.d.ts +0 -253
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +0 -32
- package/src/subgraph/graph-ts-types/common/collections.d.ts +0 -136
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +0 -11
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +0 -30
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +0 -17
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/value.d.ts +0 -120
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +0 -90
- package/src/subgraph/graph-ts-types/global/global.d.ts +0 -194
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +0 -22
- package/src/subgraph/graph-ts-types/index.d.ts +0 -102
- package/src/subgraph/graph-ts.ts +0 -4280
- package/src/subgraph/hosts.ts +0 -554
- package/src/subgraph/log.ts +0 -28
- package/src/subgraph/receipts.ts +0 -184
- package/src/subgraph/register-slots.ts +0 -93
- package/src/subgraph/rollup.ts +0 -109
- package/src/subgraph/rpc.ts +0 -337
- package/src/subgraph/runtime.ts +0 -2060
- package/src/subgraph/scope.ts +0 -177
- package/src/subgraph/stats.ts +0 -42
- package/src/subgraph/template-instances.ts +0 -66
- package/src/subgraph/toolchain.ts +0 -327
- package/src/subgraph/transient.ts +0 -176
package/src/Storage.res
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
let formatSeconds = seconds => (Math.round(seconds *. 100.) /. 100.)->Float.toString
|
|
2
|
-
|
|
3
|
-
// Every index build blocks writes to its table for as long as it runs, and on a
|
|
4
|
-
// large database that is not quick. Both build paths say so up front, so a
|
|
5
|
-
// stalled-looking indexer is explainable from the logs alone.
|
|
6
|
-
let slowOnLargeDatabaseNotice = "This can take a long time on a large database."
|
|
7
|
-
|
|
8
1
|
let storeName = (store: StorageClient.store) =>
|
|
9
2
|
switch store {
|
|
10
3
|
| Postgres => "postgres"
|
|
@@ -13,88 +6,11 @@ let storeName = (store: StorageClient.store) =>
|
|
|
13
6
|
|
|
14
7
|
let postgresName = Postgres->storeName
|
|
15
8
|
|
|
16
|
-
let logIndexEvent = (~logger, ~pgSchema, event: StorageClient.indexEvent) =>
|
|
17
|
-
switch event {
|
|
18
|
-
| Invalid({names}) =>
|
|
19
|
-
logger->Logger.warn(
|
|
20
|
-
`Ignoring invalid PostgreSQL indexes in schema "${pgSchema}". They can't serve queries, so the indexer builds its own alongside them.`,
|
|
21
|
-
~params={"storage": postgresName, "indexes": names},
|
|
22
|
-
)
|
|
23
|
-
| Planned({declared, missing, rebuilt}) =>
|
|
24
|
-
if rebuilt->Utils.Array.notEmpty {
|
|
25
|
-
logger->Logger.warn(
|
|
26
|
-
`PostgreSQL reports ${rebuilt
|
|
27
|
-
->Array.length
|
|
28
|
-
->Int.toString} of the indexer's own indexes as invalid, so they can't serve queries. Rebuilding them.`,
|
|
29
|
-
~params={"storage": postgresName, "indexes": rebuilt},
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
switch missing {
|
|
33
|
-
// The line that matters next is the indexer reporting itself ready, which
|
|
34
|
-
// finalization logs.
|
|
35
|
-
| [] =>
|
|
36
|
-
logger->Logger.info(
|
|
37
|
-
`All ${declared->Int.toString} schema indexes are already in place. Marking the indexer ready.`,
|
|
38
|
-
~params={"storage": postgresName},
|
|
39
|
-
)
|
|
40
|
-
| _ =>
|
|
41
|
-
logger->Logger.info(
|
|
42
|
-
`Creating the ${missing
|
|
43
|
-
->Array.length
|
|
44
|
-
->Int.toString} remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ${slowOnLargeDatabaseNotice}`,
|
|
45
|
-
~params={"storage": postgresName, "indexes": missing},
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
| Building({purpose: Query, name, tableName, isRebuild}) =>
|
|
49
|
-
logger->Logger.info(
|
|
50
|
-
`${isRebuild
|
|
51
|
-
? "Rebuilding unusable index"
|
|
52
|
-
: "Creating index"} "${name}" to serve a getWhere query on "${tableName}". Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
|
|
53
|
-
~params={"storage": postgresName},
|
|
54
|
-
)
|
|
55
|
-
| Building({purpose: Schema, name, isRebuild}) =>
|
|
56
|
-
logger->Logger.info(
|
|
57
|
-
`${isRebuild
|
|
58
|
-
? "Rebuilding unusable index"
|
|
59
|
-
: "Creating missing index"} "${name}" the schema promises but the database no longer has. Writes to the table are paused until it completes. ${slowOnLargeDatabaseNotice}`,
|
|
60
|
-
~params={"storage": postgresName},
|
|
61
|
-
)
|
|
62
|
-
| Built({purpose, name, seconds}) =>
|
|
63
|
-
logger->Logger.info(
|
|
64
|
-
`Index "${name}" is ready after ${seconds->formatSeconds}s.${switch purpose {
|
|
65
|
-
| Query => " Resuming indexing."
|
|
66
|
-
| Schema => ""
|
|
67
|
-
}}`,
|
|
68
|
-
~params={"storage": postgresName},
|
|
69
|
-
)
|
|
70
|
-
| Failed({purpose: Query, tableName, columns, error, ?code}) =>
|
|
71
|
-
logger->Logger.warn(
|
|
72
|
-
`Failed to create an index on "${tableName}"(${columns
|
|
73
|
-
->Array.map(column => `"${column}"`)
|
|
74
|
-
->Array.joinUnsafe(", ")}) for a getWhere query. The query runs without it.`,
|
|
75
|
-
~params={"storage": postgresName, "err": error, "code": code},
|
|
76
|
-
)
|
|
77
|
-
| Failed({purpose: Schema, name, error, ?code}) =>
|
|
78
|
-
logger->Logger.warn(
|
|
79
|
-
`Failed to restore the schema index "${name}". Queries relying on it run unindexed until the next restart.`,
|
|
80
|
-
~params={"storage": postgresName, "err": error, "code": code},
|
|
81
|
-
)
|
|
82
|
-
| ResyncFailed({name, error, ?code}) =>
|
|
83
|
-
logger->Logger.trace(
|
|
84
|
-
`Could not re-read the index "${name}" after a failed build. The next attempt reads it again.`,
|
|
85
|
-
~params={"storage": postgresName, "err": error, "code": code},
|
|
86
|
-
)
|
|
87
|
-
| Committed({count, seconds}) =>
|
|
88
|
-
logger->Logger.info(
|
|
89
|
-
`Committed ${count->Int.toString} schema indexes and the ready timestamp in ${seconds->formatSeconds}s.`,
|
|
90
|
-
~params={"storage": postgresName},
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
9
|
let postgresOptions = (
|
|
95
10
|
~pgSchema=Env.Db.publicSchema,
|
|
96
11
|
~isHasuraEnabled=false,
|
|
97
12
|
~maxConnections=Env.Db.maxConnections,
|
|
13
|
+
~isolated=false,
|
|
98
14
|
): StorageClient.postgresOptions => {
|
|
99
15
|
{
|
|
100
16
|
host: Env.Db.host,
|
|
@@ -111,456 +27,27 @@ let postgresOptions = (
|
|
|
111
27
|
// Hasura cannot read a `numeric[]`, so a schema it tracks stores those as
|
|
112
28
|
// `text[]` (issue #788).
|
|
113
29
|
isHasuraEnabled,
|
|
30
|
+
isolated,
|
|
114
31
|
}
|
|
115
32
|
}
|
|
116
33
|
|
|
117
|
-
// One client per storage: the
|
|
118
|
-
//
|
|
34
|
+
// One client per storage: the config it was created with decides every table
|
|
35
|
+
// and every statement the addon builds. Without one it keeps no entities.
|
|
119
36
|
let makeClient = (
|
|
120
37
|
~logger: Logger.t,
|
|
121
|
-
~
|
|
38
|
+
~config: option<Config.t>=?,
|
|
122
39
|
~postgres: StorageClient.postgresOptions,
|
|
123
40
|
~clickhouse: option<StorageClient.clickHouseOptions>=?,
|
|
124
41
|
): StorageClient.t =>
|
|
125
42
|
StorageClient.make(
|
|
126
|
-
{
|
|
127
|
-
~
|
|
128
|
-
~
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
// A per-chain entity's rows are partitioned by the chain that owns them, so a
|
|
132
|
-
// chain-filtered read scans one chain's partition rather than the whole table.
|
|
133
|
-
// `$` can't occur in a GraphQL entity name, so a partition name can never
|
|
134
|
-
// collide with the table another entity claims; past the identifier limit the
|
|
135
|
-
// entity index keeps what survives truncation unique.
|
|
136
|
-
let partitionTableName = (~entityConfig: Internal.entityConfig, ~chainId: ChainId.t) => {
|
|
137
|
-
let chainIdStr = chainId->ChainId.toString
|
|
138
|
-
Table.fitPgTableName(
|
|
139
|
-
`${entityConfig.table.tableName}$${chainIdStr}`,
|
|
140
|
-
~uniqueSuffix=`$${entityConfig.index->Int.toString}$${chainIdStr}`,
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// The physical tables an index on the entity is built on. A per-chain entity's
|
|
145
|
-
// rows are partitioned by chain, and an index declared on the parent cascades
|
|
146
|
-
// to every partition, which is what a run driving every chain wants: one
|
|
147
|
-
// declaration, one build. An isolated run instead builds on its own chains'
|
|
148
|
-
// partitions only: the planner uses a partition's own index either way, and
|
|
149
|
-
// building one chain's index then neither waits for nor locks the rows of a
|
|
150
|
-
// chain a sibling process drives.
|
|
151
|
-
let indexTableNames = (entityConfig: Internal.entityConfig, ~partitionChainIds) =>
|
|
152
|
-
switch (entityConfig.table->Table.getChainIdField, partitionChainIds) {
|
|
153
|
-
| (Some(_), Some(chainIds)) =>
|
|
154
|
-
chainIds->Array.map(chainId => partitionTableName(~entityConfig, ~chainId))
|
|
155
|
-
| _ => [entityConfig.table.tableName]
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Every index the entity schema promises: an `@index` field, a composite index,
|
|
159
|
-
// or the index backing a derived relationship. Deferred past the initial DDL
|
|
160
|
-
// and created once backfill completes, so a chain that reports itself ready
|
|
161
|
-
// always has all of them. With `partitionChainIds`, a per-chain entity's index
|
|
162
|
-
// is one per partition of those chains rather than one on the parent.
|
|
163
|
-
//
|
|
164
|
-
// `entities` is the Postgres-backed set, and every `@derivedFrom` target within
|
|
165
|
-
// it resolves: config parsing rejects a Postgres entity deriving from one that
|
|
166
|
-
// isn't in Postgres (`validate_relationship_storage`).
|
|
167
|
-
let getSchemaIndexes = (
|
|
168
|
-
~entities: array<Internal.entityConfig>,
|
|
169
|
-
~partitionChainIds: option<array<ChainId.t>>=?,
|
|
170
|
-
): array<IndexDefinition.t> => {
|
|
171
|
-
let derivedSchema = Schema.make(entities->Array.map(e => e.table))
|
|
172
|
-
let all = []
|
|
173
|
-
|
|
174
|
-
entities->Array.forEach(({table}) => {
|
|
175
|
-
table
|
|
176
|
-
->Table.getSingleIndexes
|
|
177
|
-
->Array.forEach(column =>
|
|
178
|
-
all->Array.push(IndexDefinition.single(~tableName=table.tableName, ~column))->ignore
|
|
179
|
-
)
|
|
180
|
-
table
|
|
181
|
-
->Table.getCompositeIndexes
|
|
182
|
-
->Array.forEach(indexFields =>
|
|
183
|
-
all
|
|
184
|
-
->Array.push(IndexDefinition.fromIndexFields(~tableName=table.tableName, ~indexFields))
|
|
185
|
-
->ignore
|
|
186
|
-
)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
entities->Array.forEach(({table}) =>
|
|
190
|
-
table
|
|
191
|
-
->Table.getDerivedFromFields
|
|
192
|
-
->Array.forEach(derivedFromField => {
|
|
193
|
-
let column =
|
|
194
|
-
derivedSchema->Schema.getDerivedFromPgFieldName(derivedFromField)->Utils.unwrapResultExn
|
|
195
|
-
// A list column holds every id in one value; a btree entry over it
|
|
196
|
-
// outgrows a page (Hats' `wearers`), so it gets GIN, as graph-node gives
|
|
197
|
-
// list attributes.
|
|
198
|
-
let isList =
|
|
199
|
-
derivedSchema
|
|
200
|
-
->Utils.Dict.dangerouslyGetNonOption(derivedFromField.derivedFromEntity)
|
|
201
|
-
->Option.flatMap(table => table->Table.getFieldByName(derivedFromField.derivedFromField))
|
|
202
|
-
->Option.mapOr(
|
|
203
|
-
false,
|
|
204
|
-
field =>
|
|
205
|
-
switch field {
|
|
206
|
-
| Table.Field({isArray}) => isArray
|
|
207
|
-
| DerivedFrom(_) => false
|
|
208
|
-
},
|
|
209
|
-
)
|
|
210
|
-
all
|
|
211
|
-
->Array.push(
|
|
212
|
-
IndexDefinition.make(
|
|
213
|
-
~tableName=derivedFromField.derivedFromEntity,
|
|
214
|
-
~columns=[{name: column, direction: Table.Asc}],
|
|
215
|
-
~method=?isList ? Some("gin") : None,
|
|
216
|
-
),
|
|
217
|
-
)
|
|
218
|
-
->ignore
|
|
219
|
-
})
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
// An `@index` field and a derived relationship pointing at it describe the
|
|
223
|
-
// same index, so the list is deduped on identity rather than on name.
|
|
224
|
-
let seen = Utils.Set.make()
|
|
225
|
-
let entityByTableName = Dict.make()
|
|
226
|
-
entities->Array.forEach(entityConfig =>
|
|
227
|
-
entityByTableName->Dict.set(entityConfig.table.tableName, entityConfig)
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
all
|
|
231
|
-
->Array.filter(definition => {
|
|
232
|
-
let identity = definition->IndexDefinition.describe
|
|
233
|
-
if seen->Utils.Set.has(identity) {
|
|
234
|
-
false
|
|
235
|
-
} else {
|
|
236
|
-
seen->Utils.Set.add(identity)->ignore
|
|
237
|
-
true
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
->Array.flatMap(definition =>
|
|
241
|
-
entityByTableName
|
|
242
|
-
->Dict.get(definition.tableName)
|
|
243
|
-
->Option.getOrThrow
|
|
244
|
-
->indexTableNames(~partitionChainIds)
|
|
245
|
-
->Array.map(tableName => {...definition, IndexDefinition.tableName})
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
let pgColumnInput = (field: Table.field): Core.pgColumnInput => {
|
|
250
|
-
let (fieldType, precision, scale, enumName) = field.fieldType->Table.pgFieldTypeParts
|
|
251
|
-
{
|
|
252
|
-
name: field->Table.getPgDbFieldName,
|
|
253
|
-
fieldType,
|
|
254
|
-
isArray: field.isArray,
|
|
255
|
-
isNullable: field.isNullable,
|
|
256
|
-
isPrimaryKey: field.isPrimaryKey,
|
|
257
|
-
defaultValue: ?field.defaultValue,
|
|
258
|
-
?precision,
|
|
259
|
-
?scale,
|
|
260
|
-
?enumName,
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// The entity as it's stored: the handler-visible schema plus the chain-id
|
|
265
|
-
// column a per-chain entity's table carries. The value for that column is
|
|
266
|
-
// stamped from the flush group's scope right before serialization, so it never
|
|
267
|
-
// has to be re-derived from a checkpoint.
|
|
268
|
-
let rowSchemaCache = Utils.WeakMap.make()
|
|
269
|
-
let getRowSchema = (entityConfig: Internal.entityConfig): S.t<Internal.entity> =>
|
|
270
|
-
switch rowSchemaCache->Utils.WeakMap.get(entityConfig) {
|
|
271
|
-
| Some(cached) => cached
|
|
272
|
-
| None =>
|
|
273
|
-
let schema = switch entityConfig.table->Table.getChainIdField {
|
|
274
|
-
| None => entityConfig.schema
|
|
275
|
-
| Some(chainIdField) =>
|
|
276
|
-
S.schema(s => {
|
|
277
|
-
let dict = Dict.make()
|
|
278
|
-
switch entityConfig.schema->S.classify {
|
|
279
|
-
| Object({items}) =>
|
|
280
|
-
items->Array.forEach(({location, schema}) => dict->Dict.set(location, s.matches(schema)))
|
|
281
|
-
| _ =>
|
|
282
|
-
JsError.throwWithMessage(
|
|
283
|
-
`Unexpected non-object schema for entity "${entityConfig.name}".`,
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
dict->Dict.set(chainIdField.fieldName, s.matches(ChainId.schema->S.toUnknown))
|
|
287
|
-
dict
|
|
288
|
-
})->(Utils.magic: S.t<dict<unknown>> => S.t<Internal.entity>)
|
|
289
|
-
}
|
|
290
|
-
rowSchemaCache->Utils.WeakMap.set(entityConfig, schema)->ignore
|
|
291
|
-
schema
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// How a batch of a Postgres table's rows crosses over: staged into an arena
|
|
295
|
-
// column by column, or rendered a parameter per cell when it has an array
|
|
296
|
-
// column.
|
|
297
|
-
type postgresTable = {staged: option<array<Staging.column>>}
|
|
298
|
-
|
|
299
|
-
// A table as the addon writes it: its handle, the columns a batch's rows carry
|
|
300
|
-
// in the order the addon expects them, and what each store that keeps it made
|
|
301
|
-
// of it.
|
|
302
|
-
type registered = {
|
|
303
|
-
handle: int,
|
|
304
|
-
home: StorageClient.store,
|
|
305
|
-
table: Table.table,
|
|
306
|
-
fields: array<Table.field>,
|
|
307
|
-
postgres: option<postgresTable>,
|
|
308
|
-
clickhouse: option<ClickHouse.table>,
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
%%private(
|
|
312
|
-
let register = (
|
|
313
|
-
client: StorageClient.t,
|
|
314
|
-
~table: Table.table,
|
|
315
|
-
~itemSchema: S.t<unknown>,
|
|
316
|
-
~postgres=true,
|
|
317
|
-
~clickhouse: option<StorageClient.clickHouseTable>=?,
|
|
318
|
-
~appendOnly=false,
|
|
319
|
-
~history=?,
|
|
320
|
-
): registered => {
|
|
321
|
-
let fields = table->Table.schemaOrderedFields(~schema=itemSchema)
|
|
322
|
-
let chainIdField = table->Table.getChainIdField
|
|
323
|
-
let registered = client->StorageClient.registerTable({
|
|
324
|
-
name: table.tableName,
|
|
325
|
-
postgres: ?(
|
|
326
|
-
postgres
|
|
327
|
-
? Some({
|
|
328
|
-
Core.columns: table->Table.getFields->Array.map(pgColumnInput),
|
|
329
|
-
writeColumns: fields->Array.map(Table.getPgDbFieldName),
|
|
330
|
-
partitionByColumn: ?(chainIdField->Option.map(Table.getPgDbFieldName)),
|
|
331
|
-
appendOnly,
|
|
332
|
-
historyTable: ?history,
|
|
333
|
-
chainIdColumn: ?switch history {
|
|
334
|
-
| Some(_) => chainIdField->Option.map(Table.getPgDbFieldName)
|
|
335
|
-
| None => None
|
|
336
|
-
},
|
|
337
|
-
})
|
|
338
|
-
: None
|
|
339
|
-
),
|
|
340
|
-
?clickhouse,
|
|
341
|
-
})
|
|
342
|
-
{
|
|
343
|
-
handle: registered.handle,
|
|
344
|
-
home: registered.home,
|
|
345
|
-
table,
|
|
346
|
-
fields,
|
|
347
|
-
postgres: registered.postgres->Option.map(({?kinds}): postgresTable => {
|
|
348
|
-
staged: kinds->Option.map(kinds =>
|
|
349
|
-
PgWriting.columns(fields, ~kinds=kinds->Array.map(Staging.kindOfOrdinal))
|
|
350
|
-
),
|
|
351
|
-
}),
|
|
352
|
-
// Named for where its rows go, which for an entity is its history table.
|
|
353
|
-
clickhouse: registered.clickhouse->Option.map(
|
|
354
|
-
ClickHouse.makeTable(
|
|
355
|
-
~handle=registered.handle,
|
|
356
|
-
~name=history->Option.getOr(table.tableName),
|
|
357
|
-
_,
|
|
358
|
-
),
|
|
359
|
-
),
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
let registerEntity = (client, entityConfig: Internal.entityConfig) =>
|
|
365
|
-
client->register(
|
|
366
|
-
~table=entityConfig.table,
|
|
367
|
-
~itemSchema=entityConfig->getRowSchema->S.toUnknown,
|
|
368
|
-
~postgres=entityConfig.storage.postgres,
|
|
369
|
-
~clickhouse=?entityConfig.storage.clickhouse
|
|
370
|
-
? Some(ClickHouse.entityTable(entityConfig))
|
|
371
|
-
: None,
|
|
372
|
-
~history=EntityHistory.historyTableName(
|
|
373
|
-
~entityName=entityConfig.name,
|
|
374
|
-
~entityIndex=entityConfig.index,
|
|
375
|
-
),
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
// A json column holds a document, which is as readily a string or a boolean as
|
|
379
|
-
// an object. By the time a parameter is rendered there is only the value to go
|
|
380
|
-
// on, and there a string is text and a boolean is `t` — neither of which the
|
|
381
|
-
// server will read back as the document it was. So the document becomes its own
|
|
382
|
-
// text here, where the column it belongs to is still known.
|
|
383
|
-
//
|
|
384
|
-
// JSON `null` is a document of its own, which a required column stores as one.
|
|
385
|
-
// A column that takes NULL keeps the two apart the other way: there `null` is
|
|
386
|
-
// the absent value, since only one of the two can survive the field's schema.
|
|
387
|
-
%%private(
|
|
388
|
-
let renderDocument = (value, ~isNullable) =>
|
|
389
|
-
switch value->(Utils.magic: unknown => Nullable.t<unknown>) {
|
|
390
|
-
| Value(_) => value->Sql.stringifyDocument->(Utils.magic: string => unknown)
|
|
391
|
-
| Null if !isNullable => "null"->(Utils.magic: string => unknown)
|
|
392
|
-
| Null | Undefined => value
|
|
393
|
-
}
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
// A list of documents is bound as an array whose elements are each their own
|
|
397
|
-
// document's text.
|
|
398
|
-
%%private(
|
|
399
|
-
let renderDocuments = (columns: array<array<unknown>>, ~at: array<(int, Table.field)>) => {
|
|
400
|
-
at->Array.forEach(((index, field)) => {
|
|
401
|
-
let values = columns->Array.getUnsafe(index)
|
|
402
|
-
for row in 0 to values->Array.length - 1 {
|
|
403
|
-
let value = values->Array.getUnsafe(row)
|
|
404
|
-
values->Array.setUnsafe(
|
|
405
|
-
row,
|
|
406
|
-
switch value->(Utils.magic: unknown => Nullable.t<array<unknown>>)->Nullable.toOption {
|
|
407
|
-
| Some(documents) if field.isArray =>
|
|
408
|
-
documents
|
|
409
|
-
->Array.map(document => document->renderDocument(~isNullable=true))
|
|
410
|
-
->(Utils.magic: array<unknown> => unknown)
|
|
411
|
-
| _ => value->renderDocument(~isNullable=field.isNullable)
|
|
412
|
-
},
|
|
413
|
-
)
|
|
414
|
-
}
|
|
415
|
-
})
|
|
416
|
-
columns
|
|
417
|
-
}
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
// An item schema compiled for one table's columns: rows in, one array per
|
|
421
|
-
// column out. The values are validated by the type system on the way in, and a
|
|
422
|
-
// value its column cannot take is refused by the server, which says why.
|
|
423
|
-
type converters = Utils.WeakMap.t<S.t<unknown>, array<unknown> => array<array<unknown>>>
|
|
424
|
-
let arenaConverters: converters = Utils.WeakMap.make()
|
|
425
|
-
let cellConverters: converters = Utils.WeakMap.make()
|
|
426
|
-
let converterOf = (registered, ~forArena, ~itemSchema) => {
|
|
427
|
-
let converters = forArena ? arenaConverters : cellConverters
|
|
428
|
-
switch converters->Utils.WeakMap.get(itemSchema) {
|
|
429
|
-
| Some(convert) => convert
|
|
430
|
-
| None =>
|
|
431
|
-
let documents =
|
|
432
|
-
registered.fields->Array.filterMapWithIndex((field, index) =>
|
|
433
|
-
field.fieldType === Table.Json ? Some((index, field)) : None
|
|
434
|
-
)
|
|
435
|
-
// Booleans travel as 1/0 and bigints as their digits, which is what an
|
|
436
|
-
// arena's slots hold.
|
|
437
|
-
let schema = forArena ? registered.table->Table.toDbSchema(~schema=itemSchema) : itemSchema
|
|
438
|
-
let convert = S.compile(
|
|
439
|
-
S.unnest(schema)
|
|
440
|
-
->S.preprocess(_ => {
|
|
441
|
-
serializer: columns =>
|
|
442
|
-
columns
|
|
443
|
-
->(Utils.magic: unknown => array<array<unknown>>)
|
|
444
|
-
->renderDocuments(~at=documents)
|
|
445
|
-
->(Utils.magic: array<array<unknown>> => unknown),
|
|
446
|
-
})
|
|
447
|
-
->S.toUnknown,
|
|
448
|
-
~input=Value,
|
|
449
|
-
~output=Unknown,
|
|
450
|
-
~mode=Sync,
|
|
451
|
-
~typeValidation=false,
|
|
452
|
-
)->(Utils.magic: (unknown => unknown) => array<unknown> => array<array<unknown>>)
|
|
453
|
-
converters->Utils.WeakMap.set(itemSchema, convert)->ignore
|
|
454
|
-
convert
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Staging refuses a non-finite float for a staged table. A table bound a cell
|
|
459
|
-
// at a time has to refuse it too: the server would store it, and the field's
|
|
460
|
-
// schema would then refuse it on every load.
|
|
461
|
-
%%private(
|
|
462
|
-
let refuseNonFinite = (values: array<array<unknown>>, ~fields: array<Table.field>) =>
|
|
463
|
-
fields->Array.forEachWithIndex((field, index) =>
|
|
464
|
-
if field.fieldType === Table.Number {
|
|
465
|
-
let column = field->Table.getPgDbFieldName
|
|
466
|
-
let check = (value: Nullable.t<float>) =>
|
|
467
|
-
switch value {
|
|
468
|
-
| Value(number) => number->Staging.finiteOrThrow(~column)->ignore
|
|
469
|
-
| Null | Undefined => ()
|
|
470
|
-
}
|
|
471
|
-
values
|
|
472
|
-
->Array.getUnsafe(index)
|
|
473
|
-
->Array.forEach(value =>
|
|
474
|
-
if field.isArray {
|
|
475
|
-
switch value->(Utils.magic: unknown => Nullable.t<array<Nullable.t<float>>>) {
|
|
476
|
-
| Value(elements) => elements->Array.forEach(check)
|
|
477
|
-
| Null | Undefined => ()
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
check(value->(Utils.magic: unknown => Nullable.t<float>))
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
}
|
|
484
|
-
)
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
// What a batch has laid out on its way to the addon: the staged handles, freed
|
|
488
|
-
// if the write never reaches it, and each store's time spent laying its rows
|
|
489
|
-
// out, which is part of writing to it.
|
|
490
|
-
type staged = {handles: array<int>, seconds: dict<float>}
|
|
491
|
-
|
|
492
|
-
let stagedFor = (staged, ~store, stage) => {
|
|
493
|
-
let started = Performance.now()
|
|
494
|
-
let result = stage()
|
|
495
|
-
let name = store->storeName
|
|
496
|
-
staged.seconds->Dict.set(
|
|
497
|
-
name,
|
|
498
|
-
staged.seconds->Dict.get(name)->Option.getOr(0.) +. started->Performance.secondsSince,
|
|
43
|
+
{postgres, ?clickhouse},
|
|
44
|
+
~configJson=?config->Option.map(config => config.envioInfo->JSON.stringify),
|
|
45
|
+
~logger,
|
|
499
46
|
)
|
|
500
|
-
result
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// A Postgres table's rows for a write. A staged batch is handed back by handle.
|
|
504
|
-
%%private(
|
|
505
|
-
let rowsOrThrow = (
|
|
506
|
-
client: StorageClient.t,
|
|
507
|
-
registered,
|
|
508
|
-
~itemSchema,
|
|
509
|
-
items: array<unknown>,
|
|
510
|
-
~staged: staged,
|
|
511
|
-
): StorageClient.rows =>
|
|
512
|
-
staged->stagedFor(~store=Postgres, () => {
|
|
513
|
-
let rows = items->Array.length
|
|
514
|
-
try {
|
|
515
|
-
switch registered.postgres {
|
|
516
|
-
| Some({staged: Some(columns)}) =>
|
|
517
|
-
let values = (registered->converterOf(~forArena=true, ~itemSchema))(items)
|
|
518
|
-
let handle =
|
|
519
|
-
client
|
|
520
|
-
->StorageClient.arena(~store=Postgres)
|
|
521
|
-
->PgWriting.stage(~table=registered.handle, ~columns, ~values, ~rows)
|
|
522
|
-
staged.handles->Array.push(handle)
|
|
523
|
-
({staged: handle, rows}: StorageClient.rows)
|
|
524
|
-
| Some({staged: None}) =>
|
|
525
|
-
let values = (registered->converterOf(~forArena=false, ~itemSchema))(items)
|
|
526
|
-
values->refuseNonFinite(~fields=registered.fields)
|
|
527
|
-
{
|
|
528
|
-
cells: values->Utils.Array.flatten->Sql.params,
|
|
529
|
-
rows,
|
|
530
|
-
}
|
|
531
|
-
| None => JsError.throwWithMessage(`Postgres doesn't keep "${registered.table.tableName}"`)
|
|
532
|
-
}
|
|
533
|
-
} catch {
|
|
534
|
-
| S.Raised(_) as exn =>
|
|
535
|
-
throw(
|
|
536
|
-
Persistence.StorageError({
|
|
537
|
-
message: `Failed to convert items for table "${registered.table.tableName}"`,
|
|
538
|
-
reason: exn,
|
|
539
|
-
}),
|
|
540
|
-
)
|
|
541
|
-
| exn =>
|
|
542
|
-
throw(
|
|
543
|
-
Persistence.StorageError({
|
|
544
|
-
message: `Failed to insert items into table "${registered.table.tableName}"`,
|
|
545
|
-
reason: exn->Utils.prettifyExn,
|
|
546
|
-
}),
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
})
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
// Ids as the text their column reads them from.
|
|
553
|
-
let renderIds = (table: Table.table, ids: array<EntityId.t>) =>
|
|
554
|
-
table
|
|
555
|
-
->Table.encodeIdsToJson(ids)
|
|
556
|
-
->(Utils.magic: JSON.t => array<unknown>)
|
|
557
|
-
->Array.map(Sql.render)
|
|
558
47
|
|
|
559
|
-
let bounds = (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
let (chainIds, checkpointIds) = byChain->Frontier.unnestParams
|
|
563
|
-
{sequence, chainIds, checkpointIds}
|
|
48
|
+
let bounds = ({byChain}: CheckpointSequence.checkpointBoundsByChain): StorageClient.bounds => {
|
|
49
|
+
chainIds: byChain->Frontier.chainIds,
|
|
50
|
+
checkpointIds: byChain->Frontier.chainIds->Array.map(chainId => byChain->Frontier.get(chainId)),
|
|
564
51
|
}
|
|
565
52
|
|
|
566
53
|
let progress = (chain: InternalTable.Chains.progressedChain): StorageClient.progress => {
|
|
@@ -604,16 +91,6 @@ let storageErrorOf = exn =>
|
|
|
604
91
|
| _ => exn
|
|
605
92
|
}
|
|
606
93
|
|
|
607
|
-
let rollbackRowStateSchema: Table.table => S.t<(
|
|
608
|
-
EntityId.t,
|
|
609
|
-
EntityHistory.RowAction.t,
|
|
610
|
-
)> = Utils.WeakMap.memoize(table =>
|
|
611
|
-
S.object(s => (
|
|
612
|
-
s.field(Table.idFieldName, table->Table.getIdSchema),
|
|
613
|
-
s.field(EntityHistory.changeFieldName, EntityHistory.RowAction.schema),
|
|
614
|
-
))
|
|
615
|
-
)
|
|
616
|
-
|
|
617
94
|
// The chain a rollback row belongs to, read from the chain-id column both
|
|
618
95
|
// rollback queries select. None for a cross-chain entity, which has no column.
|
|
619
96
|
let rollbackChainIdSchema: Table.table => option<S.t<ChainId.t>> = Utils.WeakMap.memoize(table =>
|
|
@@ -629,41 +106,96 @@ let rollbackRemovedIdSchema: Table.table => S.t<EntityId.t> = Utils.WeakMap.memo
|
|
|
629
106
|
S.object(s => s.field(Table.idFieldName, table->Table.getIdSchema))
|
|
630
107
|
)
|
|
631
108
|
|
|
109
|
+
// A change set is the entity's fields, then each change's checkpoint and 1
|
|
110
|
+
// for a change that deletes the entity.
|
|
111
|
+
let changeColumns: array<Staging.column> = [
|
|
112
|
+
{name: EntityHistory.checkpointIdFieldName, kind: I64, isNullable: false},
|
|
113
|
+
{name: EntityHistory.changeFieldName, kind: F64, isNullable: false},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
// Fills a stage the addon lent for `rows`, handing it back once every value
|
|
117
|
+
// is in, or abandoning it if one is refused.
|
|
118
|
+
let stageOrThrow = (arena, ~columns, ~rows, fill) => {
|
|
119
|
+
let stage = arena->Staging.begin(~rows, ~columns)
|
|
120
|
+
try {
|
|
121
|
+
fill(stage)
|
|
122
|
+
stage->Staging.commit
|
|
123
|
+
} catch {
|
|
124
|
+
| exn =>
|
|
125
|
+
stage->Staging.abort
|
|
126
|
+
throw(exn)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rows of a fixed layout, each value read off the item by its column's name.
|
|
131
|
+
let stageRowsOrThrow = (arena, ~writers, items: array<dict<unknown>>) =>
|
|
132
|
+
arena->stageOrThrow(~columns=writers->StagedRows.columnsOf, ~rows=items->Array.length, stage =>
|
|
133
|
+
items->Array.forEachWithIndex((item, row) =>
|
|
134
|
+
writers->Array.forEachWithIndex(
|
|
135
|
+
({field}: StagedRows.writer, column) =>
|
|
136
|
+
stage->StagedRows.writeField(~writers, ~column, ~row, item->Dict.getUnsafe(field.name)),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// Every change a group made, in order: what a set wrote, or the id a delete
|
|
142
|
+
// removed. Each store reads what it keeps off the same rows.
|
|
143
|
+
let stageChangesOrThrow = (arena, ~writers, ~changes: array<Change.t<Internal.entity>>) => {
|
|
144
|
+
let fields = writers->Array.length
|
|
145
|
+
arena->stageOrThrow(
|
|
146
|
+
~columns=writers->StagedRows.columnsOf->Array.concat(changeColumns),
|
|
147
|
+
~rows=changes->Array.length,
|
|
148
|
+
stage =>
|
|
149
|
+
changes->Array.forEachWithIndex((change, row) => {
|
|
150
|
+
let entityId = change->Change.getEntityId->(Utils.magic: EntityId.t => unknown)
|
|
151
|
+
switch change {
|
|
152
|
+
| Set({entity}) =>
|
|
153
|
+
let values = entity->(Utils.magic: Internal.entity => dict<unknown>)
|
|
154
|
+
writers->Array.forEachWithIndex(
|
|
155
|
+
({field}: StagedRows.writer, column) =>
|
|
156
|
+
stage->StagedRows.writeField(
|
|
157
|
+
~writers,
|
|
158
|
+
~column,
|
|
159
|
+
~row,
|
|
160
|
+
field.name === Table.idFieldName ? entityId : values->Dict.getUnsafe(field.name),
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
| Delete(_) =>
|
|
164
|
+
writers->Array.forEachWithIndex(
|
|
165
|
+
({field}: StagedRows.writer, column) =>
|
|
166
|
+
if field.name === Table.idFieldName {
|
|
167
|
+
stage->StagedRows.writeField(~writers, ~column, ~row, entityId)
|
|
168
|
+
} else {
|
|
169
|
+
stage->Staging.writeDeletedValue(~column, ~row, %raw(`undefined`))
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
stage->Staging.writeValue(
|
|
174
|
+
~column=fields,
|
|
175
|
+
~row,
|
|
176
|
+
change->Change.getCheckpointId->(Utils.magic: bigint => unknown),
|
|
177
|
+
)
|
|
178
|
+
stage->Staging.writeValue(
|
|
179
|
+
~column=fields + 1,
|
|
180
|
+
~row,
|
|
181
|
+
switch change {
|
|
182
|
+
| Set(_) => 0.
|
|
183
|
+
| Delete(_) => 1.
|
|
184
|
+
}->(Utils.magic: float => unknown),
|
|
185
|
+
)
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
632
190
|
let make = (
|
|
633
191
|
~client: StorageClient.t,
|
|
634
|
-
~logger: Logger.t,
|
|
635
|
-
// The entities a load can name before initialize or resume has.
|
|
636
|
-
~entities: array<Internal.entityConfig>=[],
|
|
637
192
|
// Where the effect cache is dumped to and uploaded from.
|
|
638
193
|
~cacheDir: option<NodeJs.Path.t>=?,
|
|
639
194
|
// Decides how wide an address key is, both when the config's addresses are
|
|
640
195
|
// encoded at initialize and when stored rows are grouped on resume.
|
|
641
196
|
~ecosystem: Ecosystem.name,
|
|
642
|
-
// An `envio start --chain` process: builds and looks for a per-chain entity's
|
|
643
|
-
// indexes on its own chains' partitions, never on the parent table, so no
|
|
644
|
-
// build reaches into the rows a sibling process drives.
|
|
645
|
-
~isolated=false,
|
|
646
197
|
~onInitialize=?,
|
|
647
198
|
): Persistence.storage => {
|
|
648
|
-
// A subgraph's Graph endpoint serves while it syncs, as graph-node's does,
|
|
649
|
-
// and graph-node builds every index with the table: the schema's indexes
|
|
650
|
-
// are built at initialize rather than once the backfill is done.
|
|
651
|
-
let eagerSchemaIndexes = Env.subgraphMode.contents
|
|
652
|
-
// The subgraph runtime's caches whose rows can't go stale: a contract call,
|
|
653
|
-
// a balance or code at a block number, and a file by its content id.
|
|
654
|
-
// graph-node keeps its call cache when a subgraph is synced again from
|
|
655
|
-
// scratch; so does a reset here, and a resync asks an RPC only for what it
|
|
656
|
-
// never asked before.
|
|
657
|
-
let keptEffectCaches = Env.subgraphMode.contents
|
|
658
|
-
? Some([
|
|
659
|
-
"envio_subgraph_eth_call",
|
|
660
|
-
"envio_subgraph_eth_balance",
|
|
661
|
-
"envio_subgraph_has_code",
|
|
662
|
-
"envio_subgraph_file",
|
|
663
|
-
"envio_subgraph_ipfs_cat",
|
|
664
|
-
"envio_subgraph_arweave",
|
|
665
|
-
])
|
|
666
|
-
: None
|
|
667
199
|
let cacheDirPath = switch cacheDir {
|
|
668
200
|
| Some(cacheDir) => cacheDir
|
|
669
201
|
| None =>
|
|
@@ -674,148 +206,43 @@ let make = (
|
|
|
674
206
|
])
|
|
675
207
|
}
|
|
676
208
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
registry->Dict.set(table.tableName, registered)
|
|
686
|
-
registered
|
|
687
|
-
}
|
|
688
|
-
let entityTable = (entityConfig: Internal.entityConfig) =>
|
|
689
|
-
registered(~table=entityConfig.table, () => client->registerEntity(entityConfig))
|
|
690
|
-
let ownTable = (~table: Table.table, ~itemSchema: S.t<unknown>, ~appendOnly=?) =>
|
|
691
|
-
registered(~table, () => client->register(~table, ~itemSchema, ~appendOnly?))
|
|
692
|
-
let rawEventsTable = () =>
|
|
693
|
-
ownTable(
|
|
694
|
-
~table=InternalTable.RawEvents.table,
|
|
695
|
-
~itemSchema=InternalTable.RawEvents.schema->S.toUnknown,
|
|
696
|
-
~appendOnly=true,
|
|
697
|
-
)
|
|
698
|
-
// Every cache table has the same two columns, whatever its effect's output.
|
|
699
|
-
let cacheTable = (table: Table.table) =>
|
|
700
|
-
ownTable(~table, ~itemSchema=Internal.cacheItemSchema->S.toUnknown)
|
|
701
|
-
entities->Array.forEach(entityConfig => entityConfig->entityTable->ignore)
|
|
702
|
-
let tableOf = (table: Table.table) =>
|
|
703
|
-
switch registry->Utils.Dict.dangerouslyGetNonOption(table.tableName) {
|
|
704
|
-
| Some(registered) => registered
|
|
705
|
-
// An effect's cache is loaded before anything wrote or created it.
|
|
706
|
-
| None if Internal.EffectCache.fromTableName(table.tableName)->Option.isSome =>
|
|
707
|
-
cacheTable(table)
|
|
708
|
-
| None => JsError.throwWithMessage(`The storage has no table "${table.tableName}".`)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// A per-chain entity's Postgres rows live in one partition per chain.
|
|
712
|
-
let partitions = (entities: array<Internal.entityConfig>, ~chainIds) =>
|
|
713
|
-
entities
|
|
714
|
-
->Array.filter(entityConfig =>
|
|
715
|
-
entityConfig.storage.postgres && entityConfig.table->Table.getChainIdField->Option.isSome
|
|
716
|
-
)
|
|
717
|
-
->Array.flatMap(entityConfig =>
|
|
718
|
-
chainIds->Array.map((chainId): StorageClient.partition => {
|
|
719
|
-
table: (entityConfig->entityTable).handle,
|
|
720
|
-
chainId,
|
|
721
|
-
name: partitionTableName(~entityConfig, ~chainId),
|
|
722
|
-
})
|
|
723
|
-
)
|
|
209
|
+
let layouts = client->StorageClient.layouts
|
|
210
|
+
let entityIndexByName = Dict.make()
|
|
211
|
+
layouts.entities->Array.forEachWithIndex((layout, index) =>
|
|
212
|
+
entityIndexByName->Dict.set(layout.name, index)
|
|
213
|
+
)
|
|
214
|
+
let entityWriters = layouts.entities->Array.map(layout => layout.fields->StagedRows.writersOf)
|
|
215
|
+
let rawEventWriters = layouts.rawEvents->StagedRows.writersOf
|
|
216
|
+
let effectCacheWriters = layouts.effectCache->StagedRows.writersOf
|
|
724
217
|
|
|
725
218
|
let holding = () => client->StorageClient.holding
|
|
726
219
|
|
|
727
|
-
// Scans .envio/cache into a list of (cache table, absolute TSV path). Flat
|
|
728
|
-
// `<name>.tsv` files map to cross-chain caches; a numeric subdirectory
|
|
729
|
-
// `<chainId>/<name>.tsv` maps to a chain-scoped cache. Exactly one directory
|
|
730
|
-
// level is supported. A non-numeric directory that contains TSVs is rejected.
|
|
731
|
-
// Returns [] when .envio/cache doesn't exist.
|
|
732
|
-
let scanCacheDir = async () => {
|
|
733
|
-
let topEntries = try {
|
|
734
|
-
await NodeJs.Fs.Promises.readdir(cacheDirPath)
|
|
735
|
-
} catch {
|
|
736
|
-
| _ => []
|
|
737
|
-
}
|
|
738
|
-
let result = []
|
|
739
|
-
let _ = await topEntries
|
|
740
|
-
->Array.map(async entry => {
|
|
741
|
-
let entryPath = NodeJs.Path.join(cacheDirPath, entry)
|
|
742
|
-
let isDir = (await NodeJs.Fs.Promises.stat(entryPath))->NodeJs.Fs.Promises.statsIsDirectory
|
|
743
|
-
if isDir {
|
|
744
|
-
let subEntries = await NodeJs.Fs.Promises.readdir(entryPath)
|
|
745
|
-
let tsvs = subEntries->Array.filter(sub => sub->String.endsWith(".tsv"))
|
|
746
|
-
switch Internal.EffectCache.parseChainId(entry) {
|
|
747
|
-
| Some(chainId) =>
|
|
748
|
-
tsvs->Array.forEach(sub => {
|
|
749
|
-
let effectName = sub->String.slice(~start=0, ~end=-4)
|
|
750
|
-
let table = Internal.makeCacheTable(~effectName, ~scope=Chain(chainId))
|
|
751
|
-
result
|
|
752
|
-
->Array.push((table, NodeJs.Path.join(entryPath, sub)->NodeJs.Path.toString))
|
|
753
|
-
->ignore
|
|
754
|
-
})
|
|
755
|
-
| None =>
|
|
756
|
-
if tsvs->Utils.Array.notEmpty {
|
|
757
|
-
JsError.throwWithMessage(
|
|
758
|
-
`Invalid effect cache directory ".envio/cache/${entry}". Chain cache directories must be named by a numeric chain id (e.g. "1"). Found cache files: ${tsvs->Array.joinUnsafe(
|
|
759
|
-
", ",
|
|
760
|
-
)}.`,
|
|
761
|
-
)
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
} else if entry->String.endsWith(".tsv") {
|
|
765
|
-
let effectName = entry->String.slice(~start=0, ~end=-4)
|
|
766
|
-
let table = Internal.makeCacheTable(~effectName, ~scope=CrossChain)
|
|
767
|
-
result->Array.push((table, entryPath->NodeJs.Path.toString))->ignore
|
|
768
|
-
}
|
|
769
|
-
})
|
|
770
|
-
->Promise.all
|
|
771
|
-
result
|
|
772
|
-
}
|
|
773
|
-
|
|
774
220
|
let restoreEffectCache = async (~withUpload) => {
|
|
775
|
-
if withUpload {
|
|
776
|
-
switch await scanCacheDir() {
|
|
777
|
-
| [] => logger->Logger.info("No saved effect cache to load from .envio/cache.")
|
|
778
|
-
| entries =>
|
|
779
|
-
try {
|
|
780
|
-
await client->StorageClient.uploadEffectCache(
|
|
781
|
-
entries->Array.map(((table, path)): StorageClient.cacheUpload => {
|
|
782
|
-
table: (table->cacheTable).handle,
|
|
783
|
-
path,
|
|
784
|
-
}),
|
|
785
|
-
)
|
|
786
|
-
logger->Logger.info("Successfully uploaded cache.")
|
|
787
|
-
} catch {
|
|
788
|
-
| exn => logger->Logger.errorWithExn("Failed to upload cache, continuing without it.", exn)
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
221
|
let cache = Dict.make()
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
222
|
+
|
|
223
|
+
(
|
|
224
|
+
await client->StorageClient.restoreEffectCache(cacheDirPath->NodeJs.Path.toString, withUpload)
|
|
225
|
+
)->Array.forEach(({effectName, ?chainId, tableName, rows}) =>
|
|
226
|
+
cache->Dict.set(
|
|
227
|
+
tableName,
|
|
228
|
+
(
|
|
229
|
+
{
|
|
230
|
+
effectName,
|
|
231
|
+
scope: switch chainId {
|
|
232
|
+
| Some(chainId) => Chain(chainId)
|
|
233
|
+
| None => CrossChain
|
|
234
|
+
},
|
|
235
|
+
tableName,
|
|
236
|
+
count: rows,
|
|
237
|
+
}: Persistence.effectCacheRecord
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
804
241
|
cache
|
|
805
242
|
}
|
|
806
243
|
|
|
807
|
-
let partitionChainIds = chainIds => isolated ? Some(chainIds) : None
|
|
808
|
-
|
|
809
|
-
let schemaIndexes = (~entities: array<Internal.entityConfig>, ~chainIds) =>
|
|
810
|
-
getSchemaIndexes(
|
|
811
|
-
~entities=entities->Array.filter((e: Internal.entityConfig) => e.storage.postgres),
|
|
812
|
-
~partitionChainIds=?partitionChainIds(chainIds),
|
|
813
|
-
)->Array.map(IndexDefinition.toInput)
|
|
814
|
-
|
|
815
244
|
let initialize = async (
|
|
816
245
|
~chainConfigs=[],
|
|
817
|
-
~entities=[],
|
|
818
|
-
~enums=[],
|
|
819
246
|
~contractMapping,
|
|
820
247
|
~envioInfo,
|
|
821
248
|
): Persistence.initialState => {
|
|
@@ -825,33 +252,12 @@ let make = (
|
|
|
825
252
|
)
|
|
826
253
|
|
|
827
254
|
await client->StorageClient.initialize({
|
|
828
|
-
sequence: CheckpointSequence.fromEntities(entities),
|
|
829
|
-
tables: [rawEventsTable().handle]->Array.concat(
|
|
830
|
-
entities->Array.map(entityConfig => (entityConfig->entityTable).handle),
|
|
831
|
-
),
|
|
832
|
-
partitions: entities->partitions(
|
|
833
|
-
~chainIds=chainConfigs->Array.map((chainConfig: Config.chain) => chainConfig.id),
|
|
834
|
-
),
|
|
835
|
-
enums: enums->Array.map((enumConfig: Table.enumConfig<Table.enum>): StorageClient.enum => {
|
|
836
|
-
name: enumConfig.name,
|
|
837
|
-
variants: enumConfig.variants->(Utils.magic: array<Table.enum> => array<string>),
|
|
838
|
-
}),
|
|
839
255
|
chains: chainConfigs->Array.map(toChainConfig),
|
|
840
256
|
envioInfo: envioInfo->JSON.stringify,
|
|
841
257
|
contractNames: contractMapping->ContractMapping.names,
|
|
842
258
|
addresses: rowsByChain->Array.flat->addressColumns,
|
|
843
|
-
?keptEffectCaches,
|
|
844
259
|
})
|
|
845
260
|
|
|
846
|
-
if eagerSchemaIndexes {
|
|
847
|
-
await client->StorageClient.ensureSchemaIndexes(
|
|
848
|
-
schemaIndexes(
|
|
849
|
-
~entities,
|
|
850
|
-
~chainIds=chainConfigs->Array.map((chainConfig: Config.chain) => chainConfig.id),
|
|
851
|
-
),
|
|
852
|
-
)
|
|
853
|
-
}
|
|
854
|
-
|
|
855
261
|
let cache = await restoreEffectCache(~withUpload=true)
|
|
856
262
|
|
|
857
263
|
// Integration with other tools like Hasura
|
|
@@ -887,7 +293,11 @@ let make = (
|
|
|
887
293
|
|
|
888
294
|
let loadOrThrow = async (~filter: EntityFilter.t, ~table: Table.table) => {
|
|
889
295
|
let conditions = LoadFilter.make(~filter, ~table, ~column=field => field.pgDbFieldName)
|
|
890
|
-
switch
|
|
296
|
+
let loading = switch entityIndexByName->Utils.Dict.dangerouslyGetNonOption(table.tableName) {
|
|
297
|
+
| Some(entity) => client->StorageClient.loadWhere(~entity, ~conditions)
|
|
298
|
+
| None => client->StorageClient.loadEffectCache(~table=table.tableName, ~conditions)
|
|
299
|
+
}
|
|
300
|
+
switch await loading {
|
|
891
301
|
| exception exn =>
|
|
892
302
|
throw(
|
|
893
303
|
Persistence.StorageError({
|
|
@@ -931,28 +341,15 @@ let make = (
|
|
|
931
341
|
columns
|
|
932
342
|
}
|
|
933
343
|
|
|
934
|
-
// The physical table a query index for `scope` is built on. A per-chain
|
|
935
|
-
// entity's query carries the scope's chain id, so it is planned against that
|
|
936
|
-
// chain's partition, which an index on the parent covers by cascading.
|
|
937
|
-
let queryTableName = (~entityConfig: Internal.entityConfig, ~scope: Internal.chainScope) =>
|
|
938
|
-
switch scope {
|
|
939
|
-
| Chain(chainId) =>
|
|
940
|
-
indexTableNames(
|
|
941
|
-
entityConfig,
|
|
942
|
-
~partitionChainIds=partitionChainIds([chainId]),
|
|
943
|
-
)->Array.getUnsafe(0)
|
|
944
|
-
| CrossChain => entityConfig.table.tableName
|
|
945
|
-
}
|
|
946
|
-
|
|
947
344
|
let ensureQueryIndexes = async (
|
|
948
345
|
~entityConfig: Internal.entityConfig,
|
|
949
346
|
~scope: Internal.chainScope,
|
|
950
347
|
~filters: array<EntityFilter.t>,
|
|
951
348
|
) =>
|
|
952
349
|
switch client->StorageClient.ensureQueryIndexes(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
filterColumns(~table=entityConfig.table, ~filters),
|
|
350
|
+
~entity=entityConfig.index,
|
|
351
|
+
~chainId=scope->Internal.chainScopeChainId->Null.fromOption,
|
|
352
|
+
~columns=filterColumns(~table=entityConfig.table, ~filters),
|
|
956
353
|
) {
|
|
957
354
|
| Value(building) => await building
|
|
958
355
|
| Null => ()
|
|
@@ -961,47 +358,13 @@ let make = (
|
|
|
961
358
|
// Runs on a resumed indexer that is already ready, so handlers may be issuing
|
|
962
359
|
// getWhere queries alongside it. Nothing here writes `ready_at` — the chains
|
|
963
360
|
// already carry theirs.
|
|
964
|
-
let ensureSchemaIndexes = (~
|
|
965
|
-
client->StorageClient.ensureSchemaIndexes(schemaIndexes(~entities, ~chainIds))
|
|
361
|
+
let ensureSchemaIndexes = (~chainIds) => client->StorageClient.ensureSchemaIndexes(chainIds)
|
|
966
362
|
|
|
967
|
-
let finalizeBackfill = (~
|
|
968
|
-
client->StorageClient.finalizeBackfill(
|
|
969
|
-
schemaIndexes(~entities, ~chainIds),
|
|
970
|
-
chainIds,
|
|
971
|
-
readyAt->Date.getTime,
|
|
972
|
-
)
|
|
363
|
+
let finalizeBackfill = (~chainIds, ~readyAt: Date.t) =>
|
|
364
|
+
client->StorageClient.finalizeBackfill(chainIds, readyAt->Date.getTime)
|
|
973
365
|
|
|
974
|
-
let dumpEffectCache =
|
|
975
|
-
|
|
976
|
-
let tables =
|
|
977
|
-
(await client->StorageClient.effectCacheTables)->Array.filter(({rows}) => rows > 0)
|
|
978
|
-
if tables->Utils.Array.notEmpty {
|
|
979
|
-
logger->Logger.info(
|
|
980
|
-
`Dumping cache: ${tables
|
|
981
|
-
->Array.map(({tableName, rows}) => tableName ++ " (" ++ rows->Int.toString ++ " rows)")
|
|
982
|
-
->Array.joinUnsafe(", ")}`,
|
|
983
|
-
)
|
|
984
|
-
await client->StorageClient.dumpEffectCache(
|
|
985
|
-
tables->Array.filterMap(({tableName}) =>
|
|
986
|
-
Internal.EffectCache.fromTableName(tableName)->Option.map(((
|
|
987
|
-
effectName,
|
|
988
|
-
scope,
|
|
989
|
-
)): StorageClient.cacheDump => {
|
|
990
|
-
tableName,
|
|
991
|
-
// Chain-scoped caches dump into a directory of their chain's.
|
|
992
|
-
path: NodeJs.Path.join(
|
|
993
|
-
cacheDirPath,
|
|
994
|
-
Internal.EffectCache.toCachePath(~effectName, ~scope),
|
|
995
|
-
)->NodeJs.Path.toString,
|
|
996
|
-
})
|
|
997
|
-
),
|
|
998
|
-
)
|
|
999
|
-
logger->Logger.info(`Successfully dumped cache to ${cacheDirPath->NodeJs.Path.toString}`)
|
|
1000
|
-
}
|
|
1001
|
-
} catch {
|
|
1002
|
-
| exn => logger->Logger.errorWithExn(`Failed to dump cache.`, exn)
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
366
|
+
let dumpEffectCache = () =>
|
|
367
|
+
client->StorageClient.dumpEffectCache(cacheDirPath->NodeJs.Path.toString)
|
|
1005
368
|
|
|
1006
369
|
let readAddressRows = (result): array<AddressRows.row> =>
|
|
1007
370
|
client
|
|
@@ -1044,27 +407,18 @@ let make = (
|
|
|
1044
407
|
}
|
|
1045
408
|
}
|
|
1046
409
|
|
|
1047
|
-
let addChain = (~chainConfig: Config.chain, ~
|
|
410
|
+
let addChain = (~chainConfig: Config.chain, ~contractMapping) =>
|
|
1048
411
|
client->StorageClient.addChain({
|
|
1049
412
|
chain: chainConfig->toChainConfig,
|
|
1050
|
-
partitions: entities->partitions(~chainIds=[chainConfig.id]),
|
|
1051
413
|
addresses: chainConfig
|
|
1052
414
|
->ChainState.configStorageRows(~ecosystem, ~contractMapping)
|
|
1053
415
|
->addressColumns,
|
|
1054
416
|
})
|
|
1055
417
|
|
|
1056
|
-
let resumeInitialState = async (
|
|
1057
|
-
~entities: array<Internal.entityConfig>,
|
|
1058
|
-
~chainIds,
|
|
1059
|
-
~contractMapping,
|
|
1060
|
-
): Persistence.initialState => {
|
|
418
|
+
let resumeInitialState = async (~chainIds, ~contractMapping): Persistence.initialState => {
|
|
1061
419
|
let (cache, resumed) = await Promise.all2((
|
|
1062
420
|
restoreEffectCache(~withUpload=false),
|
|
1063
|
-
client->StorageClient.resume(
|
|
1064
|
-
sequence: CheckpointSequence.fromEntities(entities),
|
|
1065
|
-
tables: entities->Array.map(entityConfig => (entityConfig->entityTable).handle),
|
|
1066
|
-
chainIds,
|
|
1067
|
-
}),
|
|
421
|
+
client->StorageClient.resume(chainIds),
|
|
1068
422
|
))
|
|
1069
423
|
let addressRowsByChainId = resumed.addresses->readAddressRows->AddressRows.group
|
|
1070
424
|
let stored =
|
|
@@ -1087,12 +441,12 @@ let make = (
|
|
|
1087
441
|
sourceBlockNumber: chain.sourceBlock,
|
|
1088
442
|
})
|
|
1089
443
|
let checkpointFrontier = Frontier.fromEntries(
|
|
1090
|
-
stored->Array.map(((id, chain)) => (id, chain.checkpointId
|
|
444
|
+
stored->Array.map(((id, chain)) => (id, chain.checkpointId)),
|
|
1091
445
|
)
|
|
1092
446
|
let reorgCheckpoints = resumed.reorgCheckpoints->Array.map((
|
|
1093
447
|
checkpoint
|
|
1094
448
|
): Internal.reorgCheckpoint => {
|
|
1095
|
-
checkpointId: checkpoint.id
|
|
449
|
+
checkpointId: checkpoint.id,
|
|
1096
450
|
chainId: checkpoint.chainId->ChainId.normalizeOrThrow,
|
|
1097
451
|
blockNumber: checkpoint.blockNumber,
|
|
1098
452
|
blockHash: checkpoint.blockHash,
|
|
@@ -1129,19 +483,13 @@ let make = (
|
|
|
1129
483
|
let pruneStaleCheckpoints = (~safeCheckpoints) =>
|
|
1130
484
|
client->StorageClient.pruneCheckpoints(safeCheckpoints->bounds)
|
|
1131
485
|
|
|
1132
|
-
let pruneStaleEntityHistory = (~entityConfig, ~safeCheckpoints) =>
|
|
1133
|
-
client->StorageClient.pruneHistory(
|
|
486
|
+
let pruneStaleEntityHistory = (~entityConfig: Internal.entityConfig, ~safeCheckpoints) =>
|
|
487
|
+
client->StorageClient.pruneHistory(entityConfig.index, safeCheckpoints->bounds)
|
|
1134
488
|
|
|
1135
|
-
let getRollbackTargetCheckpoint = async (~reorgChainId, ~lastKnownValidBlockNumber
|
|
489
|
+
let getRollbackTargetCheckpoint = async (~reorgChainId, ~lastKnownValidBlockNumber) =>
|
|
1136
490
|
(
|
|
1137
|
-
await client->StorageClient.rollbackTargetCheckpoint(
|
|
1138
|
-
|
|
1139
|
-
lastKnownValidBlockNumber,
|
|
1140
|
-
sequence,
|
|
1141
|
-
)
|
|
1142
|
-
)
|
|
1143
|
-
->Nullable.toOption
|
|
1144
|
-
->Option.map(BigInt.fromStringOrThrow)
|
|
491
|
+
await client->StorageClient.rollbackTargetCheckpoint(reorgChainId, lastKnownValidBlockNumber)
|
|
492
|
+
)->Nullable.toOption
|
|
1145
493
|
|
|
1146
494
|
let getRollbackProgressDiff = async (~floors: RollbackFloors.t) =>
|
|
1147
495
|
(
|
|
@@ -1156,7 +504,7 @@ let make = (
|
|
|
1156
504
|
|
|
1157
505
|
let getRollbackData = async (~entityConfig: Internal.entityConfig, ~floors: RollbackFloors.t) => {
|
|
1158
506
|
let {removed, restored} = await client->StorageClient.rollbackData(
|
|
1159
|
-
|
|
507
|
+
entityConfig.index,
|
|
1160
508
|
floors.checkpointBounds->bounds,
|
|
1161
509
|
)
|
|
1162
510
|
let removedIdRows = try client->StorageClient.read(removed) catch {
|
|
@@ -1172,183 +520,57 @@ let make = (
|
|
|
1172
520
|
| None => Internal.CrossChain
|
|
1173
521
|
| Some(schema) => Internal.Chain(row->S.parseOrThrow(schema))
|
|
1174
522
|
}
|
|
1175
|
-
let removals = removedIdRows->Array.map((row): Persistence.rollbackRemoval => {
|
|
1176
|
-
entityId: row->S.parseOrThrow(rollbackRemovedIdSchema(entityConfig.table)),
|
|
1177
|
-
scope: scopeOf(row),
|
|
1178
|
-
})
|
|
1179
|
-
let restoredEntitiesResult = []
|
|
1180
|
-
rollbackRows->Array.forEach(row => {
|
|
1181
|
-
let (entityId, action) = row->S.parseOrThrow(rollbackRowStateSchema(entityConfig.table))
|
|
1182
|
-
switch action {
|
|
1183
|
-
| SET => restoredEntitiesResult->Array.push(row)->ignore
|
|
1184
|
-
| DELETE => removals->Array.push({entityId, scope: scopeOf(row)})->ignore
|
|
1185
|
-
}
|
|
1186
|
-
})
|
|
1187
|
-
|
|
1188
523
|
(
|
|
1189
|
-
|
|
1190
|
-
|
|
524
|
+
removedIdRows->Array.map((row): Persistence.rollbackRemoval => {
|
|
525
|
+
entityId: row->S.parseOrThrow(rollbackRemovedIdSchema(entityConfig.table)),
|
|
526
|
+
scope: scopeOf(row),
|
|
527
|
+
}),
|
|
528
|
+
rollbackRows
|
|
1191
529
|
->(Utils.magic: array<dict<unknown>> => array<unknown>)
|
|
1192
530
|
->S.parseOrThrow(entityConfig.table->Table.pgRowsSchema)
|
|
1193
531
|
->(Utils.magic: array<unknown> => array<Internal.entity>),
|
|
1194
532
|
)
|
|
1195
533
|
}
|
|
1196
534
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
{entityConfig, scope, changes, shouldSaveHistory}: Persistence.updatedEntity,
|
|
1202
|
-
~rollback: option<Persistence.rollback>,
|
|
1203
|
-
~config: Config.t,
|
|
1204
|
-
~staged,
|
|
1205
|
-
): StorageClient.entityWrite => {
|
|
1206
|
-
let table = entityConfig->entityTable
|
|
1207
|
-
|
|
1208
|
-
let chainIdStamp = switch (entityConfig.table->Table.getChainIdField, scope) {
|
|
1209
|
-
| (Some(field), Internal.Chain(chainId)) => Some((field, chainId))
|
|
1210
|
-
| _ => None
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
let postgresWrite = (): StorageClient.postgresEntityWrite => {
|
|
1214
|
-
// Every row in this group belongs to the group's scope, so the chain id
|
|
1215
|
-
// is stamped once here instead of being looked up per row downstream.
|
|
1216
|
-
let stamped = switch chainIdStamp {
|
|
1217
|
-
| Some((field, chainId)) =>
|
|
1218
|
-
changes->Array.map(change =>
|
|
1219
|
-
switch change {
|
|
1220
|
-
| Change.Set(set) =>
|
|
1221
|
-
Change.Set({
|
|
1222
|
-
...set,
|
|
1223
|
-
entity: set.entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId),
|
|
1224
|
-
})
|
|
1225
|
-
| Delete(_) => change
|
|
1226
|
-
}
|
|
1227
|
-
)
|
|
1228
|
-
| None => changes
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// The rollback-diff change is written to the entity table only, never the
|
|
1232
|
-
// history table; when present it is an id's oldest change.
|
|
1233
|
-
let diffCheckpointId =
|
|
1234
|
-
rollback->Option.flatMap(r =>
|
|
1235
|
-
config.checkpointSequence->CheckpointSequence.findForScope(r.diffFrontier, ~scope)
|
|
1236
|
-
)
|
|
1237
|
-
|
|
1238
|
-
let historySets = []
|
|
1239
|
-
let historySetCheckpointIds = []
|
|
1240
|
-
let historyDeleteIds = []
|
|
1241
|
-
let historyDeleteCheckpointIds = []
|
|
1242
|
-
let idsWithDiff = Utils.Set.make()
|
|
1243
|
-
|
|
1244
|
-
// Each id's latest change (the last one seen) and, when saving history,
|
|
1245
|
-
// every change but the diff for the history table. Keyed by the id's
|
|
1246
|
-
// string key, while what goes to SQL keeps the real id so it serializes
|
|
1247
|
-
// with the id column's type.
|
|
1248
|
-
let latestChangeById = Dict.make()
|
|
1249
|
-
let orderedIds = []
|
|
1250
|
-
stamped->Array.forEach(change => {
|
|
1251
|
-
let entityId = change->Change.getEntityId
|
|
1252
|
-
let entityKey = entityId->EntityId.toKey
|
|
1253
|
-
if latestChangeById->Utils.Dict.dangerouslyGetNonOption(entityKey)->Option.isNone {
|
|
1254
|
-
orderedIds->Array.push(entityId)
|
|
1255
|
-
}
|
|
1256
|
-
latestChangeById->Dict.set(entityKey, change)
|
|
1257
|
-
if shouldSaveHistory {
|
|
1258
|
-
if Some(change->Change.getCheckpointId) === diffCheckpointId {
|
|
1259
|
-
idsWithDiff->Utils.Set.add(entityKey)->ignore
|
|
1260
|
-
} else {
|
|
1261
|
-
switch change {
|
|
1262
|
-
| Delete({entityId, checkpointId}) =>
|
|
1263
|
-
historyDeleteIds->Array.push(entityId)
|
|
1264
|
-
historyDeleteCheckpointIds->Array.push(checkpointId->BigInt.toString)
|
|
1265
|
-
| Set({entity, checkpointId}) =>
|
|
1266
|
-
historySets->Array.push(entity)
|
|
1267
|
-
historySetCheckpointIds->Array.push(checkpointId->BigInt.toString)
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
})
|
|
1272
|
-
|
|
1273
|
-
let sets = []
|
|
1274
|
-
let deletes = []
|
|
1275
|
-
let backfill = []
|
|
1276
|
-
orderedIds->Array.forEach(entityId => {
|
|
1277
|
-
let entityKey = entityId->EntityId.toKey
|
|
1278
|
-
switch latestChangeById->Dict.getUnsafe(entityKey) {
|
|
1279
|
-
| Set({entity}) => sets->Array.push(entity)
|
|
1280
|
-
| Delete({entityId}) => deletes->Array.push(entityId)
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// An id needs a history backfill iff none of its changes is the diff.
|
|
1284
|
-
if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityKey)) {
|
|
1285
|
-
backfill->Array.push(entityId)
|
|
1286
|
-
}
|
|
1287
|
-
})
|
|
1288
|
-
|
|
1289
|
-
let rowsOf = entities =>
|
|
1290
|
-
entities->Utils.Array.notEmpty
|
|
1291
|
-
? Some(
|
|
1292
|
-
client->rowsOrThrow(
|
|
1293
|
-
table,
|
|
1294
|
-
~itemSchema=entityConfig->getRowSchema->S.toUnknown,
|
|
1295
|
-
entities->(Utils.magic: array<Internal.entity> => array<unknown>),
|
|
1296
|
-
~staged,
|
|
1297
|
-
),
|
|
1298
|
-
)
|
|
1299
|
-
: None
|
|
1300
|
-
{
|
|
1301
|
-
sets: ?rowsOf(sets),
|
|
1302
|
-
deletes: entityConfig.table->renderIds(deletes),
|
|
1303
|
-
history: ?(
|
|
1304
|
-
shouldSaveHistory
|
|
1305
|
-
? Some(
|
|
1306
|
-
(
|
|
1307
|
-
{
|
|
1308
|
-
backfill: entityConfig.table->renderIds(backfill),
|
|
1309
|
-
sets: ?rowsOf(historySets),
|
|
1310
|
-
setCheckpointIds: historySetCheckpointIds,
|
|
1311
|
-
deleteIds: entityConfig.table->renderIds(historyDeleteIds),
|
|
1312
|
-
deleteCheckpointIds: historyDeleteCheckpointIds,
|
|
1313
|
-
}: StorageClient.historyWrite
|
|
1314
|
-
),
|
|
1315
|
-
)
|
|
1316
|
-
: None
|
|
1317
|
-
),
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
535
|
+
let stageRows = (~table, ~writers, items) =>
|
|
536
|
+
client
|
|
537
|
+
->StorageClient.arena((~rows) => client->StorageClient.beginRows(~table, ~rows))
|
|
538
|
+
->stageRowsOrThrow(~writers, items)
|
|
1320
539
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
540
|
+
let stageChanges = ({entityConfig, changes}: Persistence.updatedEntity) =>
|
|
541
|
+
client
|
|
542
|
+
->StorageClient.arena((~rows) =>
|
|
543
|
+
client->StorageClient.beginChanges(~entity=entityConfig.index, ~rows)
|
|
544
|
+
)
|
|
545
|
+
->stageChangesOrThrow(~writers=entityWriters->Array.getUnsafe(entityConfig.index), ~changes)
|
|
546
|
+
|
|
547
|
+
// A value a store can't take is reported as the store's own refusal: the
|
|
548
|
+
// store of record's, for an entity it keeps.
|
|
549
|
+
let changesErrorOf = (entityConfig: Internal.entityConfig, exn) => Persistence.StorageError({
|
|
550
|
+
message: entityConfig.storage.postgres
|
|
551
|
+
? `Failed to insert items into table "${entityConfig.table.tableName}"`
|
|
552
|
+
: `Failed to convert items for ClickHouse table "${EntityHistory.historyTableName(
|
|
553
|
+
~entityName=entityConfig.name,
|
|
554
|
+
~entityIndex=entityConfig.index,
|
|
555
|
+
)}"`,
|
|
556
|
+
reason: exn->Utils.prettifyExn,
|
|
557
|
+
})
|
|
1338
558
|
|
|
1339
559
|
let writeBatch = async (
|
|
1340
560
|
~batch: Batch.t,
|
|
1341
561
|
~rollback: option<Persistence.rollback>,
|
|
1342
562
|
~config: Config.t,
|
|
1343
|
-
~allEntities: array<Internal.entityConfig>,
|
|
1344
563
|
~updatedEffectsCache: array<Persistence.updatedEffectCache>,
|
|
1345
564
|
~updatedEntities: array<Persistence.updatedEntity>,
|
|
1346
565
|
~registeredAddresses: array<AddressRows.staged>,
|
|
1347
566
|
~chainMetaData,
|
|
1348
567
|
~onWrite,
|
|
1349
568
|
) => {
|
|
1350
|
-
let (frontierChainIds, frontierCheckpointIds) =
|
|
1351
|
-
Persistence.writtenFrontier(~batch, ~rollback)
|
|
569
|
+
let (frontierChainIds, frontierCheckpointIds) = {
|
|
570
|
+
let frontier = Persistence.writtenFrontier(~batch, ~rollback)
|
|
571
|
+
let chainIds = frontier->Frontier.chainIds
|
|
572
|
+
(chainIds, chainIds->Array.map(chainId => frontier->Frontier.get(chainId)))
|
|
573
|
+
}
|
|
1352
574
|
|
|
1353
575
|
// A single on-chain log fans out to one item per matching registration;
|
|
1354
576
|
// `raw_events` records the log itself, so it is deduped by its coordinate
|
|
@@ -1373,7 +595,14 @@ let make = (
|
|
|
1373
595
|
[]
|
|
1374
596
|
}
|
|
1375
597
|
|
|
1376
|
-
let
|
|
598
|
+
let handles = []
|
|
599
|
+
let staged = handle => {
|
|
600
|
+
handles->Array.push(handle)
|
|
601
|
+
handle
|
|
602
|
+
}
|
|
603
|
+
let stagingStarted = Performance.now()
|
|
604
|
+
let clickHouseStagingSeconds = ref(0.)
|
|
605
|
+
let clickHouseOnlyStagingSeconds = ref(0.)
|
|
1377
606
|
let input: StorageClient.batch = try {
|
|
1378
607
|
rollback: ?(
|
|
1379
608
|
rollback->Option.map(({
|
|
@@ -1383,7 +612,6 @@ let make = (
|
|
|
1383
612
|
diffCheckpoints,
|
|
1384
613
|
}): StorageClient.rollbackWrite => {
|
|
1385
614
|
bounds: floors.checkpointBounds->bounds,
|
|
1386
|
-
tables: allEntities->Array.map(entityConfig => (entityConfig->entityTable).handle),
|
|
1387
615
|
progress: progressedChains->Array.map(progress),
|
|
1388
616
|
removedAddresses: {
|
|
1389
617
|
chainIds: rolledBackAddresses->Array.map(key => key.chainId),
|
|
@@ -1392,7 +620,7 @@ let make = (
|
|
|
1392
620
|
},
|
|
1393
621
|
diffCheckpoints: diffCheckpoints->Array.map((diff): StorageClient.diffCheckpoint => {
|
|
1394
622
|
chainId: diff.chainId,
|
|
1395
|
-
checkpointId: diff.checkpointId
|
|
623
|
+
checkpointId: diff.checkpointId,
|
|
1396
624
|
blockNumber: diff.blockNumber,
|
|
1397
625
|
}),
|
|
1398
626
|
})
|
|
@@ -1408,26 +636,49 @@ let make = (
|
|
|
1408
636
|
),
|
|
1409
637
|
rawEvents: ?(
|
|
1410
638
|
rawEvents->Utils.Array.notEmpty
|
|
1411
|
-
?
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
639
|
+
? Some(
|
|
640
|
+
try stageRows(
|
|
641
|
+
~table=RawEvents,
|
|
642
|
+
~writers=rawEventWriters,
|
|
643
|
+
rawEvents->(Utils.magic: array<Internal.rawEvent> => array<dict<unknown>>),
|
|
644
|
+
)->staged catch {
|
|
645
|
+
| exn =>
|
|
646
|
+
throw(
|
|
647
|
+
Persistence.StorageError({
|
|
648
|
+
message: `Failed to insert items into table "${InternalTable.RawEvents.table.tableName}"`,
|
|
649
|
+
reason: exn->Utils.prettifyExn,
|
|
650
|
+
}),
|
|
651
|
+
)
|
|
652
|
+
},
|
|
653
|
+
)
|
|
1427
654
|
: None
|
|
1428
655
|
),
|
|
1429
|
-
|
|
1430
|
-
update
|
|
656
|
+
changes: updatedEntities->Array.filterMap(update =>
|
|
657
|
+
switch update.changes {
|
|
658
|
+
| [] => None
|
|
659
|
+
| _ =>
|
|
660
|
+
let started = Performance.now()
|
|
661
|
+
let handle = try update->stageChanges->staged catch {
|
|
662
|
+
| exn => throw(changesErrorOf(update.entityConfig, exn))
|
|
663
|
+
}
|
|
664
|
+
let seconds = started->Performance.secondsSince
|
|
665
|
+
if update.entityConfig.storage.clickhouse {
|
|
666
|
+
clickHouseStagingSeconds := clickHouseStagingSeconds.contents +. seconds
|
|
667
|
+
}
|
|
668
|
+
if !update.entityConfig.storage.postgres {
|
|
669
|
+
clickHouseOnlyStagingSeconds := clickHouseOnlyStagingSeconds.contents +. seconds
|
|
670
|
+
}
|
|
671
|
+
Some(
|
|
672
|
+
(
|
|
673
|
+
{
|
|
674
|
+
entity: update.entityConfig.index,
|
|
675
|
+
chainId: ?(update.scope->Internal.chainScopeChainId),
|
|
676
|
+
keepsHistory: update.shouldSaveHistory,
|
|
677
|
+
changes: handle,
|
|
678
|
+
}: StorageClient.changes
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
}
|
|
1431
682
|
),
|
|
1432
683
|
chainMeta: chainMetaData->Option.mapOr([], chainMeta),
|
|
1433
684
|
addresses: registeredAddresses->Array.map(staged => staged.row)->addressColumns,
|
|
@@ -1435,7 +686,7 @@ let make = (
|
|
|
1435
686
|
// A checkpoint anchors the history its chain keeps, which the batch
|
|
1436
687
|
// decides chain by chain.
|
|
1437
688
|
checkpoints: {
|
|
1438
|
-
ids: batch.checkpointIds
|
|
689
|
+
ids: batch.checkpointIds,
|
|
1439
690
|
chainIds: batch.checkpointChainIds,
|
|
1440
691
|
blockNumbers: batch.checkpointBlockNumbers,
|
|
1441
692
|
blockHashes: batch.checkpointBlockHashes,
|
|
@@ -1451,44 +702,70 @@ let make = (
|
|
|
1451
702
|
items,
|
|
1452
703
|
shouldInitialize,
|
|
1453
704
|
}): StorageClient.effectCacheWrite => {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
705
|
+
table: table.tableName,
|
|
706
|
+
create: shouldInitialize,
|
|
707
|
+
rows: try stageRows(
|
|
708
|
+
~table=EffectCache,
|
|
709
|
+
~writers=effectCacheWriters,
|
|
710
|
+
items->Array.map(item =>
|
|
711
|
+
item
|
|
712
|
+
->S.reverseConvertOrThrow(itemSchema)
|
|
713
|
+
->(Utils.magic: unknown => dict<unknown>)
|
|
1463
714
|
),
|
|
1464
|
-
|
|
715
|
+
)->staged catch {
|
|
716
|
+
| S.Raised(_) as exn =>
|
|
717
|
+
throw(
|
|
718
|
+
Persistence.StorageError({
|
|
719
|
+
message: `Failed to convert items for table "${table.tableName}"`,
|
|
720
|
+
reason: exn,
|
|
721
|
+
}),
|
|
722
|
+
)
|
|
723
|
+
| exn =>
|
|
724
|
+
throw(
|
|
725
|
+
Persistence.StorageError({
|
|
726
|
+
message: `Failed to insert items into table "${table.tableName}"`,
|
|
727
|
+
reason: exn->Utils.prettifyExn,
|
|
728
|
+
}),
|
|
729
|
+
)
|
|
730
|
+
},
|
|
1465
731
|
}),
|
|
1466
732
|
} catch {
|
|
1467
733
|
| exn =>
|
|
1468
|
-
client->StorageClient.discardStaged(
|
|
734
|
+
client->StorageClient.discardStaged(handles)
|
|
1469
735
|
throw(exn)
|
|
1470
736
|
}
|
|
737
|
+
// Each store is charged for staging what it writes: an entity both keep
|
|
738
|
+
// is staged once for the two.
|
|
739
|
+
let postgresStagingSeconds =
|
|
740
|
+
stagingStarted->Performance.secondsSince -. clickHouseOnlyStagingSeconds.contents
|
|
1471
741
|
|
|
1472
742
|
let timings = try await client->StorageClient.writeBatch(input) catch {
|
|
1473
743
|
| exn =>
|
|
1474
744
|
// Batches the addon never took, because it refused the call itself.
|
|
1475
|
-
client->StorageClient.discardStaged(
|
|
745
|
+
client->StorageClient.discardStaged(handles)
|
|
1476
746
|
throw(exn->storageErrorOf)
|
|
1477
747
|
}
|
|
1478
|
-
timings->Array.forEach(({store, seconds}) =>
|
|
1479
|
-
let name = store->storeName
|
|
748
|
+
timings->Array.forEach(({store, seconds}) =>
|
|
1480
749
|
onWrite(
|
|
1481
|
-
~storage=
|
|
1482
|
-
~timeSeconds=seconds +.
|
|
750
|
+
~storage=store->storeName,
|
|
751
|
+
~timeSeconds=seconds +.
|
|
752
|
+
switch store {
|
|
753
|
+
| Postgres => postgresStagingSeconds
|
|
754
|
+
| ClickHouse => clickHouseStagingSeconds.contents
|
|
755
|
+
},
|
|
1483
756
|
)
|
|
1484
|
-
|
|
757
|
+
)
|
|
1485
758
|
}
|
|
1486
759
|
|
|
1487
760
|
let close = () => client->StorageClient.close
|
|
1488
761
|
|
|
1489
762
|
{
|
|
1490
763
|
name: postgresName,
|
|
1491
|
-
entityStoreName: entityConfig =>
|
|
764
|
+
entityStoreName: entityConfig =>
|
|
765
|
+
(layouts.entities->Array.getUnsafe(entityConfig.index)).home->Option.mapOr(
|
|
766
|
+
postgresName,
|
|
767
|
+
storeName,
|
|
768
|
+
),
|
|
1492
769
|
holding,
|
|
1493
770
|
initialize,
|
|
1494
771
|
readStoredConfig,
|
|
@@ -1520,8 +797,13 @@ let clientFromEnv = (
|
|
|
1520
797
|
) =>
|
|
1521
798
|
makeClient(
|
|
1522
799
|
~logger,
|
|
1523
|
-
~
|
|
1524
|
-
~postgres=postgresOptions(
|
|
800
|
+
~config,
|
|
801
|
+
~postgres=postgresOptions(
|
|
802
|
+
~pgSchema,
|
|
803
|
+
~isHasuraEnabled,
|
|
804
|
+
~maxConnections?,
|
|
805
|
+
~isolated=config.isolated,
|
|
806
|
+
),
|
|
1525
807
|
~clickhouse=?config.storage.clickhouse ? Some(ClickHouse.optionsFromEnvOrThrow()) : None,
|
|
1526
808
|
)
|
|
1527
809
|
|
|
@@ -1535,11 +817,8 @@ let makeStorageFromEnv = (
|
|
|
1535
817
|
) => {
|
|
1536
818
|
make(
|
|
1537
819
|
~client=clientFromEnv(~config, ~logger, ~pgSchema, ~isHasuraEnabled, ~maxConnections?),
|
|
1538
|
-
~logger,
|
|
1539
|
-
~entities=config.userEntities,
|
|
1540
820
|
~cacheDir?,
|
|
1541
821
|
~ecosystem=config.ecosystem.name,
|
|
1542
|
-
~isolated=config.isolated,
|
|
1543
822
|
~onInitialize=?isHasuraEnabled
|
|
1544
823
|
? Some(
|
|
1545
824
|
() =>
|