envio 3.15.0-subgraph → 3.16.0-subgraph

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 (114) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +3 -3
  3. package/fuel.schema.json +2 -2
  4. package/index.d.ts +3 -3
  5. package/licenses/README.md +1 -1
  6. package/package.json +8 -8
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/ChainFetching.res +1 -6
  11. package/src/ChainFetching.res.mjs +1 -1
  12. package/src/ChainState.res +48 -7
  13. package/src/ChainState.res.mjs +21 -8
  14. package/src/Clock.res +57 -0
  15. package/src/Clock.res.mjs +72 -0
  16. package/src/Config.res +16 -4
  17. package/src/Config.res.mjs +5 -3
  18. package/src/Core.res +4 -40
  19. package/src/CrossChainState.res +10 -6
  20. package/src/CrossChainState.res.mjs +1 -1
  21. package/src/EffectState.res +10 -2
  22. package/src/EffectState.res.mjs +8 -4
  23. package/src/EffectState.resi +3 -1
  24. package/src/EntityId.res +2 -7
  25. package/src/EntityId.res.mjs +1 -8
  26. package/src/EntityTables.res +4 -1
  27. package/src/EntityTables.res.mjs +1 -1
  28. package/src/Env.res +4 -0
  29. package/src/Env.res.mjs +5 -0
  30. package/src/FetchState.res +157 -69
  31. package/src/FetchState.res.mjs +112 -47
  32. package/src/FinalizeBackfill.res +2 -4
  33. package/src/FinalizeBackfill.res.mjs +3 -4
  34. package/src/InMemoryTable.res +17 -16
  35. package/src/InMemoryTable.res.mjs +14 -15
  36. package/src/IndexerState.res +28 -4
  37. package/src/IndexerState.res.mjs +19 -6
  38. package/src/IndexerState.resi +4 -0
  39. package/src/Internal.res +0 -49
  40. package/src/Internal.res.mjs +1 -80
  41. package/src/LoadLayer.res +17 -10
  42. package/src/LoadLayer.res.mjs +11 -8
  43. package/src/Main.res.mjs +1 -1
  44. package/src/Persistence.res +3 -23
  45. package/src/Persistence.res.mjs +3 -3
  46. package/src/PreloadLookahead.res +42 -24
  47. package/src/PreloadLookahead.res.mjs +51 -30
  48. package/src/Rollback.res +0 -1
  49. package/src/Rollback.res.mjs +1 -1
  50. package/src/SafeCheckpointTracking.res +4 -4
  51. package/src/SafeCheckpointTracking.res.mjs +1 -1
  52. package/src/StagedRows.res +106 -0
  53. package/src/StagedRows.res.mjs +102 -0
  54. package/src/Staging.res +32 -14
  55. package/src/Staging.res.mjs +17 -20
  56. package/src/Storage.res +278 -973
  57. package/src/Storage.res.mjs +218 -720
  58. package/src/Supervisor.res +75 -56
  59. package/src/Supervisor.res.mjs +55 -26
  60. package/src/SyncWrites.res +24 -18
  61. package/src/SyncWrites.res.mjs +11 -11
  62. package/src/TestIndexer.res +22 -27
  63. package/src/TestIndexer.res.mjs +20 -17
  64. package/src/Throttler.res +15 -8
  65. package/src/Throttler.res.mjs +9 -6
  66. package/src/Throttler.resi +1 -1
  67. package/src/UserContext.res +13 -4
  68. package/src/UserContext.res.mjs +5 -5
  69. package/src/Utils.res +0 -6
  70. package/src/Utils.res.mjs +0 -5
  71. package/src/Writing.res +0 -1
  72. package/src/Writing.res.mjs +1 -1
  73. package/src/bindings/ClickHouse.res +0 -322
  74. package/src/bindings/ClickHouse.res.mjs +0 -338
  75. package/src/bindings/StorageClient.res +107 -174
  76. package/src/bindings/StorageClient.res.mjs +19 -8
  77. package/src/bindings/Vitest.res +0 -26
  78. package/src/bindings/Vitest.res.mjs +0 -3
  79. package/src/db/Table.res +24 -150
  80. package/src/db/Table.res.mjs +38 -272
  81. package/src/sources/ChainSources.res +2 -0
  82. package/src/sources/ChainSources.res.mjs +2 -2
  83. package/src/sources/EvmChain.res +3 -0
  84. package/src/sources/EvmChain.res.mjs +4 -2
  85. package/src/sources/EvmHyperSyncSource.res +2 -2
  86. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  87. package/src/sources/EvmRpcClient.res +13 -3
  88. package/src/sources/EvmRpcClient.res.mjs +4 -3
  89. package/src/sources/EvmRpcWs.res +4 -3
  90. package/src/sources/EvmRpcWs.res.mjs +2 -2
  91. package/src/sources/HeightFeed.res +31 -10
  92. package/src/sources/HeightFeed.res.mjs +10 -6
  93. package/src/sources/HeightStream.res +32 -17
  94. package/src/sources/HeightStream.res.mjs +10 -10
  95. package/src/sources/HyperSyncSSE.res +4 -6
  96. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  97. package/src/sources/RpcSource.res +10 -1
  98. package/src/sources/RpcSource.res.mjs +4 -3
  99. package/src/sources/Source.res +3 -0
  100. package/src/sources/SourceManager.res +85 -104
  101. package/src/sources/SourceManager.res.mjs +18 -37
  102. package/src/sources/SourceManager.resi +3 -5
  103. package/src/sources/StartBlockResolver.res +1 -0
  104. package/src/sources/StartBlockResolver.res.mjs +4 -3
  105. package/src/sources/SvmHyperSyncSource.res +2 -2
  106. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  107. package/src/subgraph/graph-ts.ts +87 -28
  108. package/src/subgraph/runtime.ts +0 -1
  109. package/src/subgraph/scope.ts +0 -1
  110. package/svm.schema.json +3 -3
  111. package/src/PgWriting.res +0 -36
  112. package/src/PgWriting.res.mjs +0 -34
  113. package/src/db/IndexDefinition.res +0 -47
  114. package/src/db/IndexDefinition.res.mjs +0 -60
@@ -27,7 +27,7 @@ async function runOnce(state) {
27
27
  let persistence = IndexerState.persistence(state);
28
28
  let storage = Persistence.getInitializedStorageOrThrow(persistence);
29
29
  let readyAt = new Date();
30
- await storage.finalizeBackfill(persistence.allEntities, chainIds, readyAt);
30
+ await storage.finalizeBackfill(chainIds, readyAt);
31
31
  return IndexerState.markReady(state, readyAt);
32
32
  }
33
33
 
@@ -42,9 +42,8 @@ function run(state) {
42
42
  }
43
43
 
44
44
  function repairSchemaIndexes(state) {
45
- let persistence = IndexerState.persistence(state);
46
- let storage = Persistence.getInitializedStorageOrThrow(persistence);
47
- return storage.ensureSchemaIndexes(persistence.allEntities, CrossChainState.chainIds(IndexerState.crossChainState(state)));
45
+ let storage = Persistence.getInitializedStorageOrThrow(IndexerState.persistence(state));
46
+ return storage.ensureSchemaIndexes(CrossChainState.chainIds(IndexerState.crossChainState(state)));
48
47
  }
49
48
 
50
49
  export {
@@ -22,6 +22,8 @@ module Entity = {
22
22
  }
23
23
 
24
24
  type t = {
25
+ // Keys every id dict and Set below; see Table.table.
26
+ entityIdToKey: EntityId.t => string,
25
27
  latestEntityChangeById: dict<Change.t<Internal.entity>>,
26
28
  // Recorded changes (new latest ids + prevEntityChanges pushes), tracked
27
29
  // manually so InMemoryStore can gauge size without scanning every dict.
@@ -45,12 +47,9 @@ module Entity = {
45
47
  mutable hasIndexes: bool,
46
48
  }
47
49
 
48
- // Helper to extract an entity's id as a dict key. The raw id may be a
49
- // string/int/bigint, so it's stringified to a stable key for in-memory
50
- // indexing.
51
- let getEntityIdUnsafe = (entity: Internal.entity): string =>
50
+ let getEntityKeyUnsafe = (self: t, entity: Internal.entity): string =>
52
51
  switch (entity->(Utils.magic: Internal.entity => {"id": option<EntityId.t>}))["id"] {
53
- | Some(id) => id->EntityId.toKey
52
+ | Some(id) => self.entityIdToKey(id)
54
53
  | None => JsError.throwWithMessage("Property 'id' does not exist on expected entity object")
55
54
  }
56
55
 
@@ -62,7 +61,8 @@ module Entity = {
62
61
  }
63
62
  }
64
63
 
65
- let make = (): t => {
64
+ let make = (~entityIdToKey): t => {
65
+ entityIdToKey,
66
66
  latestEntityChangeById: Dict.make(),
67
67
  changesCount: 0.,
68
68
  dirtyIds: Utils.Set.make(),
@@ -141,10 +141,8 @@ module Entity = {
141
141
  self.hasIndexes = false
142
142
  }
143
143
 
144
- let updateIndexes = (self: t, ~entity: Internal.entity) =>
144
+ let updateIndexes = (self: t, ~entityId: string, ~entity: Internal.entity) =>
145
145
  if self.hasIndexes {
146
- let entityId = entity->getEntityIdUnsafe
147
-
148
146
  switch self.indexesByEntityId->Utils.Dict.dangerouslyGetNonOption(entityId) {
149
147
  | None => ()
150
148
  | Some(indexes) =>
@@ -185,7 +183,7 @@ module Entity = {
185
183
  }
186
184
 
187
185
  let set = (inMemTable: t, ~committedCheckpointId, change: Change.t<Internal.entity>) => {
188
- let entityKey = change->Change.getEntityId->EntityId.toKey
186
+ let entityKey = inMemTable.entityIdToKey(change->Change.getEntityId)
189
187
  switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(entityKey) {
190
188
  | Some(prev) =>
191
189
  let prevCheckpointId = prev->Change.getCheckpointId
@@ -200,8 +198,8 @@ module Entity = {
200
198
  }
201
199
 
202
200
  switch change {
203
- | Set({entity}) => inMemTable->updateIndexes(~entity)
204
- | Delete({entityId}) => inMemTable->deleteEntityFromIndexes(~entityId=entityId->EntityId.toKey)
201
+ | Set({entity}) => inMemTable->updateIndexes(~entityId=entityKey, ~entity)
202
+ | Delete(_) => inMemTable->deleteEntityFromIndexes(~entityId=entityKey)
205
203
  }
206
204
  inMemTable.latestEntityChangeById->Dict.set(entityKey, change)
207
205
  if change->Change.getCheckpointId > committedCheckpointId {
@@ -214,11 +212,14 @@ module Entity = {
214
212
  let initValue = (
215
213
  inMemTable: t,
216
214
  ~committedCheckpointId,
217
- ~key: string,
215
+ ~entityId: EntityId.t,
218
216
  ~entity: option<Internal.entity>,
219
217
  ) =>
220
- if inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(key)->Option.isNone {
221
- let entityId = key->EntityId.unsafeOfString
218
+ if (
219
+ inMemTable.latestEntityChangeById
220
+ ->Utils.Dict.dangerouslyGetNonOption(inMemTable.entityIdToKey(entityId))
221
+ ->Option.isNone
222
+ ) {
222
223
  let change: Change.t<Internal.entity> = switch entity {
223
224
  | Some(entity) => Set({entityId, entity, checkpointId: Internal.loadedFromDbCheckpointId})
224
225
  | None => Delete({entityId, checkpointId: Internal.loadedFromDbCheckpointId})
@@ -312,7 +313,7 @@ module Entity = {
312
313
  switch change->mapChangeToEntity {
313
314
  | Some(entity) =>
314
315
  if index.matcher(entity) {
315
- inMemTable->addToIndex(~index, ~entityId=entity->getEntityIdUnsafe)
316
+ inMemTable->addToIndex(~index, ~entityId=inMemTable->getEntityKeyUnsafe(entity))
316
317
  }
317
318
  | None => ()
318
319
  }
@@ -1,7 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Utils from "./Utils.res.mjs";
4
- import * as EntityId from "./EntityId.res.mjs";
5
4
  import * as Internal from "./Internal.res.mjs";
6
5
  import * as EntityFilter from "./db/EntityFilter.res.mjs";
7
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
@@ -9,10 +8,10 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
9
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
10
 
12
- function getEntityIdUnsafe(entity) {
11
+ function getEntityKeyUnsafe(self, entity) {
13
12
  let id = entity.id;
14
13
  if (id !== undefined) {
15
- return EntityId.toKey(Primitive_option.valFromOption(id));
14
+ return self.entityIdToKey(Primitive_option.valFromOption(id));
16
15
  } else {
17
16
  return Stdlib_JsError.throwWithMessage("Property 'id' does not exist on expected entity object");
18
17
  }
@@ -28,8 +27,9 @@ function addToIndex(self, index, entityId) {
28
27
  }
29
28
  }
30
29
 
31
- function make() {
30
+ function make(entityIdToKey) {
32
31
  return {
32
+ entityIdToKey: entityIdToKey,
33
33
  latestEntityChangeById: {},
34
34
  changesCount: 0,
35
35
  dirtyIds: new Set(),
@@ -99,11 +99,10 @@ function dropCommittedChanges(self, committedCheckpointId, keepLoadedFromDb) {
99
99
  self.hasIndexes = false;
100
100
  }
101
101
 
102
- function updateIndexes(self, entity) {
102
+ function updateIndexes(self, entityId, entity) {
103
103
  if (!self.hasIndexes) {
104
104
  return;
105
105
  }
106
- let entityId = getEntityIdUnsafe(entity);
107
106
  let indexes = self.indexesByEntityId[entityId];
108
107
  if (indexes !== undefined) {
109
108
  let indexes$1 = Primitive_option.valFromOption(indexes);
@@ -144,7 +143,7 @@ function deleteEntityFromIndexes(self, entityId) {
144
143
  }
145
144
 
146
145
  function set(inMemTable, committedCheckpointId, change) {
147
- let entityKey = EntityId.toKey(change.entityId);
146
+ let entityKey = inMemTable.entityIdToKey(change.entityId);
148
147
  let prev = inMemTable.latestEntityChangeById[entityKey];
149
148
  if (prev !== undefined) {
150
149
  let prevCheckpointId = prev.checkpointId;
@@ -156,9 +155,9 @@ function set(inMemTable, committedCheckpointId, change) {
156
155
  inMemTable.changesCount = inMemTable.changesCount + 1;
157
156
  }
158
157
  if (change.type === "SET") {
159
- updateIndexes(inMemTable, change.entity);
158
+ updateIndexes(inMemTable, entityKey, change.entity);
160
159
  } else {
161
- deleteEntityFromIndexes(inMemTable, EntityId.toKey(change.entityId));
160
+ deleteEntityFromIndexes(inMemTable, entityKey);
162
161
  }
163
162
  inMemTable.latestEntityChangeById[entityKey] = change;
164
163
  if (change.checkpointId > committedCheckpointId) {
@@ -167,18 +166,18 @@ function set(inMemTable, committedCheckpointId, change) {
167
166
  }
168
167
  }
169
168
 
170
- function initValue(inMemTable, committedCheckpointId, key, entity) {
171
- if (!Stdlib_Option.isNone(inMemTable.latestEntityChangeById[key])) {
169
+ function initValue(inMemTable, committedCheckpointId, entityId, entity) {
170
+ if (!Stdlib_Option.isNone(inMemTable.latestEntityChangeById[inMemTable.entityIdToKey(entityId)])) {
172
171
  return;
173
172
  }
174
173
  let change = entity !== undefined ? ({
175
174
  type: "SET",
176
- entityId: key,
175
+ entityId: entityId,
177
176
  entity: entity,
178
177
  checkpointId: Internal.loadedFromDbCheckpointId
179
178
  }) : ({
180
179
  type: "DELETE",
181
- entityId: key,
180
+ entityId: entityId,
182
181
  checkpointId: Internal.loadedFromDbCheckpointId
183
182
  });
184
183
  set(inMemTable, committedCheckpointId, change);
@@ -285,7 +284,7 @@ function addEmptyIndex(inMemTable, filter, table) {
285
284
  Utils.Dict.forEach(inMemTable.latestEntityChangeById, change => {
286
285
  let entity = mapChangeToEntity(change);
287
286
  if (entity !== undefined && index_matcher(entity)) {
288
- return addToIndex(inMemTable, index, getEntityIdUnsafe(entity));
287
+ return addToIndex(inMemTable, index, getEntityKeyUnsafe(inMemTable, entity));
289
288
  }
290
289
  });
291
290
  }
@@ -321,7 +320,7 @@ function tryIndexFromLoadedValues(inMemTable, filter, table) {
321
320
  }
322
321
 
323
322
  let Entity = {
324
- getEntityIdUnsafe: getEntityIdUnsafe,
323
+ getEntityKeyUnsafe: getEntityKeyUnsafe,
325
324
  addToIndex: addToIndex,
326
325
  make: make,
327
326
  snapshotChanges: snapshotChanges,
@@ -108,6 +108,12 @@ type t = {
108
108
  // in-process test runner injects a callback that resolves its run promise
109
109
  // instead, so a caught-up run doesn't kill the test process.
110
110
  onExit: option<unit => unit>,
111
+ // Told, in the processing pass that gets it there, that a run holding its
112
+ // realtime transitions has gone as far as it can on its own. The test
113
+ // runner's stand-in supervisor releases from it, so the release lands at a
114
+ // point in the indexer's own progress rather than on a timer.
115
+ onArrivedAtHead: option<unit => unit>,
116
+ mutable reportedArrivalAtHead: bool,
111
117
  // Set once on any fatal error. Every loop checks it to stop iterating and
112
118
  // every launch skips when it's set, so a single failure quiesces the indexer.
113
119
  mutable isStopped: bool,
@@ -157,10 +163,13 @@ let make = (
157
163
  ~exitAfterFirstEventBlock=false,
158
164
  ~onError: ErrorHandling.t => unit,
159
165
  ~onExit=?,
166
+ ~onArrivedAtHead=?,
167
+ ~clock=Clock.real,
160
168
  ) => {
161
169
  let chainMetaThrottler = {
162
170
  let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis
163
171
  Throttler.make(
172
+ ~clock,
164
173
  ~intervalMillis,
165
174
  ~logger=logger->Logger.withFields({
166
175
  "context": "Throttler for chain metadata writes",
@@ -175,7 +184,7 @@ let make = (
175
184
  persistence,
176
185
  allEntities: persistence.allEntities,
177
186
  entities: EntityTables.make(persistence.allEntities->EntityTables.crossChain),
178
- effectState: EffectState.make(),
187
+ effectState: EffectState.make(~clock, ()),
179
188
  rollback: None,
180
189
  committedFrontier,
181
190
  processedFrontier: committedFrontier->Frontier.copy,
@@ -205,6 +214,8 @@ let make = (
205
214
  exitAfterFirstEventBlock,
206
215
  onError,
207
216
  onExit,
217
+ onArrivedAtHead,
218
+ reportedArrivalAtHead: false,
208
219
  isStopped: false,
209
220
  epoch: 0,
210
221
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
@@ -225,12 +236,13 @@ let make = (
225
236
  }
226
237
 
227
238
  // Check if progress is past the reorg threshold (safe block).
228
- // A chain is in reorg threshold when progressBlockNumber > sourceBlockNumber - maxReorgDepth.
229
- // This matches the logic in InternalTable.Checkpoints.makeGetReorgCheckpointsQuery.
239
+ // A chain is in reorg threshold once its progress passes the safe block,
240
+ // sourceBlockNumber - maxReorgDepth - 1. This matches the reorg checkpoints
241
+ // query in packages/cli/src/postgres/internal.rs.
230
242
  let isProgressInReorgThreshold = (~progressBlockNumber, ~sourceBlockNumber, ~maxReorgDepth) => {
231
243
  maxReorgDepth > 0 &&
232
244
  sourceBlockNumber > 0 &&
233
- progressBlockNumber > sourceBlockNumber - maxReorgDepth
245
+ progressBlockNumber > sourceBlockNumber - maxReorgDepth - 1
234
246
  }
235
247
 
236
248
  let makeFromDbState = (
@@ -249,6 +261,7 @@ let makeFromDbState = (
249
261
  ~holdRealtime=false,
250
262
  ~onError,
251
263
  ~onExit=?,
264
+ ~onArrivedAtHead=?,
252
265
  ) => {
253
266
  let isInReorgThreshold = if initialState.cleanRun {
254
267
  false
@@ -303,6 +316,8 @@ let makeFromDbState = (
303
316
  ~exitAfterFirstEventBlock,
304
317
  ~onError,
305
318
  ~onExit?,
319
+ ~onArrivedAtHead?,
320
+ ~clock=?sourceTiming->Option.flatMap(timing => timing.clock),
306
321
  )
307
322
  state.crossChainState->CrossChainState.markCaughtUpOnResume
308
323
  initialState.cache->Utils.Dict.forEach(({effectName, count, scope}) => {
@@ -559,6 +574,15 @@ let isHoldingRealtime = (state: t) => state.crossChainState->CrossChainState.isH
559
574
 
560
575
  let hasArrivedAtHead = (state: t) => state.crossChainState->CrossChainState.hasArrivedAtHead
561
576
 
577
+ let reportArrivalAtHead = (state: t) =>
578
+ switch state.onArrivedAtHead {
579
+ | Some(onArrivedAtHead)
580
+ if !state.reportedArrivalAtHead && state->isHoldingRealtime && state->hasArrivedAtHead =>
581
+ state.reportedArrivalAtHead = true
582
+ onArrivedAtHead()
583
+ | _ => ()
584
+ }
585
+
562
586
  let releaseRealtime = (state: t) => {
563
587
  state.crossChainState->CrossChainState.releaseRealtime
564
588
  // Every chain is parked at the head with no batch coming, so nothing would
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Env from "./Env.res.mjs";
4
4
  import * as Batch from "./Batch.res.mjs";
5
+ import * as Clock from "./Clock.res.mjs";
5
6
  import * as Utils from "./Utils.res.mjs";
6
7
  import * as Config from "./Config.res.mjs";
7
8
  import * as Logger from "./Logger.res.mjs";
@@ -26,25 +27,26 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
26
27
  import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
27
28
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
28
29
 
29
- function make(config, logger, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
30
+ function make(config, logger, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit, onArrivedAtHead, clockOpt) {
30
31
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
31
32
  let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
32
33
  let committedFrontier = committedFrontierOpt !== undefined ? committedFrontierOpt : Frontier.empty();
33
34
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
34
35
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
35
36
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
37
+ let clock = clockOpt !== undefined ? clockOpt : Clock.real;
36
38
  let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis;
37
39
  let chainMetaThrottler = Throttler.make(intervalMillis, Logger.withFields(logger, {
38
40
  context: "Throttler for chain metadata writes",
39
41
  intervalMillis: intervalMillis
40
- }));
42
+ }), clock);
41
43
  return {
42
44
  config: config,
43
45
  logger: logger,
44
46
  persistence: persistence,
45
47
  allEntities: persistence.allEntities,
46
48
  entities: EntityTables.make(EntityTables.crossChain(persistence.allEntities)),
47
- effectState: EffectState.make(),
49
+ effectState: EffectState.make(clock, undefined),
48
50
  rollback: undefined,
49
51
  committedFrontier: committedFrontier,
50
52
  processedFrontier: Frontier.copy(committedFrontier),
@@ -68,6 +70,8 @@ function make(config, logger, persistence, chainStates, isRealtime, targetBuffer
68
70
  exitAfterFirstEventBlock: exitAfterFirstEventBlock,
69
71
  onError: onError,
70
72
  onExit: onExit,
73
+ onArrivedAtHead: onArrivedAtHead,
74
+ reportedArrivalAtHead: false,
71
75
  isStopped: false,
72
76
  epoch: 0,
73
77
  scheduleProcessing: () => {},
@@ -88,7 +92,7 @@ function make(config, logger, persistence, chainStates, isRealtime, targetBuffer
88
92
  };
89
93
  }
90
94
 
91
- function makeFromDbState(config, logger, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, sourceTiming, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit) {
95
+ function makeFromDbState(config, logger, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, sourceTiming, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit, onArrivedAtHead) {
92
96
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
93
97
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
94
98
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
@@ -99,7 +103,7 @@ function makeFromDbState(config, logger, persistence, initialState, registration
99
103
  let sourceBlockNumber = resumedChainState.sourceBlockNumber;
100
104
  let maxReorgDepth = resumedChainState.maxReorgDepth;
101
105
  if (maxReorgDepth > 0 && sourceBlockNumber > 0) {
102
- return progressBlockNumber > (sourceBlockNumber - maxReorgDepth | 0);
106
+ return progressBlockNumber > ((sourceBlockNumber - maxReorgDepth | 0) - 1 | 0);
103
107
  } else {
104
108
  return false;
105
109
  }
@@ -111,7 +115,7 @@ function makeFromDbState(config, logger, persistence, initialState, registration
111
115
  let chainConfig = ChainMap.get(config.chainMap, chainId);
112
116
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, initialState.contractMapping, registrationsByChainId, logger, sourceTiming);
113
117
  });
114
- let state = make(config, logger, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
118
+ let state = make(config, logger, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit, onArrivedAtHead, Stdlib_Option.flatMap(sourceTiming, timing => timing.clock));
115
119
  CrossChainState.markCaughtUpOnResume(state.crossChainState);
116
120
  Utils.Dict.forEach(initialState.cache, param => {
117
121
  let count = param.count;
@@ -401,6 +405,14 @@ function hasArrivedAtHead(state) {
401
405
  return CrossChainState.hasArrivedAtHead(state.crossChainState);
402
406
  }
403
407
 
408
+ function reportArrivalAtHead(state) {
409
+ let onArrivedAtHead = state.onArrivedAtHead;
410
+ if (onArrivedAtHead !== undefined && !state.reportedArrivalAtHead && CrossChainState.isHoldingRealtime(state.crossChainState) && CrossChainState.hasArrivedAtHead(state.crossChainState)) {
411
+ state.reportedArrivalAtHead = true;
412
+ return onArrivedAtHead();
413
+ }
414
+ }
415
+
404
416
  function releaseRealtime(state) {
405
417
  CrossChainState.releaseRealtime(state.crossChainState);
406
418
  state.scheduleProcessing();
@@ -917,6 +929,7 @@ export {
917
929
  bindScheduleProcessing,
918
930
  isHoldingRealtime,
919
931
  hasArrivedAtHead,
932
+ reportArrivalAtHead,
920
933
  releaseRealtime,
921
934
  markReady,
922
935
  reportFinished,
@@ -24,6 +24,8 @@ let make: (
24
24
  ~exitAfterFirstEventBlock: bool=?,
25
25
  ~onError: ErrorHandling.t => unit,
26
26
  ~onExit: unit => unit=?,
27
+ ~onArrivedAtHead: unit => unit=?,
28
+ ~clock: Clock.t=?,
27
29
  ) => t
28
30
 
29
31
  let makeFromDbState: (
@@ -40,6 +42,7 @@ let makeFromDbState: (
40
42
  ~holdRealtime: bool=?,
41
43
  ~onError: ErrorHandling.t => unit,
42
44
  ~onExit: unit => unit=?,
45
+ ~onArrivedAtHead: unit => unit=?,
43
46
  ) => t
44
47
 
45
48
  let unexpectedErrorMsg: string
@@ -106,6 +109,7 @@ let shouldFinalizeIndexes: t => bool
106
109
  let bindScheduleProcessing: (t, unit => unit) => unit
107
110
  let isHoldingRealtime: t => bool
108
111
  let hasArrivedAtHead: t => bool
112
+ let reportArrivalAtHead: t => unit
109
113
  // The supervisor's go-ahead for a process driving part of a split run.
110
114
  let releaseRealtime: t => unit
111
115
  let markReady: (t, ~readyAt: Date.t) => unit
package/src/Internal.res CHANGED
@@ -1011,55 +1011,6 @@ module EffectCache = {
1011
1011
  | CrossChain => cacheTablePrefix ++ effectName
1012
1012
  | Chain(chainId) => `envio_${chainId->ChainId.toString}_effect_${effectName}`
1013
1013
  }
1014
-
1015
- // Only accepts a canonical decimal chain id ("7", not "007" or "1foo") —
1016
- // the schema's parser follows parseFloat semantics and accepts both.
1017
- let parseChainId = str =>
1018
- switch try Some(str->ChainId.normalizeOrThrow) catch {
1019
- | _ => None
1020
- } {
1021
- | Some(chainId) if chainId->ChainId.toString === str => Some(chainId)
1022
- | _ => None
1023
- }
1024
-
1025
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/
1026
- let crossChainRe = /^envio_effect_(.+)$/
1027
-
1028
- // Inverse of toTableName. Returns None for any table name that isn't a cache
1029
- // table. Chain-scoped is tried first: the `_effect_` separator keeps effect
1030
- // names that themselves start with digits unambiguous.
1031
- let fromTableName = (tableName): option<(string, chainScope)> =>
1032
- switch RegExp.exec(chainScopedRe, tableName) {
1033
- | Some(result) =>
1034
- switch (
1035
- RegExp.Result.matches(result)->Array.get(0),
1036
- RegExp.Result.matches(result)->Array.get(1),
1037
- ) {
1038
- | (Some(Some(chainIdStr)), Some(Some(effectName))) =>
1039
- switch parseChainId(chainIdStr) {
1040
- | Some(chainId) => Some((effectName, Chain(chainId)))
1041
- | None => None
1042
- }
1043
- | _ => None
1044
- }
1045
- | None =>
1046
- switch RegExp.exec(crossChainRe, tableName) {
1047
- | Some(result) =>
1048
- switch RegExp.Result.matches(result)->Array.get(0) {
1049
- | Some(Some(effectName)) => Some((effectName, CrossChain))
1050
- | _ => None
1051
- }
1052
- | None => None
1053
- }
1054
- }
1055
-
1056
- // Relative posix path within .envio/cache. Chain-scoped caches live one
1057
- // directory level deep, named by chain id.
1058
- let toCachePath = (~effectName, ~scope) =>
1059
- switch scope {
1060
- | CrossChain => effectName ++ ".tsv"
1061
- | Chain(chainId) => `${chainId->ChainId.toString}/${effectName}.tsv`
1062
- }
1063
1014
  }
1064
1015
 
1065
1016
  let cacheOutputSchema = S.json(~validate=false)->(Utils.magic: S.t<JSON.t> => S.t<effectOutput>)
@@ -271,87 +271,8 @@ function toTableName(effectName, scope) {
271
271
  }
272
272
  }
273
273
 
274
- function parseChainId(str) {
275
- let chainId;
276
- try {
277
- chainId = Primitive_option.some(ChainId.normalizeOrThrow(str));
278
- } catch (exn) {
279
- chainId = undefined;
280
- }
281
- if (chainId === undefined) {
282
- return;
283
- }
284
- let chainId$1 = Primitive_option.valFromOption(chainId);
285
- if (ChainId.toString(chainId$1) === str) {
286
- return Primitive_option.some(chainId$1);
287
- }
288
- }
289
-
290
- let chainScopedRe = /^envio_([0-9]+)_effect_(.+)$/;
291
-
292
- let crossChainRe = /^envio_effect_(.+)$/;
293
-
294
- function fromTableName(tableName) {
295
- let result = chainScopedRe.exec(tableName);
296
- if (result == null) {
297
- let result$1 = crossChainRe.exec(tableName);
298
- if (result$1 == null) {
299
- return;
300
- }
301
- let match = result$1.slice(1)[0];
302
- if (match === undefined) {
303
- return;
304
- }
305
- let effectName = Primitive_option.valFromOption(match);
306
- if (effectName !== undefined) {
307
- return [
308
- effectName,
309
- "crossChain"
310
- ];
311
- } else {
312
- return;
313
- }
314
- }
315
- let match$1 = result.slice(1)[0];
316
- let match$2 = result.slice(1)[1];
317
- if (match$1 === undefined) {
318
- return;
319
- }
320
- let chainIdStr = Primitive_option.valFromOption(match$1);
321
- if (chainIdStr === undefined) {
322
- return;
323
- }
324
- if (match$2 === undefined) {
325
- return;
326
- }
327
- let effectName$1 = Primitive_option.valFromOption(match$2);
328
- if (effectName$1 === undefined) {
329
- return;
330
- }
331
- let chainId = parseChainId(chainIdStr);
332
- if (chainId !== undefined) {
333
- return [
334
- effectName$1,
335
- Primitive_option.valFromOption(chainId)
336
- ];
337
- }
338
- }
339
-
340
- function toCachePath(effectName, scope) {
341
- if (scope === "crossChain") {
342
- return effectName + ".tsv";
343
- } else {
344
- return ChainId.toString(scope) + `/` + effectName + `.tsv`;
345
- }
346
- }
347
-
348
274
  let EffectCache = {
349
- toTableName: toTableName,
350
- parseChainId: parseChainId,
351
- chainScopedRe: chainScopedRe,
352
- crossChainRe: crossChainRe,
353
- fromTableName: fromTableName,
354
- toCachePath: toCachePath
275
+ toTableName: toTableName
355
276
  };
356
277
 
357
278
  let cacheOutputSchema = S$RescriptSchema.json(false);
package/src/LoadLayer.res CHANGED
@@ -16,7 +16,12 @@ let loadById = (
16
16
  ~entityId,
17
17
  ) => {
18
18
  let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
19
- switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(entityId) {
19
+ // The ids here are typed as strings but hold whatever scalar the id is.
20
+ let entityIdToKey =
21
+ inMemTable.entityIdToKey->(Utils.magic: (EntityId.t => string) => string => string)
22
+ switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(
23
+ entityIdToKey(entityId),
24
+ ) {
20
25
  // A row already in memory is final, so even a grouped call skips the load
21
26
  // manager: grouping it would only resolve it a tick later to the same value.
22
27
  | Some(change) => Promise.resolve(change->InMemoryTable.Entity.mapChangeToEntity)
@@ -53,14 +58,14 @@ let loadById = (
53
58
  //Set the entity in the in memory store
54
59
  for idx in 0 to dbEntities->Array.length - 1 {
55
60
  let entity = dbEntities->Array.getUnsafe(idx)
56
- entitiesMap->Dict.set(entity.id, entity)
61
+ entitiesMap->Dict.set(entityIdToKey(entity.id), entity)
57
62
  }
58
63
  let committedCheckpointId = indexerState->IndexerState.committedCheckpointIdFor(~scope)
59
64
  idsToLoad->Array.forEach(entityId => {
60
65
  inMemTable->InMemoryTable.Entity.initValue(
61
66
  ~committedCheckpointId,
62
- ~key=entityId,
63
- ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityId),
67
+ ~entityId=entityId->EntityId.unsafeOfString,
68
+ ~entity=entitiesMap->Utils.Dict.dangerouslyGetNonOption(entityIdToKey(entityId)),
64
69
  )
65
70
  })
66
71
 
@@ -77,7 +82,7 @@ let loadById = (
77
82
  ~key,
78
83
  ~load,
79
84
  ~shouldGroup,
80
- ~hasher=LoadManager.noopHasher,
85
+ ~hasher=entityIdToKey,
81
86
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafe,
82
87
  ~hasInMemory=hash => inMemTable.latestEntityChangeById->Dict.has(hash),
83
88
  ~input=entityId,
@@ -140,7 +145,7 @@ let rec executeWithRateLimit = (
140
145
  }
141
146
 
142
147
  | Some(state) =>
143
- let now = Date.now()
148
+ let now = state.clock.now()
144
149
 
145
150
  // Check if we need to reset the window
146
151
  if now >= state.windowStartTime +. state.durationMs->Int.toFloat {
@@ -188,7 +193,8 @@ let rec executeWithRateLimit = (
188
193
  | None =>
189
194
  millisUntilReset :=
190
195
  (state.windowStartTime +. state.durationMs->Int.toFloat -. now)->Float.toInt
191
- let p = Utils.delay(millisUntilReset.contents)
196
+ let p =
197
+ state.clock->Clock.delay(~label="effect rate limit window", millisUntilReset.contents)
192
198
  state.nextWindowPromise = Some(p)
193
199
  p
194
200
  }
@@ -355,8 +361,9 @@ let getByIdInMemory = (
355
361
  ~entityId: string,
356
362
  ) => {
357
363
  let inMemTable = indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope)
358
- inMemTable.latestEntityChangeById->Dict.has(entityId)
359
- ? Some((inMemTable->InMemoryTable.Entity.getUnsafe)(entityId))
364
+ let entityKey = inMemTable.entityIdToKey(entityId->EntityId.unsafeOfString)
365
+ inMemTable.latestEntityChangeById->Dict.has(entityKey)
366
+ ? Some((inMemTable->InMemoryTable.Entity.getUnsafe)(entityKey))
360
367
  : None
361
368
  }
362
369
 
@@ -425,7 +432,7 @@ let loadByParsedFilter = (
425
432
  entities->Array.forEach(entity => {
426
433
  inMemTable->InMemoryTable.Entity.initValue(
427
434
  ~committedCheckpointId,
428
- ~key=entity.id,
435
+ ~entityId=entity.id->EntityId.unsafeOfString,
429
436
  ~entity=Some(entity),
430
437
  )
431
438
  })