envio 3.0.2 → 3.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +0 -1
  2. package/evm.schema.json +15 -8
  3. package/fuel.schema.json +19 -12
  4. package/index.d.ts +0 -2
  5. package/package.json +6 -7
  6. package/rescript.json +1 -1
  7. package/src/Batch.res +4 -214
  8. package/src/Batch.res.mjs +6 -165
  9. package/src/ChainFetcher.res +12 -28
  10. package/src/ChainFetcher.res.mjs +8 -17
  11. package/src/ChainManager.res +10 -9
  12. package/src/ChainManager.res.mjs +6 -10
  13. package/src/Config.res +9 -25
  14. package/src/Config.res.mjs +17 -27
  15. package/src/Core.res +7 -0
  16. package/src/Ctx.res +1 -0
  17. package/src/Env.res +0 -8
  18. package/src/Env.res.mjs +0 -6
  19. package/src/EventConfigBuilder.res +13 -123
  20. package/src/EventConfigBuilder.res.mjs +6 -73
  21. package/src/EventProcessing.res +5 -29
  22. package/src/EventProcessing.res.mjs +11 -20
  23. package/src/EventUtils.res +0 -27
  24. package/src/EventUtils.res.mjs +0 -24
  25. package/src/FetchState.res +2 -15
  26. package/src/FetchState.res.mjs +3 -18
  27. package/src/GlobalState.res +26 -39
  28. package/src/GlobalState.res.mjs +12 -40
  29. package/src/HandlerLoader.res +6 -5
  30. package/src/HandlerLoader.res.mjs +27 -9
  31. package/src/HandlerRegister.res +1 -12
  32. package/src/HandlerRegister.res.mjs +1 -6
  33. package/src/HandlerRegister.resi +1 -1
  34. package/src/Hasura.res +96 -32
  35. package/src/Hasura.res.mjs +93 -38
  36. package/src/InMemoryStore.res +205 -45
  37. package/src/InMemoryStore.res.mjs +157 -40
  38. package/src/InMemoryTable.res +165 -249
  39. package/src/InMemoryTable.res.mjs +156 -227
  40. package/src/Internal.res +10 -34
  41. package/src/Internal.res.mjs +9 -3
  42. package/src/LoadLayer.res +5 -5
  43. package/src/LoadLayer.res.mjs +5 -5
  44. package/src/LogSelection.res +15 -19
  45. package/src/LogSelection.res.mjs +5 -6
  46. package/src/Main.res +4 -6
  47. package/src/Main.res.mjs +26 -15
  48. package/src/Persistence.res +7 -132
  49. package/src/Persistence.res.mjs +1 -102
  50. package/src/PgStorage.res +57 -40
  51. package/src/PgStorage.res.mjs +60 -34
  52. package/src/ReorgDetection.res +35 -58
  53. package/src/ReorgDetection.res.mjs +21 -29
  54. package/src/SimulateItems.res.mjs +21 -3
  55. package/src/Sink.res +2 -2
  56. package/src/Sink.res.mjs +1 -1
  57. package/src/TableIndices.res +9 -2
  58. package/src/TableIndices.res.mjs +7 -1
  59. package/src/TestIndexer.res +53 -60
  60. package/src/TestIndexer.res.mjs +77 -63
  61. package/src/TestIndexerProxyStorage.res +4 -14
  62. package/src/TestIndexerProxyStorage.res.mjs +1 -5
  63. package/src/UserContext.res +2 -4
  64. package/src/UserContext.res.mjs +4 -5
  65. package/src/Utils.res +0 -2
  66. package/src/Utils.res.mjs +0 -3
  67. package/src/bindings/ClickHouse.res +45 -38
  68. package/src/bindings/ClickHouse.res.mjs +16 -17
  69. package/src/bindings/Vitest.res +3 -0
  70. package/src/db/InternalTable.res +59 -18
  71. package/src/db/InternalTable.res.mjs +82 -51
  72. package/src/db/Table.res +9 -2
  73. package/src/db/Table.res.mjs +10 -7
  74. package/src/sources/EnvioApiClient.res +15 -0
  75. package/src/sources/EnvioApiClient.res.mjs +24 -0
  76. package/src/sources/EvmChain.res +32 -10
  77. package/src/sources/EvmChain.res.mjs +31 -5
  78. package/src/sources/HyperFuelSource.res +15 -58
  79. package/src/sources/HyperFuelSource.res.mjs +20 -39
  80. package/src/sources/HyperSync.res +54 -100
  81. package/src/sources/HyperSync.res.mjs +67 -96
  82. package/src/sources/HyperSync.resi +4 -22
  83. package/src/sources/HyperSyncClient.res +70 -247
  84. package/src/sources/HyperSyncClient.res.mjs +47 -46
  85. package/src/sources/HyperSyncSource.res +94 -166
  86. package/src/sources/HyperSyncSource.res.mjs +100 -127
  87. package/src/sources/RpcSource.res +43 -22
  88. package/src/sources/RpcSource.res.mjs +50 -35
  89. package/src/sources/SimulateSource.res +1 -7
  90. package/src/sources/SimulateSource.res.mjs +1 -7
  91. package/src/sources/Source.res +10 -1
  92. package/src/sources/Source.res.mjs +3 -0
  93. package/src/sources/SourceManager.res +177 -8
  94. package/src/sources/SourceManager.res.mjs +141 -3
  95. package/src/sources/SourceManager.resi +19 -0
  96. package/src/tui/Tui.res +44 -6
  97. package/src/tui/Tui.res.mjs +56 -8
  98. package/src/tui/components/TuiData.res +3 -0
  99. package/svm.schema.json +11 -4
  100. package/src/sources/HyperSyncJsonApi.res +0 -390
  101. package/src/sources/HyperSyncJsonApi.res.mjs +0 -237
package/src/Hasura.res CHANGED
@@ -110,7 +110,13 @@ let clearHasuraMetadata = async (~endpoint, ~auth) => {
110
110
  }
111
111
  }
112
112
 
113
- let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableNames: array<string>) => {
113
+ type trackTableConfig = {
114
+ tableName: string,
115
+ description: option<string>,
116
+ columnDescriptions: dict<string>,
117
+ }
118
+
119
+ let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableConfigs: array<trackTableConfig>) => {
114
120
  try {
115
121
  let result = await trackTablesRoute->Rest.fetch(
116
122
  {
@@ -118,17 +124,33 @@ let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableNames: array<string>
118
124
  "args": {
119
125
  // If set to false, any warnings will cause the API call to fail and no new tables to be tracked. Otherwise tables that fail to track will be raised as warnings. (default: true)
120
126
  "allow_warnings": false,
121
- "tables": tableNames->Array.map(tableName =>
127
+ "tables": tableConfigs->Array.map(({tableName, description, columnDescriptions}) => {
128
+ let configuration = dict{
129
+ "custom_name": tableName->(Utils.magic: string => JSON.t),
130
+ }
131
+ switch description {
132
+ | Some(d) => configuration->Dict.set("comment", d->(Utils.magic: string => JSON.t))
133
+ | None => ()
134
+ }
135
+ let columnConfigEntries = columnDescriptions->Dict.toArray
136
+ if columnConfigEntries->Array.length > 0 {
137
+ let columnConfig = dict{}
138
+ columnConfigEntries->Array.forEach(((column, comment)) =>
139
+ columnConfig->Dict.set(column, {"comment": comment}->(Utils.magic: {..} => JSON.t))
140
+ )
141
+ configuration->Dict.set(
142
+ "column_config",
143
+ columnConfig->(Utils.magic: dict<JSON.t> => JSON.t),
144
+ )
145
+ }
122
146
  {
123
147
  "table": {
124
148
  "name": tableName,
125
149
  "schema": pgSchema,
126
150
  },
127
- "configuration": {
128
- "custom_name": tableName,
129
- },
151
+ "configuration": configuration,
130
152
  }
131
- ),
153
+ }),
132
154
  }->(Utils.magic: 'a => JSON.t),
133
155
  },
134
156
  ~client=Rest.client(endpoint),
@@ -139,13 +161,13 @@ let trackTables = async (~endpoint, ~auth, ~pgSchema, ~tableNames: array<string>
139
161
  }
140
162
  Logging.trace({
141
163
  "msg": msg,
142
- "tableNames": tableNames,
164
+ "tableNames": tableConfigs->Array.map(c => c.tableName),
143
165
  })
144
166
  } catch {
145
167
  | exn =>
146
168
  Logging.error({
147
169
  "msg": `There was an issue tracking tables in hasura - indexing may still work - but you may have issues querying the data in hasura.`,
148
- "tableNames": tableNames,
170
+ "tableNames": tableConfigs->Array.map(c => c.tableName),
149
171
  "err": exn->Utils.prettifyExn,
150
172
  })
151
173
  }
@@ -192,31 +214,41 @@ let createEntityRelationship = async (
192
214
  ~objectName: string,
193
215
  ~mappedEntity: string,
194
216
  ~isDerivedFrom: bool,
217
+ ~comment: option<string>=?,
195
218
  ) => {
196
219
  let derivedFromTo = isDerivedFrom ? `"id": "${relationalKey}"` : `"${relationalKey}_id" : "id"`
197
220
 
221
+ let tableJson = {
222
+ "schema": pgSchema,
223
+ "name": tableName,
224
+ }->(Utils.magic: {..} => JSON.t)
225
+ let usingJson = {
226
+ "manual_configuration": {
227
+ "remote_table": {
228
+ "schema": pgSchema,
229
+ "name": mappedEntity,
230
+ },
231
+ "column_mapping": JSON.parseOrThrow(`{${derivedFromTo}}`),
232
+ },
233
+ }->(Utils.magic: {..} => JSON.t)
234
+
235
+ let args = dict{
236
+ "table": tableJson,
237
+ "name": objectName->(Utils.magic: string => JSON.t),
238
+ "source": "default"->(Utils.magic: string => JSON.t),
239
+ "using": usingJson,
240
+ }
241
+ switch comment {
242
+ | Some(c) => args->Dict.set("comment", c->(Utils.magic: string => JSON.t))
243
+ | None => ()
244
+ }
245
+
198
246
  await sendOperation(
199
247
  ~endpoint,
200
248
  ~auth,
201
249
  ~operation={
202
250
  "type": `pg_create_${relationshipType}_relationship`,
203
- "args": {
204
- "table": {
205
- "schema": pgSchema,
206
- "name": tableName,
207
- },
208
- "name": objectName,
209
- "source": "default",
210
- "using": {
211
- "manual_configuration": {
212
- "remote_table": {
213
- "schema": pgSchema,
214
- "name": mappedEntity,
215
- },
216
- "column_mapping": JSON.parseOrThrow(`{${derivedFromTo}}`),
217
- },
218
- },
219
- },
251
+ "args": args,
220
252
  }->(Utils.magic: 'a => JSON.t),
221
253
  )
222
254
  }
@@ -230,19 +262,49 @@ let trackDatabase = async (
230
262
  ~responseLimit,
231
263
  ~schema,
232
264
  ) => {
233
- let exposedInternalTableNames = [
234
- InternalTable.RawEvents.table.tableName,
235
- InternalTable.Views.metaViewName,
236
- InternalTable.Views.chainMetadataViewName,
265
+ let exposedInternalTableConfigs = [
266
+ {
267
+ tableName: InternalTable.RawEvents.table.tableName,
268
+ description: None,
269
+ columnDescriptions: dict{},
270
+ },
271
+ {
272
+ tableName: InternalTable.Views.metaViewName,
273
+ description: None,
274
+ columnDescriptions: dict{},
275
+ },
276
+ {
277
+ tableName: InternalTable.Views.chainMetadataViewName,
278
+ description: None,
279
+ columnDescriptions: dict{},
280
+ },
237
281
  ]
238
- let userTableNames = userEntities->Array.map(entity => entity.table.tableName)
239
- let tableNames = [exposedInternalTableNames, userTableNames]->Belt.Array.concatMany
282
+ let userTableConfigs = userEntities->Array.map(entity => {
283
+ let columnDescriptions = dict{}
284
+ entity.table.fields->Array.forEach(fieldOrDerived =>
285
+ switch fieldOrDerived {
286
+ | Table.Field(field) =>
287
+ switch field.description {
288
+ | Some(d) => columnDescriptions->Dict.set(field->Table.getDbFieldName, d)
289
+ | None => ()
290
+ }
291
+ | Table.DerivedFrom(_) => ()
292
+ }
293
+ )
294
+ {
295
+ tableName: entity.table.tableName,
296
+ description: entity.table.description,
297
+ columnDescriptions,
298
+ }
299
+ })
300
+ let tableConfigs = [exposedInternalTableConfigs, userTableConfigs]->Belt.Array.concatMany
301
+ let tableNames = tableConfigs->Array.map(c => c.tableName)
240
302
 
241
303
  Logging.info("Tracking tables in Hasura")
242
304
 
243
305
  let _ = await clearHasuraMetadata(~endpoint, ~auth)
244
306
 
245
- await trackTables(~endpoint, ~auth, ~pgSchema, ~tableNames)
307
+ await trackTables(~endpoint, ~auth, ~pgSchema, ~tableConfigs)
246
308
 
247
309
  for i in 0 to tableNames->Array.length - 1 {
248
310
  let tableName = tableNames->Array.getUnsafe(i)
@@ -278,6 +340,7 @@ let trackDatabase = async (
278
340
  ~objectName=derivedFromField.fieldName,
279
341
  ~relationalKey=relationalFieldName,
280
342
  ~mappedEntity=derivedFromField.derivedFromEntity,
343
+ ~comment=?derivedFromField.description,
281
344
  )
282
345
  }
283
346
 
@@ -295,6 +358,7 @@ let trackDatabase = async (
295
358
  ~objectName=field.fieldName,
296
359
  ~relationalKey=field.fieldName,
297
360
  ~mappedEntity=linkedEntityName,
361
+ ~comment=?field.description,
298
362
  )
299
363
  }
300
364
  }
@@ -112,34 +112,52 @@ async function clearHasuraMetadata(endpoint, auth) {
112
112
  }
113
113
  }
114
114
 
115
- async function trackTables(endpoint, auth, pgSchema, tableNames) {
115
+ async function trackTables(endpoint, auth, pgSchema, tableConfigs) {
116
116
  try {
117
117
  let result = await Rest.fetch(trackTablesRoute, {
118
118
  auth: auth,
119
119
  args: {
120
120
  allow_warnings: false,
121
- tables: tableNames.map(tableName => ({
122
- table: {
123
- name: tableName,
124
- schema: pgSchema
125
- },
126
- configuration: {
121
+ tables: tableConfigs.map(param => {
122
+ let description = param.description;
123
+ let tableName = param.tableName;
124
+ let configuration = {
127
125
  custom_name: tableName
126
+ };
127
+ if (description !== undefined) {
128
+ configuration["comment"] = description;
128
129
  }
129
- }))
130
+ let columnConfigEntries = Object.entries(param.columnDescriptions);
131
+ if (columnConfigEntries.length !== 0) {
132
+ let columnConfig = {};
133
+ columnConfigEntries.forEach(param => {
134
+ columnConfig[param[0]] = {
135
+ comment: param[1]
136
+ };
137
+ });
138
+ configuration["column_config"] = columnConfig;
139
+ }
140
+ return {
141
+ table: {
142
+ name: tableName,
143
+ schema: pgSchema
144
+ },
145
+ configuration: configuration
146
+ };
147
+ })
130
148
  }
131
149
  }, Rest.client(endpoint, undefined));
132
150
  let msg;
133
151
  msg = result === "QuerySucceeded" ? "Hasura finished tracking tables" : "Hasura tables already tracked";
134
152
  return Logging.trace({
135
153
  msg: msg,
136
- tableNames: tableNames
154
+ tableNames: tableConfigs.map(c => c.tableName)
137
155
  });
138
156
  } catch (raw_exn) {
139
157
  let exn = Primitive_exceptions.internalToException(raw_exn);
140
158
  return Logging.error({
141
159
  msg: `There was an issue tracking tables in hasura - indexing may still work - but you may have issues querying the data in hasura.`,
142
- tableNames: tableNames,
160
+ tableNames: tableConfigs.map(c => c.tableName),
143
161
  err: Utils.prettifyExn(exn)
144
162
  });
145
163
  }
@@ -165,44 +183,81 @@ async function createSelectPermission(endpoint, auth, tableName, pgSchema, respo
165
183
  });
166
184
  }
167
185
 
168
- async function createEntityRelationship(endpoint, auth, pgSchema, tableName, relationshipType, relationalKey, objectName, mappedEntity, isDerivedFrom) {
186
+ async function createEntityRelationship(endpoint, auth, pgSchema, tableName, relationshipType, relationalKey, objectName, mappedEntity, isDerivedFrom, comment) {
169
187
  let derivedFromTo = isDerivedFrom ? `"id": "` + relationalKey + `"` : `"` + relationalKey + `_id" : "id"`;
170
- return await sendOperation(endpoint, auth, {
171
- type: `pg_create_` + relationshipType + `_relationship`,
172
- args: {
173
- table: {
188
+ let tableJson = {
189
+ schema: pgSchema,
190
+ name: tableName
191
+ };
192
+ let usingJson = {
193
+ manual_configuration: {
194
+ remote_table: {
174
195
  schema: pgSchema,
175
- name: tableName
196
+ name: mappedEntity
176
197
  },
177
- name: objectName,
178
- source: "default",
179
- using: {
180
- manual_configuration: {
181
- remote_table: {
182
- schema: pgSchema,
183
- name: mappedEntity
184
- },
185
- column_mapping: JSON.parse(`{` + derivedFromTo + `}`)
186
- }
187
- }
198
+ column_mapping: JSON.parse(`{` + derivedFromTo + `}`)
188
199
  }
200
+ };
201
+ let args = {
202
+ table: tableJson,
203
+ name: objectName,
204
+ source: "default",
205
+ using: usingJson
206
+ };
207
+ if (comment !== undefined) {
208
+ args["comment"] = comment;
209
+ }
210
+ return await sendOperation(endpoint, auth, {
211
+ type: `pg_create_` + relationshipType + `_relationship`,
212
+ args: args
189
213
  });
190
214
  }
191
215
 
192
216
  async function trackDatabase(endpoint, auth, pgSchema, userEntities, aggregateEntities, responseLimit, schema) {
193
- let exposedInternalTableNames = [
194
- InternalTable.RawEvents.table.tableName,
195
- InternalTable.Views.metaViewName,
196
- InternalTable.Views.chainMetadataViewName
217
+ let exposedInternalTableConfigs = [
218
+ {
219
+ tableName: InternalTable.RawEvents.table.tableName,
220
+ description: undefined,
221
+ columnDescriptions: {}
222
+ },
223
+ {
224
+ tableName: InternalTable.Views.metaViewName,
225
+ description: undefined,
226
+ columnDescriptions: {}
227
+ },
228
+ {
229
+ tableName: InternalTable.Views.chainMetadataViewName,
230
+ description: undefined,
231
+ columnDescriptions: {}
232
+ }
197
233
  ];
198
- let userTableNames = userEntities.map(entity => entity.table.tableName);
199
- let tableNames = Belt_Array.concatMany([
200
- exposedInternalTableNames,
201
- userTableNames
234
+ let userTableConfigs = userEntities.map(entity => {
235
+ let columnDescriptions = {};
236
+ entity.table.fields.forEach(fieldOrDerived => {
237
+ if (fieldOrDerived.TAG !== "Field") {
238
+ return;
239
+ }
240
+ let field = fieldOrDerived._0;
241
+ let d = field.description;
242
+ if (d !== undefined) {
243
+ columnDescriptions[Table.getDbFieldName(field)] = d;
244
+ return;
245
+ }
246
+ });
247
+ return {
248
+ tableName: entity.table.tableName,
249
+ description: entity.table.description,
250
+ columnDescriptions: columnDescriptions
251
+ };
252
+ });
253
+ let tableConfigs = Belt_Array.concatMany([
254
+ exposedInternalTableConfigs,
255
+ userTableConfigs
202
256
  ]);
257
+ let tableNames = tableConfigs.map(c => c.tableName);
203
258
  Logging.info("Tracking tables in Hasura");
204
259
  await clearHasuraMetadata(endpoint, auth);
205
- await trackTables(endpoint, auth, pgSchema, tableNames);
260
+ await trackTables(endpoint, auth, pgSchema, tableConfigs);
206
261
  for (let i = 0, i_finish = tableNames.length; i < i_finish; ++i) {
207
262
  let tableName = tableNames[i];
208
263
  await createSelectPermission(endpoint, auth, tableName, pgSchema, responseLimit, aggregateEntities);
@@ -215,13 +270,13 @@ async function trackDatabase(endpoint, auth, pgSchema, userEntities, aggregateEn
215
270
  for (let j = 0, j_finish = derivedFromFields.length; j < j_finish; ++j) {
216
271
  let derivedFromField = derivedFromFields[j];
217
272
  let relationalFieldName = Utils.unwrapResultExn(Schema.getDerivedFromFieldName(schema, derivedFromField));
218
- await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "array", relationalFieldName, derivedFromField.fieldName, derivedFromField.derivedFromEntity, true);
273
+ await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "array", relationalFieldName, derivedFromField.fieldName, derivedFromField.derivedFromEntity, true, derivedFromField.description);
219
274
  }
220
275
  let linkedEntityFields = Table.getLinkedEntityFields(entityConfig.table);
221
276
  for (let j$1 = 0, j_finish$1 = linkedEntityFields.length; j$1 < j_finish$1; ++j$1) {
222
277
  let match$1 = linkedEntityFields[j$1];
223
278
  let field = match$1[0];
224
- await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "object", field.fieldName, field.fieldName, match$1[1], false);
279
+ await createEntityRelationship(endpoint, auth, pgSchema, tableName$1, "object", field.fieldName, field.fieldName, match$1[1], false, field.description);
225
280
  }
226
281
  }
227
282
  return Logging.info("Hasura configuration completed");
@@ -1,42 +1,23 @@
1
- type rawEventsKey = {
2
- chainId: int,
3
- eventId: string,
4
- }
5
-
6
- let hashRawEventsKey = (key: rawEventsKey) =>
7
- EventUtils.getEventIdKeyString(~chainId=key.chainId, ~eventId=key.eventId)
8
-
9
1
  module EntityTables = {
10
- type t = dict<InMemoryTable.Entity.t<Internal.entity>>
2
+ type t = dict<InMemoryTable.Entity.t>
11
3
  exception UndefinedEntity({entityName: string})
12
4
  let make = (entities: array<Internal.entityConfig>): t => {
13
5
  let init = Dict.make()
14
- entities->Belt.Array.forEach(entityConfig => {
6
+ entities->Array.forEach(entityConfig => {
15
7
  init->Dict.set((entityConfig.name :> string), InMemoryTable.Entity.make())
16
8
  })
17
9
  init
18
10
  }
19
11
 
20
- let get = (type entity, self: t, ~entityName: string) => {
12
+ let get = (self: t, ~entityName: string) => {
21
13
  switch self->Utils.Dict.dangerouslyGetNonOption(entityName) {
22
- | Some(table) =>
23
- table->(
24
- Utils.magic: InMemoryTable.Entity.t<Internal.entity> => InMemoryTable.Entity.t<entity>
25
- )
26
-
14
+ | Some(table) => table
27
15
  | None =>
28
16
  UndefinedEntity({entityName: entityName})->ErrorHandling.mkLogAndRaise(
29
17
  ~msg="Unexpected, entity InMemoryTable is undefined",
30
18
  )
31
19
  }
32
20
  }
33
-
34
- let clone = (self: t) => {
35
- self
36
- ->Dict.toArray
37
- ->Belt.Array.map(((k, v)) => (k, v->InMemoryTable.Entity.clone))
38
- ->Dict.fromArray
39
- }
40
21
  }
41
22
 
42
23
  type effectCacheInMemTable = {
@@ -47,30 +28,29 @@ type effectCacheInMemTable = {
47
28
  }
48
29
 
49
30
  type t = {
50
- rawEvents: InMemoryTable.t<rawEventsKey, InternalTable.RawEvents.t>,
51
- entities: dict<InMemoryTable.Entity.t<Internal.entity>>,
52
- effects: dict<effectCacheInMemTable>,
53
- rollbackTargetCheckpointId: option<Internal.checkpointId>,
31
+ allEntities: array<Internal.entityConfig>,
32
+ mutable rawEvents: array<InternalTable.RawEvents.t>,
33
+ mutable entities: dict<InMemoryTable.Entity.t>,
34
+ mutable effects: dict<effectCacheInMemTable>,
35
+ mutable rollback: option<Persistence.rollback>,
36
+ mutable committedCheckpointId: Internal.checkpointId,
54
37
  }
55
38
 
56
- let make = (~entities: array<Internal.entityConfig>, ~rollbackTargetCheckpointId=?): t => {
57
- rawEvents: InMemoryTable.make(~hash=hashRawEventsKey),
39
+ let make = (
40
+ ~entities: array<Internal.entityConfig>,
41
+ ~committedCheckpointId=Internal.initialCheckpointId,
42
+ ): t => {
43
+ allEntities: entities,
44
+ rawEvents: [],
58
45
  entities: EntityTables.make(entities),
59
46
  effects: Dict.make(),
60
- rollbackTargetCheckpointId,
47
+ rollback: None,
48
+ committedCheckpointId,
61
49
  }
62
50
 
63
- let clone = (self: t) => {
64
- rawEvents: self.rawEvents->InMemoryTable.clone,
65
- entities: self.entities->EntityTables.clone,
66
- effects: Dict.mapValues(self.effects, table => {
67
- idsToStore: table.idsToStore->Array.copy,
68
- invalidationsCount: table.invalidationsCount,
69
- dict: table.dict->Utils.Dict.shallowCopy,
70
- effect: table.effect,
71
- }),
72
- rollbackTargetCheckpointId: self.rollbackTargetCheckpointId,
73
- }
51
+ // Once the store holds this many entities across all tables, we drop them
52
+ // after a batch write so it doesn't grow unbounded on long running indexers.
53
+ let keepLatestChangesLimit = 50_000.
74
54
 
75
55
  let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => {
76
56
  let key = effect.name
@@ -91,13 +71,193 @@ let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => {
91
71
  let getInMemTable = (
92
72
  inMemoryStore: t,
93
73
  ~entityConfig: Internal.entityConfig,
94
- ): InMemoryTable.Entity.t<Internal.entity> => {
74
+ ): InMemoryTable.Entity.t => {
95
75
  inMemoryStore.entities->EntityTables.get(~entityName=entityConfig.name)
96
76
  }
97
77
 
98
- let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollbackTargetCheckpointId !== None
78
+ let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollback !== None
79
+
80
+ let writeBatch = async (
81
+ inMemoryStore: t,
82
+ ~persistence: Persistence.t,
83
+ ~batch,
84
+ ~config,
85
+ ~isInReorgThreshold,
86
+ ) =>
87
+ switch persistence.storageStatus {
88
+ | Unknown
89
+ | Initializing(_) =>
90
+ JsError.throwWithMessage(`Failed to access the indexer storage. The Persistence layer is not initialized.`)
91
+ | Ready({cache}) =>
92
+ let committedCheckpointId = inMemoryStore.committedCheckpointId
93
+ // Decide before the keepMap below trims prevEntityChanges from changesCount,
94
+ // so the signal still reflects every change currently held in memory.
95
+ let keepLatestChanges = {
96
+ let totalChanges = ref(0.)
97
+ persistence.allEntities->Array.forEach(entityConfig => {
98
+ totalChanges :=
99
+ totalChanges.contents +. (inMemoryStore->getInMemTable(~entityConfig)).changesCount
100
+ })
101
+ totalChanges.contents < keepLatestChangesLimit
102
+ }
103
+ let updatedEntities = persistence.allEntities->Array.filterMap(entityConfig => {
104
+ let table = inMemoryStore->getInMemTable(~entityConfig)
105
+
106
+ // The reset below drops prevEntityChanges and we reuse the array as the
107
+ // write buffer here, so drop it from the count before appending to it.
108
+ table.changesCount = table.changesCount -. table.prevEntityChanges->Array.length->Int.toFloat
109
+ let changes = table.prevEntityChanges
110
+ table.latestEntityChangeById->Utils.Dict.forEach(change =>
111
+ if change->Change.getCheckpointId > committedCheckpointId {
112
+ changes->Array.push(change)
113
+ }
114
+ )
115
+ if changes->Utils.Array.isEmpty {
116
+ None
117
+ } else {
118
+ Some(({entityConfig, changes}: Persistence.updatedEntity))
119
+ }
120
+ })
121
+ await persistence.storage.writeBatch(
122
+ ~batch,
123
+ ~rawEvents=inMemoryStore.rawEvents,
124
+ ~rollback=inMemoryStore.rollback,
125
+ ~isInReorgThreshold,
126
+ ~config,
127
+ ~allEntities=persistence.allEntities,
128
+ ~updatedEntities,
129
+ ~updatedEffectsCache={
130
+ let acc = []
131
+ inMemoryStore.effects->Utils.Dict.forEach(inMemTable => {
132
+ let {idsToStore, dict, effect, invalidationsCount} = inMemTable
133
+ switch idsToStore {
134
+ | [] => ()
135
+ | ids =>
136
+ let items = ids->Array.map((id): Internal.effectCacheItem => {
137
+ id,
138
+ output: dict->Dict.getUnsafe(id),
139
+ })
140
+ let effectName = effect.name
141
+ let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(effectName) {
142
+ | Some(c) => c
143
+ | None =>
144
+ let c: Persistence.effectCacheRecord = {effectName, count: 0}
145
+ cache->Dict.set(effectName, c)
146
+ c
147
+ }
148
+ let shouldInitialize = effectCacheRecord.count === 0
149
+ effectCacheRecord.count =
150
+ effectCacheRecord.count + items->Array.length - invalidationsCount
151
+ Prometheus.EffectCacheCount.set(~count=effectCacheRecord.count, ~effectName)
152
+ acc
153
+ ->Array.push(({effect, items, shouldInitialize}: Persistence.updatedEffectCache))
154
+ ->ignore
155
+ }
156
+ })
157
+ acc
158
+ },
159
+ )
160
+
161
+ inMemoryStore.rawEvents = []
162
+ inMemoryStore.effects = Dict.make()
163
+ inMemoryStore.rollback = None
164
+ inMemoryStore.committedCheckpointId = switch batch.checkpointIds->Utils.Array.last {
165
+ | Some(checkpointId) => checkpointId
166
+ | None => committedCheckpointId
167
+ }
168
+ if keepLatestChanges {
169
+ persistence.allEntities->Array.forEach(entityConfig => {
170
+ let table = inMemoryStore->getInMemTable(~entityConfig)
171
+ inMemoryStore.entities->Dict.set(
172
+ (entityConfig.name :> string),
173
+ table->InMemoryTable.Entity.resetButKeepLatestChanges,
174
+ )
175
+ })
176
+ } else {
177
+ // Over the limit: drop everything written in a batch and keep only the
178
+ // entities loaded from the db, so the next batch can still read them
179
+ // without hitting the database.
180
+ let loadedFromDbCount = ref(0.)
181
+ let resetTables = persistence.allEntities->Array.map(entityConfig => {
182
+ let resetTable =
183
+ inMemoryStore
184
+ ->getInMemTable(~entityConfig)
185
+ ->InMemoryTable.Entity.resetButKeepLoadedFromDbChanges
186
+ loadedFromDbCount := loadedFromDbCount.contents +. resetTable.changesCount
187
+ resetTable
188
+ })
189
+ // Even the loaded-from-db entities alone exceed the limit, so there's no
190
+ // point keeping them around - drop everything.
191
+ let dropEverything = loadedFromDbCount.contents >= keepLatestChangesLimit
192
+ persistence.allEntities->Array.forEachWithIndex((entityConfig, idx) => {
193
+ inMemoryStore.entities->Dict.set(
194
+ (entityConfig.name :> string),
195
+ dropEverything ? InMemoryTable.Entity.make() : resetTables->Array.getUnsafe(idx),
196
+ )
197
+ })
198
+ }
199
+ }
200
+
201
+ let prepareRollbackDiff = async (
202
+ inMemoryStore: t,
203
+ ~persistence: Persistence.t,
204
+ ~rollbackTargetCheckpointId,
205
+ ~rollbackDiffCheckpointId,
206
+ ) => {
207
+ inMemoryStore.rawEvents = []
208
+ inMemoryStore.entities = EntityTables.make(inMemoryStore.allEntities)
209
+ inMemoryStore.effects = Dict.make()
210
+ inMemoryStore.rollback = Some({
211
+ targetCheckpointId: rollbackTargetCheckpointId,
212
+ diffCheckpointId: rollbackDiffCheckpointId,
213
+ })
214
+
215
+ let deletedEntities = Dict.make()
216
+ let setEntities = Dict.make()
217
+
218
+ let _ = await persistence.allEntities
219
+ ->Array.map(async entityConfig => {
220
+ let entityTable = inMemoryStore->getInMemTable(~entityConfig)
221
+
222
+ let (removedIdsResult, restoredEntitiesResult) = await persistence.storage.getRollbackData(
223
+ ~entityConfig,
224
+ ~rollbackTargetCheckpointId,
225
+ )
226
+
227
+ removedIdsResult->Array.forEach(data => {
228
+ deletedEntities->Utils.Dict.push(entityConfig.name, data["id"])
229
+ entityTable->InMemoryTable.Entity.set(
230
+ ~committedCheckpointId=inMemoryStore.committedCheckpointId,
231
+ Delete({
232
+ entityId: data["id"],
233
+ checkpointId: rollbackDiffCheckpointId,
234
+ }),
235
+ )
236
+ })
237
+
238
+ let restoredEntities = restoredEntitiesResult->S.parseOrThrow(entityConfig.rowsSchema)
239
+
240
+ restoredEntities->Array.forEach((entity: Internal.entity) => {
241
+ setEntities->Utils.Dict.push(entityConfig.name, entity.id)
242
+ entityTable->InMemoryTable.Entity.set(
243
+ ~committedCheckpointId=inMemoryStore.committedCheckpointId,
244
+ Set({
245
+ entityId: entity.id,
246
+ checkpointId: rollbackDiffCheckpointId,
247
+ entity,
248
+ }),
249
+ )
250
+ })
251
+ })
252
+ ->Promise.all
253
+
254
+ {
255
+ "deletedEntities": deletedEntities,
256
+ "setEntities": setEntities,
257
+ }
258
+ }
99
259
 
100
- let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => {
260
+ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t) => {
101
261
  let inMemTable =
102
262
  inMemoryStore->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig)
103
263
 
@@ -126,12 +286,12 @@ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => {
126
286
  }
127
287
 
128
288
  inMemTable->InMemoryTable.Entity.set(
289
+ ~committedCheckpointId=inMemoryStore.committedCheckpointId,
129
290
  Set({
130
291
  entityId: entity.id,
131
292
  checkpointId,
132
293
  entity: entity->InternalTable.EnvioAddresses.castToInternal,
133
294
  }),
134
- ~shouldSaveHistory,
135
295
  )
136
296
  }
137
297
  }