envio 3.0.0-rc.1 → 3.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.0.0-rc.1",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -71,10 +71,10 @@
71
71
  "tsx": "4.21.0"
72
72
  },
73
73
  "optionalDependencies": {
74
- "envio-linux-x64": "3.0.0-rc.1",
75
- "envio-linux-x64-musl": "3.0.0-rc.1",
76
- "envio-linux-arm64": "3.0.0-rc.1",
77
- "envio-darwin-x64": "3.0.0-rc.1",
78
- "envio-darwin-arm64": "3.0.0-rc.1"
74
+ "envio-linux-x64": "3.0.1",
75
+ "envio-linux-x64-musl": "3.0.1",
76
+ "envio-linux-arm64": "3.0.1",
77
+ "envio-darwin-x64": "3.0.1",
78
+ "envio-darwin-arm64": "3.0.1"
79
79
  }
80
80
  }
package/src/Config.res CHANGED
@@ -957,11 +957,18 @@ let rec canonicalJson = (json: JSON.t): JSON.t =>
957
957
  }
958
958
 
959
959
  // Returns dotted leaf paths (`a.b[i].c`) where `stored` differs from
960
- // `current`. Skips subtrees whose canonical JSON matches.
960
+ // `current`, restricted to the highest-priority top-level tier with any
961
+ // diff. Tiers in order: version → name → storage → ecosystem
962
+ // (evm/fuel/svm) → entities → other top-level keys. The first tier
963
+ // containing a diff is the only one rendered; lower tiers are silenced
964
+ // so a single noisy section doesn't bury the actionable change.
961
965
  let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
966
+ let canonEq = (a: JSON.t, b: JSON.t) =>
967
+ JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b))
968
+
962
969
  let acc = []
963
970
  let rec go = (s: JSON.t, c: JSON.t, prefix: string) => {
964
- if JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c)) {
971
+ if canonEq(s, c) {
965
972
  ()
966
973
  } else {
967
974
  switch (s, c) {
@@ -987,25 +994,91 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
987
994
  | (Some(sv), Some(cv)) => go(sv, cv, p)
988
995
  }
989
996
  }
990
- | _ =>
991
- // Type mismatch or scalar diff
992
- acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
997
+ | _ => acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ let getTopKey = (j: JSON.t, k: string) =>
1003
+ switch j {
1004
+ | Object(d) => d->Dict.get(k)
1005
+ | _ => None
1006
+ }
1007
+ let topKeyDiffers = (k: string) =>
1008
+ switch (getTopKey(stored, k), getTopKey(current, k)) {
1009
+ | (None, None) => false
1010
+ | (None, _) | (_, None) => true
1011
+ | (Some(s), Some(c)) => !canonEq(s, c)
1012
+ }
1013
+ let runTier = (keys: array<string>) =>
1014
+ keys->Array.forEach(k =>
1015
+ switch (getTopKey(stored, k), getTopKey(current, k)) {
1016
+ | (None, None) => ()
1017
+ | (None, _) | (_, None) => acc->Array.push(k)->ignore
1018
+ | (Some(s), Some(c)) => go(s, c, k)
1019
+ }
1020
+ )
1021
+
1022
+ switch (stored, current) {
1023
+ | (Object(sObj), Object(cObj)) =>
1024
+ let tiers = [["version"], ["name"], ["storage"], ["evm", "fuel", "svm"], ["entities"]]
1025
+ let firstHit = tiers->Array.reduce(None, (acc, tier) =>
1026
+ switch acc {
1027
+ | Some(_) => acc
1028
+ | None =>
1029
+ switch tier->Array.filter(topKeyDiffers) {
1030
+ | [] => None
1031
+ | hits => Some(hits)
1032
+ }
993
1033
  }
1034
+ )
1035
+ switch firstHit {
1036
+ | Some(hits) => runTier(hits)
1037
+ | None =>
1038
+ let knownSet = Utils.Set.fromArray(tiers->Belt.Array.concatMany)
1039
+ let extras =
1040
+ Utils.Set.fromArray(Array.concat(sObj->Dict.keysToArray, cObj->Dict.keysToArray))
1041
+ ->Utils.Set.toArray
1042
+ ->Array.filter(k => !(knownSet->Utils.Set.has(k)))
1043
+ ->Array.toSorted(String.compare)
1044
+ ->Array.filter(topKeyDiffers)
1045
+ runTier(extras)
994
1046
  }
1047
+ | _ => go(stored, current, "")
995
1048
  }
996
- go(stored, current, "")
997
1049
  acc
998
1050
  }
999
1051
 
1000
1052
  // Throws an `incompatible config` error listing each path in `changedPaths`,
1001
- // plus the revert/reset remediation. `~resetCommand` lets each call site
1002
- // (`envio dev -r` / `envio start -r` / `envio local db-migrate setup`)
1003
- // surface the right wipe-and-restart command.
1004
- let throwIfIncompatible = (changedPaths: array<string>, ~resetCommand: string) => {
1053
+ // plus the remediation options. `~resetCommand` is rendered as-is for
1054
+ // option 2 (the wipe-and-redo). `~runCommand` controls option 3 (parallel
1055
+ // indexer recipe): when `None`, option 3 is omitted — the migrate flow
1056
+ // uses this because running a second indexer doesn't apply.
1057
+ // `~hasClickhouse` adds the extra env line so users running both
1058
+ // Postgres and Clickhouse get a complete override.
1059
+ let throwIfIncompatible = (
1060
+ changedPaths: array<string>,
1061
+ ~resetCommand: string,
1062
+ ~runCommand: option<string>,
1063
+ ~hasClickhouse: bool,
1064
+ ) => {
1005
1065
  if changedPaths->Array.length > 0 {
1006
1066
  let bullets = changedPaths->Array.map(p => ` - ${p}`)->Array.joinUnsafe("\n")
1067
+ let option1 = "Revert the changes above"
1068
+ let padTo = (s, col) => s ++ " "->String.repeat(Math.Int.max(col - String.length(s), 1))
1069
+ let col = Math.Int.max(String.length(option1), String.length(resetCommand)) + 2
1070
+ let option3 = switch runCommand {
1071
+ | None => ""
1072
+ | Some(cmd) =>
1073
+ let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : ""
1074
+ `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n${clickhouseLine} ENVIO_INDEXER_PORT=<new_port> \\\n ${cmd}`
1075
+ }
1007
1076
  JsError.throwWithMessage(
1008
- `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ${resetCommand} # wipe the database and re-index from scratch`,
1077
+ `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n 1. ${option1->padTo(
1078
+ col,
1079
+ )}# resume indexing where it left off\n 2. ${resetCommand->padTo(
1080
+ col,
1081
+ )}# delete all indexed data and start over${option3}`,
1009
1082
  )
1010
1083
  }
1011
1084
  }
@@ -780,9 +780,10 @@ function canonicalJson(json) {
780
780
  }
781
781
 
782
782
  function diffPaths(stored, current) {
783
+ let canonEq = (a, b) => JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
783
784
  let acc = [];
784
785
  let go = (s, c, prefix) => {
785
- if (JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c))) {
786
+ if (canonEq(s, c)) {
786
787
  return;
787
788
  }
788
789
  if (Array.isArray(s)) {
@@ -830,16 +831,92 @@ function diffPaths(stored, current) {
830
831
  }
831
832
  acc.push(prefix === "" ? "<root>" : prefix);
832
833
  };
833
- go(stored, current, "");
834
+ let getTopKey = (j, k) => {
835
+ if (typeof j === "object" && j !== null && !Array.isArray(j)) {
836
+ return j[k];
837
+ }
838
+ };
839
+ let topKeyDiffers = k => {
840
+ let match = getTopKey(stored, k);
841
+ let match$1 = getTopKey(current, k);
842
+ if (match !== undefined) {
843
+ if (match$1 !== undefined) {
844
+ return !canonEq(match, match$1);
845
+ } else {
846
+ return true;
847
+ }
848
+ } else {
849
+ return match$1 !== undefined;
850
+ }
851
+ };
852
+ let runTier = keys => {
853
+ keys.forEach(k => {
854
+ let match = getTopKey(stored, k);
855
+ let match$1 = getTopKey(current, k);
856
+ if (match !== undefined) {
857
+ if (match$1 !== undefined) {
858
+ return go(match, match$1, k);
859
+ } else {
860
+ acc.push(k);
861
+ return;
862
+ }
863
+ } else if (match$1 !== undefined) {
864
+ acc.push(k);
865
+ return;
866
+ } else {
867
+ return;
868
+ }
869
+ });
870
+ };
871
+ if (typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof current === "object" && current !== null && !Array.isArray(current)) {
872
+ let tiers = [
873
+ ["version"],
874
+ ["name"],
875
+ ["storage"],
876
+ [
877
+ "evm",
878
+ "fuel",
879
+ "svm"
880
+ ],
881
+ ["entities"]
882
+ ];
883
+ let firstHit = Stdlib_Array.reduce(tiers, undefined, (acc, tier) => {
884
+ if (acc !== undefined) {
885
+ return acc;
886
+ }
887
+ let hits = tier.filter(topKeyDiffers);
888
+ if (hits.length !== 0) {
889
+ return hits;
890
+ }
891
+ });
892
+ if (firstHit !== undefined) {
893
+ runTier(firstHit);
894
+ } else {
895
+ let knownSet = new Set(Belt_Array.concatMany(tiers));
896
+ runTier(Array.from(new Set(Object.keys(stored).concat(Object.keys(current)))).filter(k => !knownSet.has(k)).toSorted(Primitive_string.compare).filter(topKeyDiffers));
897
+ }
898
+ } else {
899
+ go(stored, current, "");
900
+ }
834
901
  return acc;
835
902
  }
836
903
 
837
- function throwIfIncompatible(changedPaths, resetCommand) {
904
+ function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse) {
838
905
  if (changedPaths.length === 0) {
839
906
  return;
840
907
  }
841
908
  let bullets = changedPaths.map(p => ` - ` + p).join("\n");
842
- Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ` + resetCommand + ` # wipe the database and re-index from scratch`);
909
+ let option1 = "Revert the changes above";
910
+ let padTo = (s, col) => s + " ".repeat(Math.max(col - s.length | 0, 1));
911
+ let col = Math.max(option1.length, resetCommand.length) + 2 | 0;
912
+ let option3;
913
+ if (runCommand !== undefined) {
914
+ let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : "";
915
+ option3 = `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n` + clickhouseLine + ` ENVIO_INDEXER_PORT=<new_port> \\\n ` + runCommand;
916
+ } else {
917
+ option3 = "";
918
+ }
919
+ Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n 1. ` + padTo(option1, col) + `# resume indexing where it left off\n 2. ` + padTo(resetCommand, col) + `# delete all indexed data and start over` + option3);
843
920
  }
844
921
 
845
922
  function loadWithoutRegistrations() {
@@ -71,7 +71,6 @@ module Entity = {
71
71
  fieldNameIndices: make(~hash=TableIndices.Index.getFieldName),
72
72
  }
73
73
 
74
- exception UndefinedKey(string)
75
74
  let updateIndices = (
76
75
  self: t<'entity>,
77
76
  ~entity: 'entity,
@@ -83,8 +82,7 @@ module Entity = {
83
82
  let fieldValue =
84
83
  entity
85
84
  ->(Utils.magic: 'entity => dict<TableIndices.FieldValue.t>)
86
- ->Dict.get(fieldName)
87
- ->Option.getUnsafe
85
+ ->Dict.getUnsafe(fieldName)
88
86
  if !(index->TableIndices.Index.evaluate(~fieldName, ~fieldValue)) {
89
87
  entityIndices->Utils.Set.delete(index)->ignore
90
88
  }
@@ -93,27 +91,25 @@ module Entity = {
93
91
  self.fieldNameIndices.dict
94
92
  ->Dict.keysToArray
95
93
  ->Array.forEach(fieldName => {
96
- switch (
97
- entity->(Utils.magic: 'entity => dict<TableIndices.FieldValue.t>)->Dict.get(fieldName),
98
- self.fieldNameIndices.dict->Dict.get(fieldName),
99
- ) {
100
- | (Some(fieldValue), Some(indices)) =>
101
- indices
102
- ->values
103
- ->Array.forEach(((index, relatedEntityIds)) => {
104
- if index->TableIndices.Index.evaluate(~fieldName, ~fieldValue) {
105
- //Add entity id to indices and add index to entity indicies
106
- relatedEntityIds->Utils.Set.add(getEntityIdUnsafe(entity))->ignore
107
- entityIndices->Utils.Set.add(index)->ignore
108
- } else {
109
- relatedEntityIds->Utils.Set.delete(getEntityIdUnsafe(entity))->ignore
110
- }
111
- })
112
- | _ =>
113
- UndefinedKey(fieldName)->ErrorHandling.mkLogAndRaise(
114
- ~msg="Expected field name to exist on the referenced index and the provided entity",
115
- )
116
- }
94
+ let indices = self.fieldNameIndices.dict->Dict.getUnsafe(fieldName)
95
+ // A missing key reads as `undefined`, which matches the `None` arm of
96
+ // `FieldValue.t` (`option<...>`). Mirror `addEmptyIndex` so nullable
97
+ // FK columns that were omitted on the set entity don't crash.
98
+ let fieldValue =
99
+ entity
100
+ ->(Utils.magic: 'entity => dict<TableIndices.FieldValue.t>)
101
+ ->Dict.getUnsafe(fieldName)
102
+ indices
103
+ ->values
104
+ ->Array.forEach(((index, relatedEntityIds)) => {
105
+ if index->TableIndices.Index.evaluate(~fieldName, ~fieldValue) {
106
+ //Add entity id to indices and add index to entity indicies
107
+ relatedEntityIds->Utils.Set.add(getEntityIdUnsafe(entity))->ignore
108
+ entityIndices->Utils.Set.add(index)->ignore
109
+ } else {
110
+ relatedEntityIds->Utils.Set.delete(getEntityIdUnsafe(entity))->ignore
111
+ }
112
+ })
117
113
  })
118
114
  }
119
115
 
@@ -90,8 +90,6 @@ function make$1() {
90
90
  };
91
91
  }
92
92
 
93
- let UndefinedKey = /* @__PURE__ */Primitive_exceptions.create("InMemoryTable.Entity.UndefinedKey");
94
-
95
93
  function updateIndices(self, entity, entityIndices) {
96
94
  entityIndices.forEach(index => {
97
95
  let fieldName = TableIndices.Index.getFieldName(index);
@@ -102,22 +100,9 @@ function updateIndices(self, entity, entityIndices) {
102
100
  }
103
101
  });
104
102
  Object.keys(self.fieldNameIndices.dict).forEach(fieldName => {
105
- let match = entity[fieldName];
106
- let match$1 = self.fieldNameIndices.dict[fieldName];
107
- if (match === undefined) {
108
- return ErrorHandling.mkLogAndRaise(undefined, "Expected field name to exist on the referenced index and the provided entity", {
109
- RE_EXN_ID: UndefinedKey,
110
- _1: fieldName
111
- });
112
- }
113
- if (match$1 === undefined) {
114
- return ErrorHandling.mkLogAndRaise(undefined, "Expected field name to exist on the referenced index and the provided entity", {
115
- RE_EXN_ID: UndefinedKey,
116
- _1: fieldName
117
- });
118
- }
119
- let fieldValue = Primitive_option.valFromOption(match);
120
- Object.values(match$1.dict).forEach(param => {
103
+ let indices = self.fieldNameIndices.dict[fieldName];
104
+ let fieldValue = entity[fieldName];
105
+ Object.values(indices.dict).forEach(param => {
121
106
  let relatedEntityIds = param[1];
122
107
  let index = param[0];
123
108
  if (TableIndices.Index.evaluate(index, fieldName, fieldValue)) {
@@ -333,7 +318,6 @@ let Entity = {
333
318
  getEntityIdUnsafe: getEntityIdUnsafe,
334
319
  makeIndicesSerializedToValue: makeIndicesSerializedToValue,
335
320
  make: make$1,
336
- UndefinedKey: UndefinedKey,
337
321
  updateIndices: updateIndices,
338
322
  deleteEntityFromIndices: deleteEntityFromIndices,
339
323
  initValue: initValue,
package/src/Main.res CHANGED
@@ -622,6 +622,7 @@ let migrate = async (~reset) => {
622
622
  ~chainConfigs=config.chainMap->ChainMap.values,
623
623
  ~envioInfo=getEnvioInfo(),
624
624
  ~resetCommand="envio local db-migrate setup",
625
+ ~runCommand=None,
625
626
  )
626
627
  await persistence.storage.close()
627
628
  }
@@ -668,6 +669,7 @@ let start = async (
668
669
  ~chainConfigs=configWithoutRegistrations.chainMap->ChainMap.values,
669
670
  ~envioInfo=getEnvioInfo(),
670
671
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
672
+ ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
671
673
  )
672
674
 
673
675
  // `Config.loadWithoutRegistrations` never sees registration state; handler,
package/src/Main.res.mjs CHANGED
@@ -472,7 +472,7 @@ function getEnvioInfo() {
472
472
  async function migrate(reset) {
473
473
  let config = Config.loadWithoutRegistrations();
474
474
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
475
- await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", reset);
475
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset);
476
476
  return await persistence.storage.close();
477
477
  }
478
478
 
@@ -497,7 +497,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
497
497
  let isDevelopmentMode = !isTest && configWithoutRegistrations.isDev;
498
498
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(configWithoutRegistrations, undefined);
499
499
  globalPersistenceRef.contents = persistence$1;
500
- await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", reset);
500
+ await Persistence.init(persistence$1, ChainMap.values(configWithoutRegistrations.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
501
501
  let match = await HandlerLoader.registerAllHandlers(configWithoutRegistrations);
502
502
  let registrations = match[1];
503
503
  let config = match[0];
@@ -168,7 +168,7 @@ let make = (
168
168
  }
169
169
 
170
170
  let init = {
171
- async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~reset=false) => {
171
+ async (persistence, ~chainConfigs, ~envioInfo, ~resetCommand, ~runCommand, ~reset=false) => {
172
172
  try {
173
173
  let shouldRun = switch persistence.storageStatus {
174
174
  | Unknown => true
@@ -212,7 +212,22 @@ let init = {
212
212
  | None => ["envio info is missing — storage initialized by an older envio"]
213
213
  | Some(stored) => Config.diffPaths(~stored, ~current=envioInfo)
214
214
  }
215
- Config.throwIfIncompatible(changedPaths, ~resetCommand)
215
+ // `storage.clickhouse` is serialized as a plain bool by the
216
+ // public config (see Rust `StorageConfig`), so probe for
217
+ // `Boolean(true)`, not an object.
218
+ let hasClickhouse = switch envioInfo {
219
+ | Object(d) =>
220
+ switch d->Dict.get("storage") {
221
+ | Some(Object(s)) =>
222
+ switch s->Dict.get("clickhouse") {
223
+ | Some(Boolean(true)) => true
224
+ | _ => false
225
+ }
226
+ | _ => false
227
+ }
228
+ | _ => false
229
+ }
230
+ Config.throwIfIncompatible(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
216
231
  persistence.storageStatus = Ready(initialState)
217
232
  let progress = Dict.make()
218
233
  initialState.chains->Array.forEach(c => {
@@ -27,7 +27,7 @@ function make(userEntities, allEnums, storage) {
27
27
  };
28
28
  }
29
29
 
30
- async function init(persistence, chainConfigs, envioInfo, resetCommand, resetOpt) {
30
+ async function init(persistence, chainConfigs, envioInfo, resetCommand, runCommand, resetOpt) {
31
31
  let reset = resetOpt !== undefined ? resetOpt : false;
32
32
  try {
33
33
  let promise = persistence.storageStatus;
@@ -70,7 +70,19 @@ async function init(persistence, chainConfigs, envioInfo, resetCommand, resetOpt
70
70
  let initialState$1 = await persistence.storage.resumeInitialState();
71
71
  let stored = initialState$1.envioInfo;
72
72
  let changedPaths = stored !== undefined ? Config.diffPaths(stored, envioInfo) : ["envio info is missing — storage initialized by an older envio"];
73
- Config.throwIfIncompatible(changedPaths, resetCommand);
73
+ let hasClickhouse;
74
+ if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
75
+ let match$1 = envioInfo["storage"];
76
+ if (typeof match$1 === "object" && match$1 !== null && !Array.isArray(match$1)) {
77
+ let match$2 = match$1["clickhouse"];
78
+ hasClickhouse = match$2 === true;
79
+ } else {
80
+ hasClickhouse = false;
81
+ }
82
+ } else {
83
+ hasClickhouse = false;
84
+ }
85
+ Config.throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse);
74
86
  persistence.storageStatus = {
75
87
  TAG: "Ready",
76
88
  _0: initialState$1