envio 3.12.0 → 3.13.0-alpha.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.
Files changed (84) hide show
  1. package/package.json +6 -6
  2. package/src/BatchProcessing.res +9 -9
  3. package/src/BatchProcessing.res.mjs +5 -5
  4. package/src/Bin.res +24 -17
  5. package/src/Bin.res.mjs +5 -0
  6. package/src/ChainFetching.res +31 -13
  7. package/src/ChainFetching.res.mjs +9 -4
  8. package/src/ChainState.res +55 -3
  9. package/src/ChainState.res.mjs +46 -3
  10. package/src/ChainState.resi +3 -1
  11. package/src/Config.res +35 -0
  12. package/src/Config.res.mjs +39 -0
  13. package/src/Core.res +4 -0
  14. package/src/Core.res.mjs +4 -0
  15. package/src/CrossChainState.res +61 -4
  16. package/src/CrossChainState.res.mjs +39 -5
  17. package/src/CrossChainState.resi +10 -1
  18. package/src/Env.res +4 -0
  19. package/src/FetchState.res +51 -46
  20. package/src/FetchState.res.mjs +54 -36
  21. package/src/InMemoryTable.res +155 -67
  22. package/src/InMemoryTable.res.mjs +151 -60
  23. package/src/IndexerLoop.res +2 -0
  24. package/src/IndexerLoop.res.mjs +1 -0
  25. package/src/IndexerState.res +41 -1
  26. package/src/IndexerState.res.mjs +43 -4
  27. package/src/IndexerState.resi +10 -0
  28. package/src/LoadLayer.res +57 -34
  29. package/src/LoadLayer.res.mjs +45 -62
  30. package/src/LoadLayer.resi +1 -1
  31. package/src/Logging.res +38 -6
  32. package/src/Logging.res.mjs +32 -5
  33. package/src/Main.res +131 -268
  34. package/src/Main.res.mjs +28 -152
  35. package/src/Metrics.res +263 -102
  36. package/src/Metrics.res.mjs +227 -48
  37. package/src/Persistence.res +27 -2
  38. package/src/Persistence.res.mjs +9 -2
  39. package/src/PgStorage.res +109 -71
  40. package/src/PgStorage.res.mjs +88 -58
  41. package/src/Server.res +181 -0
  42. package/src/Server.res.mjs +143 -0
  43. package/src/Supervisor.res +415 -0
  44. package/src/Supervisor.res.mjs +325 -0
  45. package/src/TestIndexer.res +9 -25
  46. package/src/TestIndexer.res.mjs +5 -4
  47. package/src/UserContext.res +13 -32
  48. package/src/UserContext.res.mjs +1 -7
  49. package/src/Utils.res +1 -4
  50. package/src/Utils.res.mjs +7 -16
  51. package/src/Worker.res +95 -0
  52. package/src/Worker.res.mjs +80 -0
  53. package/src/bindings/NodeJs.res +41 -0
  54. package/src/db/EntityFilter.res +487 -275
  55. package/src/db/EntityFilter.res.mjs +557 -309
  56. package/src/db/InternalTable.res +8 -1
  57. package/src/db/InternalTable.res.mjs +5 -1
  58. package/src/db/Table.res +21 -6
  59. package/src/db/Table.res.mjs +13 -4
  60. package/src/sources/BlockStore.res +7 -2
  61. package/src/sources/EvmHyperSyncSource.res +2 -0
  62. package/src/sources/EvmHyperSyncSource.res.mjs +2 -2
  63. package/src/sources/FuelHyperSyncSource.res +1 -0
  64. package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
  65. package/src/sources/HyperSync.res +4 -0
  66. package/src/sources/HyperSync.res.mjs +4 -2
  67. package/src/sources/HyperSync.resi +1 -0
  68. package/src/sources/HyperSyncClient.res +3 -0
  69. package/src/sources/HyperSyncSSE.res +1 -1
  70. package/src/sources/HyperSyncSSE.res.mjs +4 -10
  71. package/src/sources/RpcSource.res +1 -0
  72. package/src/sources/RpcSource.res.mjs +1 -1
  73. package/src/sources/SimulateSource.res +1 -0
  74. package/src/sources/SimulateSource.res.mjs +1 -1
  75. package/src/sources/Source.res +7 -0
  76. package/src/sources/SourceManager.res +4 -3
  77. package/src/sources/SourceManager.res.mjs +2 -2
  78. package/src/sources/SvmHyperSyncClient.res +5 -0
  79. package/src/sources/SvmHyperSyncSource.res +3 -0
  80. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
  81. package/src/tui/Tui.res +24 -0
  82. package/src/tui/Tui.res.mjs +18 -0
  83. package/src/tui/components/SyncETA.res +12 -6
  84. package/src/tui/components/SyncETA.res.mjs +12 -8
@@ -115,6 +115,10 @@ type t = {
115
115
  // waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
116
116
  // response or waiter carrying an older epoch than this is discarded.
117
117
  mutable epoch: int,
118
+ // The loop's one door in from outside it: IndexerLoop owns scheduling and
119
+ // wires this when it starts, so an event the loop can't see for itself can
120
+ // still make it re-evaluate. A no-op before then.
121
+ mutable scheduleProcessing: unit => unit,
118
122
  // None off the simulate path.
119
123
  simulateDeadInputTracker: option<SimulateDeadInputTracker.t>,
120
124
  // --- Metric counters, rendered by Metrics at scrape time. ---
@@ -141,6 +145,7 @@ let make = (
141
145
  ~chainStates: dict<ChainState.t>,
142
146
  ~isRealtime: bool,
143
147
  ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
148
+ ~holdRealtime=false,
144
149
  ~committedFrontier=Frontier.empty(),
145
150
  ~isDevelopmentMode=false,
146
151
  ~shouldUseTui=false,
@@ -180,11 +185,17 @@ let make = (
180
185
  chainMetaDirty: false,
181
186
  chainMetaThrottler,
182
187
  isProcessing: false,
183
- crossChainState: CrossChainState.make(~chainStates, ~isRealtime, ~targetBufferSize),
188
+ crossChainState: CrossChainState.make(
189
+ ~chainStates,
190
+ ~isRealtime,
191
+ ~targetBufferSize,
192
+ ~holdRealtime,
193
+ ),
184
194
  indexerStartTime: Date.make(),
185
195
  indexerStartTimeRef: Performance.now(),
186
196
  rollbackState: NoRollback,
187
197
  lastPrunedAtMillis: Dict.make(),
198
+ scheduleProcessing: () => (),
188
199
  loadManager: LoadManager.make(),
189
200
  keepProcessAlive: isDevelopmentMode || shouldUseTui,
190
201
  exitAfterFirstEventBlock,
@@ -227,6 +238,9 @@ let makeFromDbState = (
227
238
  ~exitAfterFirstEventBlock=false,
228
239
  ~reducedPollingInterval=?,
229
240
  ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
241
+ // A process driving part of a split run waits for its supervisor before
242
+ // entering the reorg threshold or switching to realtime.
243
+ ~holdRealtime=false,
230
244
  ~onError,
231
245
  ~onExit=?,
232
246
  ) => {
@@ -274,6 +288,7 @@ let makeFromDbState = (
274
288
  ~chainStates,
275
289
  ~isRealtime,
276
290
  ~targetBufferSize,
291
+ ~holdRealtime,
277
292
  ~committedFrontier=initialState.checkpointFrontier,
278
293
  ~isDevelopmentMode,
279
294
  ~shouldUseTui,
@@ -500,9 +515,33 @@ let isFinalizingIndexes = (state: t) =>
500
515
  state.crossChainState->CrossChainState.isCaughtUp &&
501
516
  !(state.crossChainState->CrossChainState.isRealtime)
502
517
 
518
+ // The FinalizingIndexes phase is the transition a held process waits on: it
519
+ // ends with `ready_at` committed and the indexer realtime.
520
+ let shouldFinalizeIndexes = (state: t) =>
521
+ state->isFinalizingIndexes && !(state.crossChainState->CrossChainState.isHoldingRealtime)
522
+
503
523
  let markCaughtUpIfSettled = (state: t) =>
504
524
  state.crossChainState->CrossChainState.markCaughtUpIfSettled
505
525
 
526
+ let isReadyToEnterReorgThreshold = (state: t, ~batch) =>
527
+ state.crossChainState->CrossChainState.isReadyToEnterReorgThreshold(~batch)
528
+
529
+ let bindScheduleProcessing = (state: t, scheduleProcessing) =>
530
+ state.scheduleProcessing = scheduleProcessing
531
+
532
+ // A process still waiting on its supervisor owes the schema the indexes its
533
+ // chains deferred, so reaching every end block doesn't make it done.
534
+ let isHoldingRealtime = (state: t) => state.crossChainState->CrossChainState.isHoldingRealtime
535
+
536
+ let hasArrivedAtHead = (state: t) => state.crossChainState->CrossChainState.hasArrivedAtHead
537
+
538
+ let releaseRealtime = (state: t) => {
539
+ state.crossChainState->CrossChainState.releaseRealtime
540
+ // Every chain is parked at the head with no batch coming, so nothing would
541
+ // notice the hold is gone without a pass through processing.
542
+ state.scheduleProcessing()
543
+ }
544
+
506
545
  let markReady = (state: t, ~readyAt) => state.crossChainState->CrossChainState.markReady(~readyAt)
507
546
 
508
547
  let rollbackState = (state: t) => state.rollbackState
@@ -572,6 +611,7 @@ let toMetrics = (state: t): Metrics.t => {
572
611
  elapsedSeconds: state.indexerStartTimeRef->Performance.secondsSince,
573
612
  targetBufferSize: state.crossChainState->CrossChainState.targetBufferSize,
574
613
  isInReorgThreshold: state.crossChainState->CrossChainState.isInReorgThreshold,
614
+ hasArrivedAtHead: state.crossChainState->CrossChainState.hasArrivedAtHead,
575
615
  rollbackEnabled: state.config.shouldRollbackOnReorg,
576
616
  maxBatchSize: state.config.batchSize,
577
617
  preloadSeconds: state.preloadSeconds,
@@ -24,8 +24,9 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
24
24
  import * as CheckpointSequence from "./db/CheckpointSequence.res.mjs";
25
25
  import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
26
26
 
27
- function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
27
+ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt, holdRealtimeOpt, committedFrontierOpt, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, onError, onExit) {
28
28
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
29
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
29
30
  let committedFrontier = committedFrontierOpt !== undefined ? committedFrontierOpt : Frontier.empty();
30
31
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
31
32
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
@@ -54,7 +55,7 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
54
55
  chainMetaDirty: false,
55
56
  chainMetaThrottler: chainMetaThrottler,
56
57
  isProcessing: false,
57
- crossChainState: CrossChainState.make(chainStates, isRealtime, targetBufferSize),
58
+ crossChainState: CrossChainState.make(chainStates, isRealtime, targetBufferSize, holdRealtime),
58
59
  rollbackState: "NoRollback",
59
60
  indexerStartTime: new Date(),
60
61
  indexerStartTimeRef: Performance.now(),
@@ -66,6 +67,7 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
66
67
  onExit: onExit,
67
68
  isStopped: false,
68
69
  epoch: 0,
70
+ scheduleProcessing: () => {},
69
71
  simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
70
72
  preloadSeconds: 0,
71
73
  processingSeconds: 0,
@@ -82,11 +84,12 @@ function make(config, persistence, chainStates, isRealtime, targetBufferSizeOpt,
82
84
  };
83
85
  }
84
86
 
85
- function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, onError, onExit) {
87
+ function makeFromDbState(config, persistence, initialState, registrationsByChainId, isDevelopmentModeOpt, shouldUseTuiOpt, exitAfterFirstEventBlockOpt, reducedPollingInterval, targetBufferSizeOpt, holdRealtimeOpt, onError, onExit) {
86
88
  let isDevelopmentMode = isDevelopmentModeOpt !== undefined ? isDevelopmentModeOpt : false;
87
89
  let shouldUseTui = shouldUseTuiOpt !== undefined ? shouldUseTuiOpt : false;
88
90
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
89
91
  let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : CrossChainState.calculateTargetBufferSize();
92
+ let holdRealtime = holdRealtimeOpt !== undefined ? holdRealtimeOpt : false;
90
93
  let isInReorgThreshold = initialState.cleanRun ? false : initialState.chains.some(resumedChainState => {
91
94
  let progressBlockNumber = resumedChainState.progressBlockNumber;
92
95
  let sourceBlockNumber = resumedChainState.sourceBlockNumber;
@@ -104,7 +107,7 @@ function makeFromDbState(config, persistence, initialState, registrationsByChain
104
107
  let chainConfig = ChainMap.get(config.chainMap, chainId);
105
108
  chainStates[resumedChainState.id] = ChainState.makeFromDbState(chainConfig, resumedChainState, initialState.reorgCheckpoints, isInReorgThreshold, isRealtime, config, initialState.contractMapping, registrationsByChainId, reducedPollingInterval);
106
109
  });
107
- let state = make(config, persistence, chainStates, isRealtime, targetBufferSize, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
110
+ let state = make(config, persistence, chainStates, isRealtime, targetBufferSize, holdRealtime, initialState.checkpointFrontier, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError, onExit);
108
111
  CrossChainState.markCaughtUpOnResume(state.crossChainState);
109
112
  Utils.Dict.forEach(initialState.cache, param => {
110
113
  let count = param.count;
@@ -350,10 +353,39 @@ function isFinalizingIndexes(state) {
350
353
  }
351
354
  }
352
355
 
356
+ function shouldFinalizeIndexes(state) {
357
+ if (isFinalizingIndexes(state)) {
358
+ return !CrossChainState.isHoldingRealtime(state.crossChainState);
359
+ } else {
360
+ return false;
361
+ }
362
+ }
363
+
353
364
  function markCaughtUpIfSettled(state) {
354
365
  CrossChainState.markCaughtUpIfSettled(state.crossChainState);
355
366
  }
356
367
 
368
+ function isReadyToEnterReorgThreshold(state, batch) {
369
+ return CrossChainState.isReadyToEnterReorgThreshold(state.crossChainState, batch);
370
+ }
371
+
372
+ function bindScheduleProcessing(state, scheduleProcessing) {
373
+ state.scheduleProcessing = scheduleProcessing;
374
+ }
375
+
376
+ function isHoldingRealtime(state) {
377
+ return CrossChainState.isHoldingRealtime(state.crossChainState);
378
+ }
379
+
380
+ function hasArrivedAtHead(state) {
381
+ return CrossChainState.hasArrivedAtHead(state.crossChainState);
382
+ }
383
+
384
+ function releaseRealtime(state) {
385
+ CrossChainState.releaseRealtime(state.crossChainState);
386
+ state.scheduleProcessing();
387
+ }
388
+
357
389
  function markReady(state, readyAt) {
358
390
  CrossChainState.markReady(state.crossChainState, readyAt);
359
391
  }
@@ -447,6 +479,7 @@ function toMetrics(state) {
447
479
  elapsedSeconds: Performance.secondsSince(state.indexerStartTimeRef),
448
480
  targetBufferSize: CrossChainState.targetBufferSize(state.crossChainState),
449
481
  isInReorgThreshold: CrossChainState.isInReorgThreshold(state.crossChainState),
482
+ hasArrivedAtHead: CrossChainState.hasArrivedAtHead(state.crossChainState),
450
483
  rollbackEnabled: state.config.shouldRollbackOnReorg,
451
484
  maxBatchSize: state.config.batchSize,
452
485
  preloadSeconds: state.preloadSeconds,
@@ -844,6 +877,12 @@ export {
844
877
  isRealtime,
845
878
  isFinalizingIndexes,
846
879
  markCaughtUpIfSettled,
880
+ isReadyToEnterReorgThreshold,
881
+ shouldFinalizeIndexes,
882
+ bindScheduleProcessing,
883
+ isHoldingRealtime,
884
+ hasArrivedAtHead,
885
+ releaseRealtime,
847
886
  markReady,
848
887
  rollbackState,
849
888
  indexerStartTime,
@@ -16,6 +16,7 @@ let make: (
16
16
  ~chainStates: dict<ChainState.t>,
17
17
  ~isRealtime: bool,
18
18
  ~targetBufferSize: int=?,
19
+ ~holdRealtime: bool=?,
19
20
  ~committedFrontier: Frontier.t=?,
20
21
  ~isDevelopmentMode: bool=?,
21
22
  ~shouldUseTui: bool=?,
@@ -34,6 +35,7 @@ let makeFromDbState: (
34
35
  ~exitAfterFirstEventBlock: bool=?,
35
36
  ~reducedPollingInterval: int=?,
36
37
  ~targetBufferSize: int=?,
38
+ ~holdRealtime: bool=?,
37
39
  ~onError: ErrorHandling.t => unit,
38
40
  ~onExit: unit => unit=?,
39
41
  ) => t
@@ -93,6 +95,14 @@ let shouldSaveHistory: t => dict<bool>
93
95
  let isRealtime: t => bool
94
96
  let isFinalizingIndexes: t => bool
95
97
  let markCaughtUpIfSettled: t => unit
98
+ let isReadyToEnterReorgThreshold: (t, ~batch: Batch.t) => bool
99
+ let shouldFinalizeIndexes: t => bool
100
+ // Wires the loop's way back in. IndexerLoop calls this as it starts.
101
+ let bindScheduleProcessing: (t, unit => unit) => unit
102
+ let isHoldingRealtime: t => bool
103
+ let hasArrivedAtHead: t => bool
104
+ // The supervisor's go-ahead for a process driving part of a split run.
105
+ let releaseRealtime: t => unit
96
106
  let markReady: (t, ~readyAt: Date.t) => unit
97
107
  let rollbackState: t => rollbackState
98
108
  let indexerStartTime: t => Date.t
package/src/LoadLayer.res CHANGED
@@ -6,23 +6,6 @@ let scopeKeySuffix = (scope: Internal.chainScope) =>
6
6
  | Chain(chainId) => `.${chainId->ChainId.toString}`
7
7
  }
8
8
 
9
- // Narrows a query to the scope's chain. Cross-chain entities have no chain-id
10
- // column, so their filter is left untouched.
11
- let scopeFilter = (filter: EntityFilter.t, ~table: Table.table, ~scope: Internal.chainScope) =>
12
- switch (scope, table->Table.getChainIdField) {
13
- | (Chain(chainId), Some(field)) =>
14
- EntityFilter.And({
15
- filters: [
16
- filter,
17
- Eq({
18
- fieldName: field.fieldName,
19
- fieldValue: chainId->(Utils.magic: ChainId.t => unknown),
20
- }),
21
- ],
22
- })
23
- | _ => filter
24
- }
25
-
26
9
  let loadById = (
27
10
  ~loadManager,
28
11
  ~persistence: Persistence.t,
@@ -48,10 +31,10 @@ let loadById = (
48
31
  (
49
32
  await storage.loadOrThrow(
50
33
  ~table=entityConfig.table,
51
- ~filter=EntityFilter.In({
52
- fieldName: Table.idFieldName,
53
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
54
- })->scopeFilter(~table=entityConfig.table, ~scope),
34
+ ~filter=EntityFilter.byIds(idsToLoad)->EntityFilter.scoped(
35
+ ~table=entityConfig.table,
36
+ ~scope,
37
+ ),
55
38
  )
56
39
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
57
40
  } catch {
@@ -273,15 +256,9 @@ let loadEffect = (
273
256
  let {outputSchema} = effect.storageMeta
274
257
 
275
258
  let dbEntities = try {
276
- (
277
- await storage.loadOrThrow(
278
- ~table,
279
- ~filter=EntityFilter.In({
280
- fieldName: Table.idFieldName,
281
- fieldValue: idsToLoad->(Utils.magic: array<string> => array<unknown>),
282
- }),
283
- )
284
- )->(Utils.magic: array<unknown> => array<Internal.effectCacheItem>)
259
+ (await storage.loadOrThrow(~table, ~filter=EntityFilter.byIds(idsToLoad)))->(
260
+ Utils.magic: array<unknown> => array<Internal.effectCacheItem>
261
+ )
285
262
  } catch {
286
263
  | exn =>
287
264
  Ecosystem.getItemLogger(item, ~ecosystem)->Logging.childWarn({
@@ -355,7 +332,7 @@ let loadEffect = (
355
332
  )
356
333
  }
357
334
 
358
- let loadByFilter = (
335
+ let loadByParsedFilter = (
359
336
  ~loadManager,
360
337
  ~persistence: Persistence.t,
361
338
  ~entityConfig: Internal.entityConfig,
@@ -378,7 +355,9 @@ let loadByFilter = (
378
355
 
379
356
  let size = ref(0)
380
357
 
381
- filters->Array.forEach(filter => inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter))
358
+ filters->Array.forEach(filter =>
359
+ inMemTable->InMemoryTable.Entity.addEmptyIndex(~filter, ~table=entityConfig.table)
360
+ )
382
361
 
383
362
  // Any non-derived field can be filtered on, so the columns this query reads
384
363
  // are indexed on demand before it runs rather than promised by the schema.
@@ -399,7 +378,7 @@ let loadByFilter = (
399
378
  (
400
379
  await storage.loadOrThrow(
401
380
  ~table=entityConfig.table,
402
- ~filter=filter->scopeFilter(~table=entityConfig.table, ~scope),
381
+ ~filter=filter->EntityFilter.scoped(~table=entityConfig.table, ~scope),
403
382
  )
404
383
  )->(Utils.magic: array<unknown> => array<Internal.entity>)
405
384
 
@@ -412,6 +391,10 @@ let loadByFilter = (
412
391
  )
413
392
  })
414
393
 
394
+ // Every row this filter's values could match is now in the table, so a
395
+ // later getWhere naming any of them needs no round trip of its own.
396
+ inMemTable->InMemoryTable.Entity.recordLoadedValues(~filter, ~table=entityConfig.table)
397
+
415
398
  size := size.contents + entities->Array.length
416
399
  } catch {
417
400
  | Persistence.StorageError({message, reason}) =>
@@ -442,13 +425,53 @@ let loadByFilter = (
442
425
  )
443
426
  }
444
427
 
428
+ // Keying an _in walks every value, so it's computed once here and handed to
429
+ // the load manager rather than recomputed by the hasher.
430
+ let filterKey = filter->EntityFilter.toString(~table=entityConfig.table)
431
+
432
+ if !(inMemTable->InMemoryTable.Entity.hasIndex)(filterKey) {
433
+ inMemTable->InMemoryTable.Entity.tryIndexFromLoadedValues(~filter, ~table=entityConfig.table)
434
+ }
435
+
445
436
  loadManager->LoadManager.call(
446
437
  ~key,
447
438
  ~load,
448
439
  ~input=filter,
449
440
  ~shouldGroup,
450
- ~hasher=EntityFilter.toString,
441
+ ~hasher=_ => filterKey,
451
442
  ~getUnsafeInMemory=inMemTable->InMemoryTable.Entity.getUnsafeOnIndex,
452
443
  ~hasInMemory=inMemTable->InMemoryTable.Entity.hasIndex,
453
444
  )
454
445
  }
446
+
447
+ let loadByFilter = (
448
+ ~loadManager,
449
+ ~persistence,
450
+ ~entityConfig: Internal.entityConfig,
451
+ ~scope,
452
+ ~indexerState,
453
+ ~shouldGroup,
454
+ ~item,
455
+ ~ecosystem,
456
+ ~filter: dict<dict<unknown>>,
457
+ ) =>
458
+ // Rejecting rather than throwing keeps a bad filter failing only its own
459
+ // call, even when the caller batches several with Promise.all.
460
+ try {
461
+ loadByParsedFilter(
462
+ ~loadManager,
463
+ ~persistence,
464
+ ~entityConfig,
465
+ ~scope,
466
+ ~indexerState,
467
+ ~shouldGroup,
468
+ ~item,
469
+ ~ecosystem,
470
+ ~filter=filter->EntityFilter.parseOrThrow(
471
+ ~entityName=entityConfig.name,
472
+ ~table=entityConfig.table,
473
+ ),
474
+ )
475
+ } catch {
476
+ | exn => Promise.reject(exn->Utils.prettifyExn)
477
+ }
@@ -1,6 +1,5 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Table from "./db/Table.res.mjs";
4
3
  import * as Utils from "./Utils.res.mjs";
5
4
  import * as ChainId from "./ChainId.res.mjs";
6
5
  import * as Logging from "./Logging.res.mjs";
@@ -27,25 +26,6 @@ function scopeKeySuffix(scope) {
27
26
  }
28
27
  }
29
28
 
30
- function scopeFilter(filter, table, scope) {
31
- let match = Table.getChainIdField(table);
32
- if (scope === "crossChain" || match === undefined) {
33
- return filter;
34
- } else {
35
- return {
36
- operator: "and",
37
- filters: [
38
- filter,
39
- {
40
- operator: "=",
41
- fieldName: match.fieldName,
42
- fieldValue: scope
43
- }
44
- ]
45
- };
46
- }
47
- }
48
-
49
29
  function loadById(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, entityId) {
50
30
  let key = entityConfig.name + `.get` + scopeKeySuffix(scope);
51
31
  let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
@@ -54,11 +34,7 @@ function loadById(loadManager, persistence, entityConfig, scope, indexerState, s
54
34
  let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
55
35
  let dbEntities;
56
36
  try {
57
- dbEntities = await storage.loadOrThrow(scopeFilter({
58
- operator: "in",
59
- fieldName: Table.idFieldName,
60
- fieldValue: idsToLoad
61
- }, entityConfig.table, scope), entityConfig.table);
37
+ dbEntities = await storage.loadOrThrow(EntityFilter.scoped(EntityFilter.byIds(idsToLoad), entityConfig.table, scope), entityConfig.table);
62
38
  } catch (raw_exn) {
63
39
  let exn = Primitive_exceptions.internalToException(raw_exn);
64
40
  if (exn.RE_EXN_ID === Persistence.StorageError) {
@@ -157,11 +133,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
157
133
  let outputSchema = match$1.outputSchema;
158
134
  let dbEntities;
159
135
  try {
160
- dbEntities = await storage.loadOrThrow({
161
- operator: "in",
162
- fieldName: Table.idFieldName,
163
- fieldValue: idsToLoad
164
- }, table);
136
+ dbEntities = await storage.loadOrThrow(EntityFilter.byIds(idsToLoad), table);
165
137
  } catch (raw_exn) {
166
138
  let exn = Primitive_exceptions.internalToException(raw_exn);
167
139
  Logging.childWarn(Ecosystem.getItemLogger(item, ecosystem), {
@@ -211,38 +183,49 @@ function loadEffect(loadManager, persistence, effect, effectArgs, scope, indexer
211
183
  }
212
184
 
213
185
  function loadByFilter(loadManager, persistence, entityConfig, scope, indexerState, shouldGroup, item, ecosystem, filter) {
214
- let key = EntityFilter.toOperationKey(filter, entityConfig.name) + scopeKeySuffix(scope);
215
- let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
216
- let load = async (filters, param) => {
217
- let storage = Persistence.getInitializedStorageOrThrow(persistence);
218
- let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
219
- let size = {
220
- contents: 0
221
- };
222
- filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter));
223
- await storage.ensureQueryIndexes(entityConfig, scope, filters);
224
- let queries = EntityFilter.merge(filters);
225
- await Promise.all(queries.map(async filter => {
226
- try {
227
- let entities = await storage.loadOrThrow(scopeFilter(filter, entityConfig.table, scope), entityConfig.table);
228
- let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
229
- entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
230
- size.contents = size.contents + entities.length | 0;
231
- return;
232
- } catch (raw_exn) {
233
- let exn = Primitive_exceptions.internalToException(raw_exn);
234
- if (exn.RE_EXN_ID === Persistence.StorageError) {
235
- return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
236
- operation: key,
237
- params: EntityFilter.getParams(filter)
238
- }), exn.message, exn.reason);
186
+ try {
187
+ let filter$1 = EntityFilter.parseOrThrow(filter, entityConfig.name, entityConfig.table);
188
+ let key = EntityFilter.toOperationKey(filter$1, entityConfig.name) + scopeKeySuffix(scope);
189
+ let inMemTable = InMemoryStore.getInMemTable(indexerState, entityConfig, scope);
190
+ let load = async (filters, param) => {
191
+ let storage = Persistence.getInitializedStorageOrThrow(persistence);
192
+ let timerRef = IndexerState.startStorageLoad(indexerState, storage.name, key);
193
+ let size = {
194
+ contents: 0
195
+ };
196
+ filters.forEach(filter => InMemoryTable.Entity.addEmptyIndex(inMemTable, filter, entityConfig.table));
197
+ await storage.ensureQueryIndexes(entityConfig, scope, filters);
198
+ let queries = EntityFilter.merge(filters);
199
+ await Promise.all(queries.map(async filter => {
200
+ try {
201
+ let entities = await storage.loadOrThrow(EntityFilter.scoped(filter, entityConfig.table, scope), entityConfig.table);
202
+ let committedCheckpointId = IndexerState.committedCheckpointIdFor(indexerState, scope);
203
+ entities.forEach(entity => InMemoryTable.Entity.initValue(inMemTable, committedCheckpointId, entity.id, entity));
204
+ InMemoryTable.Entity.recordLoadedValues(inMemTable, filter, entityConfig.table);
205
+ size.contents = size.contents + entities.length | 0;
206
+ return;
207
+ } catch (raw_exn) {
208
+ let exn = Primitive_exceptions.internalToException(raw_exn);
209
+ if (exn.RE_EXN_ID === Persistence.StorageError) {
210
+ return ErrorHandling.mkLogAndRaise(Logging.createChildFrom(Ecosystem.getItemLogger(item, ecosystem), {
211
+ operation: key,
212
+ params: EntityFilter.getParams(filter)
213
+ }), exn.message, exn.reason);
214
+ }
215
+ throw exn;
239
216
  }
240
- throw exn;
241
- }
242
- }));
243
- return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
244
- };
245
- return LoadManager.call(loadManager, filter, key, load, EntityFilter.toString, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
217
+ }));
218
+ return IndexerState.endStorageLoad(indexerState, timerRef, storage.name, key, Stdlib_Array.reduce(queries, 0, (acc, query) => acc + EntityFilter.valuesCount(query) | 0), size.contents);
219
+ };
220
+ let filterKey = EntityFilter.toString(filter$1, entityConfig.table);
221
+ if (!InMemoryTable.Entity.hasIndex(inMemTable)(filterKey)) {
222
+ InMemoryTable.Entity.tryIndexFromLoadedValues(inMemTable, filter$1, entityConfig.table);
223
+ }
224
+ return LoadManager.call(loadManager, filter$1, key, load, param => filterKey, shouldGroup, InMemoryTable.Entity.hasIndex(inMemTable), InMemoryTable.Entity.getUnsafeOnIndex(inMemTable));
225
+ } catch (raw_exn) {
226
+ let exn = Primitive_exceptions.internalToException(raw_exn);
227
+ return Promise.reject(Utils.prettifyExn(exn));
228
+ }
246
229
  }
247
230
 
248
231
  export {
@@ -250,4 +233,4 @@ export {
250
233
  loadByFilter,
251
234
  loadEffect,
252
235
  }
253
- /* Table Not a pure module */
236
+ /* Utils Not a pure module */
@@ -19,7 +19,7 @@ let loadByFilter: (
19
19
  ~shouldGroup: bool,
20
20
  ~item: Internal.item,
21
21
  ~ecosystem: Ecosystem.t,
22
- ~filter: EntityFilter.t,
22
+ ~filter: dict<dict<unknown>>,
23
23
  ) => promise<array<Internal.entity>>
24
24
 
25
25
  let loadEffect: (
package/src/Logging.res CHANGED
@@ -26,6 +26,30 @@ let logLevels = [
26
26
 
27
27
  %%private(let logger = ref(None))
28
28
 
29
+ // Fields every line this process logs carries. Merged into each line rather
30
+ // than bound to a child logger: pino writes a child's bindings and the line's
31
+ // own fields side by side, so a line that names the same key would carry it
32
+ // twice. A fresh object per line, since pino merges the line's fields into
33
+ // whatever this returns.
34
+ %%private(let context: ref<dict<JSON.t>> = ref(Dict.make()))
35
+ %%private(let mixin = () => JSON.Object(context.contents->Dict.copy))
36
+
37
+ // A child logger that binds a field the process already carries would have pino
38
+ // write it twice: a child's bindings and the context are concatenated into the
39
+ // line, not merged. The context is the one place a line names it, which it can
40
+ // be because a process only takes one when its every line is about that chain.
41
+ %%private(
42
+ let withoutContext = (params: 'a) =>
43
+ switch context.contents->Dict.keysToArray {
44
+ | [] => params
45
+ | keys => {
46
+ let narrowed = params->(Utils.magic: 'a => dict<JSON.t>)->Dict.copy
47
+ keys->Array.forEach(key => narrowed->Dict.delete(key))
48
+ narrowed->(Utils.magic: dict<JSON.t> => 'a)
49
+ }
50
+ }
51
+ )
52
+
29
53
  let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLevel) => {
30
54
  // Currently unused - useful if using multiple transports.
31
55
  // let pinoRaw = {"target": "pino/file", "level": Config.userLogLevel}
@@ -56,17 +80,19 @@ let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLeve
56
80
  ...Pino.ECS.make(),
57
81
  customLevels: logLevels,
58
82
  base,
83
+ mixin,
59
84
  },
60
85
  Transport.make(pinoFile),
61
86
  )
62
87
  | EcsConsoleMultistream =>
63
- makeMultiStreamLogger(~logFile=None, ~options=Some({...Pino.ECS.make(), base}))
88
+ makeMultiStreamLogger(~logFile=None, ~options=Some({...Pino.ECS.make(), base, mixin}))
64
89
  | EcsConsole =>
65
90
  make({
66
91
  ...Pino.ECS.make(),
67
92
  level: userLogLevel,
68
93
  customLevels: logLevels,
69
94
  base,
95
+ mixin,
70
96
  })
71
97
  | FileOnly =>
72
98
  makeWithOptionsAndTransport(
@@ -74,12 +100,13 @@ let makeLogger = (~logStrategy, ~logFilePath, ~defaultFileLogLevel, ~userLogLeve
74
100
  customLevels: logLevels,
75
101
  level: defaultFileLogLevel,
76
102
  base,
103
+ mixin,
77
104
  },
78
105
  Transport.make(pinoFile),
79
106
  )
80
- | ConsoleRaw => makeMultiStreamLogger(~logFile=None, ~options=Some({base: base}))
81
- | ConsolePretty => makeMultiStreamLogger(~logFile=None, ~options=Some({base: base}))
82
- | Both => makeMultiStreamLogger(~logFile=Some(logFilePath), ~options=Some({base: base}))
107
+ | ConsoleRaw => makeMultiStreamLogger(~logFile=None, ~options=Some({base, mixin}))
108
+ | ConsolePretty => makeMultiStreamLogger(~logFile=None, ~options=Some({base, mixin}))
109
+ | Both => makeMultiStreamLogger(~logFile=Some(logFilePath), ~options=Some({base, mixin}))
83
110
  }
84
111
  }
85
112
 
@@ -149,10 +176,15 @@ let childFatal = (logger, params: 'a) => {
149
176
  }
150
177
 
151
178
  let createChild = (~params: 'a) => {
152
- getLogger()->child(params->createChildParams)
179
+ getLogger()->child(params->withoutContext->createChildParams)
153
180
  }
181
+
182
+ // What belongs on every line is the run's to decide; the logger only carries
183
+ // what it is handed. A line that names one of these fields itself wins.
184
+ let setContext = (fields: dict<JSON.t>) => context := fields
185
+
154
186
  let createChildFrom = (~logger: t, ~params: 'a) => {
155
- logger->child(params->createChildParams)
187
+ logger->child(params->withoutContext->createChildParams)
156
188
  }
157
189
 
158
190
  @inline