envio 3.1.0-rc.1 → 3.1.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.
@@ -53,49 +53,41 @@ function make() {
53
53
  };
54
54
  }
55
55
 
56
- function resetButKeepLatestChanges(self) {
57
- let init = {
58
- latestEntityChangeById: {},
59
- changesCount: 0,
60
- prevEntityChanges: [],
61
- indicesByEntityId: {},
62
- fieldNameIndices: {}
63
- };
64
- return {
65
- latestEntityChangeById: self.latestEntityChangeById,
66
- changesCount: self.changesCount,
67
- prevEntityChanges: init.prevEntityChanges,
68
- indicesByEntityId: init.indicesByEntityId,
69
- fieldNameIndices: init.fieldNameIndices
70
- };
56
+ function snapshotChanges(self, committedCheckpointId, upToCheckpointId) {
57
+ let changes = [];
58
+ let keptPrev = [];
59
+ self.prevEntityChanges.forEach(change => {
60
+ if (change.checkpointId > upToCheckpointId) {
61
+ keptPrev.push(change);
62
+ } else {
63
+ changes.push(change);
64
+ }
65
+ });
66
+ self.prevEntityChanges = keptPrev;
67
+ self.changesCount = self.changesCount - changes.length;
68
+ Utils.Dict.forEach(self.latestEntityChangeById, change => {
69
+ let checkpointId = change.checkpointId;
70
+ if (checkpointId > committedCheckpointId && checkpointId <= upToCheckpointId) {
71
+ changes.push(change);
72
+ return;
73
+ }
74
+ });
75
+ return changes;
71
76
  }
72
77
 
73
- function resetButKeepLoadedFromDbChanges(self) {
74
- let latestEntityChangeById = {};
75
- let keptCount = {
76
- contents: 0
77
- };
78
+ function dropCommittedChanges(self, committedCheckpointId, keepLoadedFromDb) {
79
+ let keysToDelete = [];
78
80
  Utils.Dict.forEachWithKey(self.latestEntityChangeById, (change, key) => {
79
- if (change.checkpointId === Internal.loadedFromDbCheckpointId) {
80
- latestEntityChangeById[key] = change;
81
- keptCount.contents = keptCount.contents + 1;
81
+ let checkpointId = change.checkpointId;
82
+ if (checkpointId <= committedCheckpointId && !(keepLoadedFromDb && checkpointId === Internal.loadedFromDbCheckpointId)) {
83
+ keysToDelete.push(key);
82
84
  return;
83
85
  }
84
86
  });
85
- let init = {
86
- latestEntityChangeById: {},
87
- changesCount: 0,
88
- prevEntityChanges: [],
89
- indicesByEntityId: {},
90
- fieldNameIndices: {}
91
- };
92
- return {
93
- latestEntityChangeById: latestEntityChangeById,
94
- changesCount: keptCount.contents,
95
- prevEntityChanges: init.prevEntityChanges,
96
- indicesByEntityId: init.indicesByEntityId,
97
- fieldNameIndices: init.fieldNameIndices
98
- };
87
+ keysToDelete.forEach(key => Utils.Dict.deleteInPlace(self.latestEntityChangeById, key));
88
+ self.changesCount = self.changesCount - keysToDelete.length;
89
+ self.indicesByEntityId = {};
90
+ self.fieldNameIndices = {};
99
91
  }
100
92
 
101
93
  function updateIndices(self, entity) {
@@ -259,8 +251,8 @@ let Entity = {
259
251
  getOrCreateEntityIndices: getOrCreateEntityIndices,
260
252
  makeIndicesSerializedToValue: makeIndicesSerializedToValue,
261
253
  make: make,
262
- resetButKeepLatestChanges: resetButKeepLatestChanges,
263
- resetButKeepLoadedFromDbChanges: resetButKeepLoadedFromDbChanges,
254
+ snapshotChanges: snapshotChanges,
255
+ dropCommittedChanges: dropCommittedChanges,
264
256
  updateIndices: updateIndices,
265
257
  deleteEntityFromIndices: deleteEntityFromIndices,
266
258
  set: set,
package/src/Internal.res CHANGED
@@ -537,6 +537,9 @@ type effectArgs = {
537
537
  input: effectInput,
538
538
  context: effectContext,
539
539
  cacheKey: string,
540
+ // The processing checkpoint that referenced this effect; stamped on the
541
+ // in-memory cache entry so it's evicted once the checkpoint commits.
542
+ checkpointId: bigint,
540
543
  }
541
544
  type effectCacheItem = {id: string, output: effectOutput}
542
545
  type effectCacheStorageMeta = {
package/src/LoadLayer.res CHANGED
@@ -89,10 +89,12 @@ let callEffect = (
89
89
 
90
90
  effect.handler(arg)
91
91
  ->Promise.thenResolve(output => {
92
- inMemTable.dict->Dict.set(arg.cacheKey, output)
93
- if arg.context.cache {
94
- inMemTable.idsToStore->Array.push(arg.cacheKey)->ignore
95
- }
92
+ inMemTable->InMemoryStore.setEffectOutput(
93
+ ~checkpointId=arg.checkpointId,
94
+ ~cacheKey=arg.cacheKey,
95
+ ~output,
96
+ ~shouldCache=arg.context.cache,
97
+ )
96
98
  })
97
99
  ->Utils.Promise.catchResolve(exn => {
98
100
  onError(~inputKey=arg.cacheKey, ~exn)
@@ -283,7 +285,7 @@ let loadEffect = (
283
285
  try {
284
286
  let output = dbEntity.output->S.parseOrThrow(outputSchema)
285
287
  idsFromCache->Utils.Set.add(dbEntity.id)->ignore
286
- inMemTable.dict->Dict.set(dbEntity.id, output)
288
+ inMemTable->InMemoryStore.initEffectOutputFromDb(~cacheKey=dbEntity.id, ~output)
287
289
  } catch {
288
290
  | S.Raised(error) =>
289
291
  inMemTable.invalidationsCount = inMemTable.invalidationsCount + 1
@@ -334,8 +336,8 @@ let loadEffect = (
334
336
  ~load,
335
337
  ~shouldGroup,
336
338
  ~hasher=args => args.cacheKey,
337
- ~getUnsafeInMemory=hash => inMemTable.dict->Dict.getUnsafe(hash),
338
- ~hasInMemory=hash => inMemTable.dict->Dict.has(hash),
339
+ ~getUnsafeInMemory=hash => inMemTable->InMemoryStore.getEffectOutput(hash)->Option.getUnsafe,
340
+ ~hasInMemory=hash => inMemTable->InMemoryStore.getEffectOutput(hash)->Option.isSome,
339
341
  ~input=effectArgs,
340
342
  )
341
343
  }
@@ -12,6 +12,7 @@ import * as TableIndices from "./TableIndices.res.mjs";
12
12
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
13
13
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
14
14
  import * as InMemoryTable from "./InMemoryTable.res.mjs";
15
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
15
16
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
16
17
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
17
18
 
@@ -55,13 +56,7 @@ function callEffect(effect, arg, inMemTable, timerRef, onError) {
55
56
  }
56
57
  }
57
58
  effect.prevCallStartTimerRef = timerRef;
58
- return effect.handler(arg).then(output => {
59
- inMemTable.dict[arg.cacheKey] = output;
60
- if (arg.context.cache) {
61
- inMemTable.idsToStore.push(arg.cacheKey);
62
- return;
63
- }
64
- }).catch(exn => onError(arg.cacheKey, exn)).finally(() => {
59
+ return effect.handler(arg).then(output => InMemoryStore.setEffectOutput(inMemTable, arg.checkpointId, arg.cacheKey, output, arg.context.cache)).catch(exn => onError(arg.cacheKey, exn)).finally(() => {
65
60
  effect.activeCallsCount = effect.activeCallsCount - 1 | 0;
66
61
  Prometheus.SafeGauge.handleInt(Prometheus.EffectCalls.activeCallsCount, effectName, effect.activeCallsCount);
67
62
  let newTimer = Hrtime.makeTimer();
@@ -159,8 +154,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore,
159
154
  try {
160
155
  let output = S$RescriptSchema.parseOrThrow(dbEntity.output, outputSchema);
161
156
  idsFromCache.add(dbEntity.id);
162
- inMemTable.dict[dbEntity.id] = output;
163
- return;
157
+ return InMemoryStore.initEffectOutputFromDb(inMemTable, dbEntity.id, output);
164
158
  } catch (raw_error) {
165
159
  let error = Primitive_exceptions.internalToException(raw_error);
166
160
  if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
@@ -193,7 +187,7 @@ function loadEffect(loadManager, persistence, effect, effectArgs, inMemoryStore,
193
187
  return await executeWithRateLimit(effect, argsToCall, inMemTable, onError, false);
194
188
  }
195
189
  };
196
- return LoadManager.call(loadManager, effectArgs, key, load, args => args.cacheKey, shouldGroup, hash => hash in inMemTable.dict, hash => inMemTable.dict[hash]);
190
+ return LoadManager.call(loadManager, effectArgs, key, load, args => args.cacheKey, shouldGroup, hash => Stdlib_Option.isSome(InMemoryStore.getEffectOutput(inMemTable, hash)), hash => InMemoryStore.getEffectOutput(inMemTable, hash));
197
191
  }
198
192
 
199
193
  function loadByField(loadManager, persistence, operator, entityConfig, inMemoryStore, fieldName, fieldValueSchema, shouldGroup, item, fieldValue) {
package/src/Main.res CHANGED
@@ -312,6 +312,13 @@ let getGlobalIndexer = (): 'indexer => {
312
312
  )
313
313
  }
314
314
 
315
+ let onRollbackCommitFn = (callback: 'a) => {
316
+ HandlerRegister.throwIfFinishedRegistration(
317
+ ~methodName="~internalAndWillBeRemovedSoon_onRollbackCommit",
318
+ )
319
+ let _ = RollbackCommit.register(callback->(Utils.magic: 'a => RollbackCommit.callback))
320
+ }
321
+
315
322
  // Two-stage parse: first the ecosystem-specific outer schema unwraps the
316
323
  // wrapper (`block.number` / `block.height` / `slot`) and surfaces the
317
324
  // inner chunk as raw `unknown`; then the shared `blockRangeSchema`
@@ -453,8 +460,16 @@ let getGlobalIndexer = (): 'indexer => {
453
460
  "onEvent",
454
461
  "contractRegister",
455
462
  "onBlock",
463
+ "~internalAndWillBeRemovedSoon_onRollbackCommit",
464
+ ]
465
+ | Svm => [
466
+ "name",
467
+ "description",
468
+ "chainIds",
469
+ "chains",
470
+ "onSlot",
471
+ "~internalAndWillBeRemovedSoon_onRollbackCommit",
456
472
  ]
457
- | Svm => ["name", "description", "chainIds", "chains", "onSlot"]
458
473
  }
459
474
  keysMemo := Some(keys)
460
475
  keys
@@ -477,6 +492,7 @@ let getGlobalIndexer = (): 'indexer => {
477
492
  | "onEvent" => onEventFn->Utils.magic
478
493
  | "contractRegister" => contractRegisterFn->Utils.magic
479
494
  | "onBlock" | "onSlot" => onBlockFn->Utils.magic
495
+ | "~internalAndWillBeRemovedSoon_onRollbackCommit" => onRollbackCommitFn->Utils.magic
480
496
  | _ =>
481
497
  JsError.throwWithMessage(
482
498
  `Field \`${prop}\` does not exist on \`indexer\`. Available fields: ${getKeys()->Array.join(
@@ -686,6 +702,12 @@ let start = async (
686
702
  | Some(patchConfig) => patchConfig(config, registrations)
687
703
  | None => config
688
704
  }
705
+ // The single fatal-error handler, shared by the store, ErrorExit, and the
706
+ // manager's catch.
707
+ let onError = (errHandler: ErrorHandling.t) => {
708
+ errHandler->ErrorHandling.log
709
+ NodeJs.process->NodeJs.exitWithCode(Failure)
710
+ }
689
711
  let ctx = {
690
712
  Ctx.registrations,
691
713
  config,
@@ -693,6 +715,9 @@ let start = async (
693
715
  inMemoryStore: InMemoryStore.make(
694
716
  ~entities=persistence.allEntities,
695
717
  ~committedCheckpointId=(persistence->Persistence.getInitializedState).checkpointId,
718
+ ~persistence,
719
+ ~config,
720
+ ~onError=exn => onError(exn->ErrorHandling.make(~msg="Failed writing batch to the database")),
696
721
  ),
697
722
  }
698
723
 
@@ -763,8 +788,12 @@ let start = async (
763
788
  ~isDevelopmentMode,
764
789
  ~shouldUseTui,
765
790
  ~exitAfterFirstEventBlock,
791
+ ~onError,
766
792
  )
767
- let gsManager = globalState->GlobalStateManager.make
793
+ let gsManager =
794
+ globalState->GlobalStateManager.make(~onError=exn =>
795
+ onError(exn->ErrorHandling.make(~msg="Indexer has failed with an unexpected error"))
796
+ )
768
797
  if shouldUseTui {
769
798
  let _rerender = Tui.start(~getState=() => gsManager->GlobalStateManager.getState)
770
799
  }
package/src/Main.res.mjs CHANGED
@@ -18,12 +18,14 @@ import * as PromClient from "prom-client";
18
18
  import Yargs from "yargs/yargs";
19
19
  import * as ChainFetcher from "./ChainFetcher.res.mjs";
20
20
  import * as ChainManager from "./ChainManager.res.mjs";
21
+ import * as ErrorHandling from "./ErrorHandling.res.mjs";
21
22
  import * as HandlerLoader from "./HandlerLoader.res.mjs";
22
23
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
23
24
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
24
25
  import * as SourceManager from "./sources/SourceManager.res.mjs";
25
26
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
26
27
  import * as Helpers from "yargs/helpers";
28
+ import * as RollbackCommit from "./RollbackCommit.res.mjs";
27
29
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
28
30
  import * as HandlerRegister from "./HandlerRegister.res.mjs";
29
31
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
@@ -254,6 +256,10 @@ function getGlobalIndexer() {
254
256
  let match = parseIdentityConfig(identityConfig);
255
257
  HandlerRegister.setContractRegister(match[0], match[1], handler, match[2], undefined);
256
258
  };
259
+ let onRollbackCommitFn = callback => {
260
+ HandlerRegister.throwIfFinishedRegistration("~internalAndWillBeRemovedSoon_onRollbackCommit");
261
+ RollbackCommit.register(callback);
262
+ };
257
263
  let extractRange = (filter, name, ecosystem) => {
258
264
  try {
259
265
  let inner = S$RescriptSchema.parseOrThrow(filter, ecosystem.onBlockFilterSchema);
@@ -340,7 +346,8 @@ function getGlobalIndexer() {
340
346
  "description",
341
347
  "chainIds",
342
348
  "chains",
343
- "onSlot"
349
+ "onSlot",
350
+ "~internalAndWillBeRemovedSoon_onRollbackCommit"
344
351
  ];
345
352
  break;
346
353
  }
@@ -352,7 +359,8 @@ function getGlobalIndexer() {
352
359
  "chains",
353
360
  "onEvent",
354
361
  "contractRegister",
355
- "onBlock"
362
+ "onBlock",
363
+ "~internalAndWillBeRemovedSoon_onRollbackCommit"
356
364
  ];
357
365
  }
358
366
  keysMemo.contents = keys;
@@ -375,6 +383,8 @@ function getGlobalIndexer() {
375
383
  case "onBlock" :
376
384
  case "onSlot" :
377
385
  return onBlockFn;
386
+ case "~internalAndWillBeRemovedSoon_onRollbackCommit" :
387
+ return onRollbackCommitFn;
378
388
  default:
379
389
  return Stdlib_JsError.throwWithMessage(`Field \`` + prop + `\` does not exist on \`indexer\`. Available fields: ` + getKeys().join(", ") + `.`);
380
390
  }
@@ -523,7 +533,11 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
523
533
  allEnums: config.allEnums
524
534
  }) : config;
525
535
  let config$2 = patchConfig !== undefined ? patchConfig(config$1, registrations) : config$1;
526
- let ctx_inMemoryStore = InMemoryStore.make(persistence$1.allEntities, Persistence.getInitializedState(persistence$1).checkpointId);
536
+ let onError = errHandler => {
537
+ ErrorHandling.log(errHandler);
538
+ Process.exit(1);
539
+ };
540
+ let ctx_inMemoryStore = InMemoryStore.make(persistence$1.allEntities, Persistence.getInitializedState(persistence$1).checkpointId, persistence$1, config$2, exn => onError(ErrorHandling.make(exn, undefined, "Failed writing batch to the database")));
527
541
  let ctx = {
528
542
  registrations: registrations,
529
543
  config: config$2,
@@ -572,8 +586,8 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
572
586
  }, ctx, isDevelopmentMode);
573
587
  }
574
588
  let chainManager = ChainManager.makeFromDbState(Persistence.getInitializedState(persistence$1), config$2, registrations, undefined);
575
- let globalState = GlobalState.make(ctx, chainManager, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock);
576
- let gsManager = GlobalStateManager.make(globalState, undefined);
589
+ let globalState = GlobalState.make(ctx, chainManager, isDevelopmentMode, shouldUseTui, exitAfterFirstEventBlock, onError);
590
+ let gsManager = GlobalStateManager.make(globalState, exn => onError(ErrorHandling.make(exn, undefined, "Indexer has failed with an unexpected error")));
577
591
  if (shouldUseTui) {
578
592
  Tui.start(() => GlobalStateManager.getState(gsManager));
579
593
  }
@@ -51,6 +51,9 @@ type updatedEffectCache = {
51
51
  type rollback = {
52
52
  targetCheckpointId: Internal.checkpointId,
53
53
  diffCheckpointId: Internal.checkpointId,
54
+ // Last valid block per chain affected by the rollback. Read by
55
+ // `RollbackCommit.fire` once the diff is durably written.
56
+ progressBlockNumberByChainId: dict<int>,
54
57
  }
55
58
 
56
59
  type updatedEntity = {
@@ -126,13 +129,15 @@ type storage = {
126
129
  // Write batch to storage
127
130
  writeBatch: (
128
131
  ~batch: Batch.t,
129
- ~rawEvents: array<InternalTable.RawEvents.t>,
130
132
  ~rollback: option<rollback>,
131
133
  ~isInReorgThreshold: bool,
132
134
  ~config: Config.t,
133
135
  ~allEntities: array<Internal.entityConfig>,
134
136
  ~updatedEffectsCache: array<updatedEffectCache>,
135
137
  ~updatedEntities: array<updatedEntity>,
138
+ // Chain metadata stale since the last write, persisted in the same
139
+ // transaction so it never races the batch write.
140
+ ~chainMetaData: option<dict<InternalTable.Chains.metaFields>>,
136
141
  ) => promise<unit>,
137
142
  // Release any long-lived resources (e.g. the postgres connection pool) so
138
143
  // short-lived CLI commands like `db-migrate setup` can exit cleanly.