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.
package/src/PgStorage.res CHANGED
@@ -450,27 +450,64 @@ let chunkArray = (arr: array<'a>, ~chunkSize) => {
450
450
  chunks
451
451
  }
452
452
 
453
- let removeInvalidUtf8InPlace = entities =>
454
- entities->Array.forEach(item => {
455
- let dict = item->(Utils.magic: 'a => dict<unknown>)
456
- dict->Utils.Dict.forEachWithKey((value, key) => {
457
- if value->typeof === #string {
458
- let value = value->(Utils.magic: unknown => string)
459
-
460
- dict->Dict.set(
461
- key,
462
- value
463
- ->Utils.String.replaceAll("\x00", "")
464
- ->(Utils.magic: string => unknown),
465
- )
466
- }
467
- })
468
- })
453
+ // Strips NUL bytes, recursing into nested objects/arrays so a NUL buried
454
+ // inside a jsonb column (an event param object, a json entity field) is
455
+ // removed too — Postgres rejects it in both text (0x00) and jsonb (22P05).
456
+ let rec removeInvalidUtf8DeepInPlace = (value: unknown): unknown => {
457
+ if value->typeof === #string {
458
+ value
459
+ ->(Utils.magic: unknown => string)
460
+ ->Utils.String.replaceAll("\x00", "")
461
+ ->(Utils.magic: string => unknown)
462
+ } else if value->typeof === #object && value !== %raw(`null`) {
463
+ let dict = value->(Utils.magic: unknown => dict<unknown>)
464
+ dict->Utils.Dict.forEachWithKey((v, k) => dict->Dict.set(k, removeInvalidUtf8DeepInPlace(v)))
465
+ value
466
+ } else {
467
+ value
468
+ }
469
+ }
470
+
471
+ let removeInvalidUtf8InPlace = items =>
472
+ items->Array.forEach(item =>
473
+ removeInvalidUtf8DeepInPlace(item->(Utils.magic: 'a => unknown))->ignore
474
+ )
469
475
 
470
476
  let pgErrorMessageSchema = S.object(s => s.field("message", S.string))
471
477
 
472
478
  exception PgEncodingError({table: Table.table})
473
479
 
480
+ // Classifies a write failure, parking it in `specificError` so the
481
+ // transaction can unwind and the outer handler can react. Both Postgres
482
+ // encoding failures we recognize are NUL-related — `0x00` in a text column
483
+ // and a NUL rejected by jsonb (22P05) — so they become a PgEncodingError
484
+ // that triggers an escape-and-retry of the offending table, where deep NUL
485
+ // stripping resolves them. We escape lazily on first failure to keep the
486
+ // happy path free of per-item sanitization. The aborted-transaction cascade
487
+ // is ignored so it never masks the original error.
488
+ let classifyWriteError = (~specificError: ref<option<exn>>, ~table: Table.table, ~exn) => {
489
+ /* Note: Entity History doesn't return StorageError yet, and directly throws JsError */
490
+ let normalizedExn = switch exn {
491
+ | JsExn(_) => exn
492
+ | Persistence.StorageError({reason: exn}) => exn
493
+ | _ => exn
494
+ }->JsExn.anyToExnInternal
495
+
496
+ switch normalizedExn {
497
+ | JsExn(error) =>
498
+ switch error->S.parseOrThrow(pgErrorMessageSchema) {
499
+ | `current transaction is aborted, commands ignored until end of transaction block` => ()
500
+ | `invalid byte sequence for encoding "UTF8": 0x00`
501
+ | `unsupported Unicode escape sequence` =>
502
+ specificError.contents = Some(PgEncodingError({table: table}))
503
+ | _ => specificError.contents = Some(exn->Utils.prettifyExn)
504
+ | exception _ => ()
505
+ }
506
+ | S.Raised(_) => throw(normalizedExn) // But rethrow this one, since it's not a PG error
507
+ | _ => ()
508
+ }
509
+ }
510
+
474
511
  // WeakMap for caching table batch set queries
475
512
  let setQueryCache = Utils.WeakMap.make()
476
513
  let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema) => {
@@ -712,10 +749,77 @@ let executeSet = (
712
749
  }
713
750
  }
714
751
 
752
+ let convertFieldsToJson = (fields: option<dict<unknown>>) => {
753
+ switch fields {
754
+ | None => %raw(`{}`)
755
+ | Some(fields) =>
756
+ // Convert bigint fields to string. There are no fields with nested
757
+ // bigints, so iterating only the top level is safe.
758
+ fields
759
+ ->Utils.Dict.mapValues(value =>
760
+ typeof(value) === #bigint
761
+ ? value
762
+ ->(Utils.magic: unknown => bigint)
763
+ ->BigInt.toString
764
+ ->(Utils.magic: string => unknown)
765
+ : value
766
+ )
767
+ ->(Utils.magic: dict<unknown> => JSON.t)
768
+ }
769
+ }
770
+
771
+ let makeRawEvent = (
772
+ eventItem: Internal.eventItem,
773
+ ~config: Config.t,
774
+ ): InternalTable.RawEvents.t => {
775
+ let {event, eventConfig, chain, blockNumber, timestamp: blockTimestamp} = eventItem
776
+ let {block, transaction, params, logIndex, srcAddress} = event
777
+ let chainId = chain->ChainMap.Chain.toChainId
778
+ let eventId = EventUtils.packEventIndex(~logIndex, ~blockNumber)
779
+ let blockFields =
780
+ block
781
+ ->(Utils.magic: Internal.eventBlock => option<dict<unknown>>)
782
+ ->convertFieldsToJson
783
+ let transactionFields =
784
+ transaction
785
+ ->(Utils.magic: Internal.eventTransaction => option<dict<unknown>>)
786
+ ->convertFieldsToJson
787
+
788
+ blockFields->config.ecosystem.cleanUpRawEventFieldsInPlace
789
+
790
+ // Serialize to unknown, because serializing to Js.Json.t fails for Bytes Fuel type, since it has unknown schema
791
+ let params =
792
+ params
793
+ ->S.reverseConvertOrThrow(eventConfig.paramsRawEventSchema)
794
+ ->(Utils.magic: unknown => JSON.t)
795
+ let params = if params === %raw(`null`) {
796
+ // Should probably make the params field nullable
797
+ // But this is currently needed to make events
798
+ // with empty params work
799
+ %raw(`"null"`)
800
+ } else {
801
+ params
802
+ }
803
+
804
+ {
805
+ chainId,
806
+ eventId,
807
+ eventName: eventConfig.name,
808
+ contractName: eventConfig.contractName,
809
+ blockNumber,
810
+ logIndex,
811
+ srcAddress,
812
+ blockHash: block->config.ecosystem.getId,
813
+ blockTimestamp,
814
+ blockFields,
815
+ transactionFields,
816
+ params,
817
+ }
818
+ }
819
+
715
820
  let rec writeBatch = async (
716
821
  sql,
717
822
  ~batch: Batch.t,
718
- ~rawEvents,
719
823
  ~pgSchema,
720
824
  ~rollback: option<Persistence.rollback>,
721
825
  ~isInReorgThreshold,
@@ -725,6 +829,7 @@ let rec writeBatch = async (
725
829
  ~updatedEffectsCache,
726
830
  ~updatedEntities: array<Persistence.updatedEntity>,
727
831
  ~sinkPromise: option<promise<option<exn>>>,
832
+ ~chainMetaData: option<dict<InternalTable.Chains.metaFields>>,
728
833
  ~escapeTables=?,
729
834
  ) => {
730
835
  try {
@@ -732,18 +837,37 @@ let rec writeBatch = async (
732
837
 
733
838
  let specificError = ref(None)
734
839
 
735
- let setRawEvents = executeSet(
736
- _,
737
- ~dbFunction=(sql, items) => {
738
- sql->setOrThrow(
739
- ~items,
740
- ~table=InternalTable.RawEvents.table,
741
- ~itemSchema=InternalTable.RawEvents.schema,
742
- ~pgSchema,
743
- )
744
- },
745
- ~items=rawEvents,
746
- )
840
+ let rawEvents = if config.enableRawEvents {
841
+ let rows = batch.items->Belt.Array.keepMap(item =>
842
+ switch item {
843
+ | Internal.Event(_) => Some(item->Internal.castUnsafeEventItem->makeRawEvent(~config))
844
+ | Internal.Block(_) => None
845
+ }
846
+ )
847
+ switch escapeTables {
848
+ | Some(tables) if tables->Utils.Set.has(InternalTable.RawEvents.table) =>
849
+ rows->removeInvalidUtf8InPlace
850
+ | _ => ()
851
+ }
852
+ rows
853
+ } else {
854
+ []
855
+ }
856
+
857
+ let setRawEvents = async sql => {
858
+ try {
859
+ await sql->executeSet(~dbFunction=(sql, items) => {
860
+ sql->setOrThrow(
861
+ ~items,
862
+ ~table=InternalTable.RawEvents.table,
863
+ ~itemSchema=InternalTable.RawEvents.schema,
864
+ ~pgSchema,
865
+ )
866
+ }, ~items=rawEvents)
867
+ } catch {
868
+ | exn => classifyWriteError(~specificError, ~table=InternalTable.RawEvents.table, ~exn)
869
+ }
870
+ }
747
871
 
748
872
  let setEntities = updatedEntities->Belt.Array.map(({entityConfig, changes}) => {
749
873
  let entitiesToSet = []
@@ -883,42 +1007,11 @@ let rec writeBatch = async (
883
1007
  // might throw PG error, earlier, than the handled error
884
1008
  // from setOrThrow will be passed through.
885
1009
  // This is needed for the utf8 encoding fix.
886
- | exn => {
887
- /* Note: Entity History doesn't return StorageError yet, and directly throws JsError */
888
- let normalizedExn = switch exn {
889
- | JsExn(_) => exn
890
- | Persistence.StorageError({reason: exn}) => exn
891
- | _ => exn
892
- }->JsExn.anyToExnInternal
893
-
894
- switch normalizedExn {
895
- | JsExn(error) =>
896
- // Workaround for https://github.com/enviodev/hyperindex/issues/446
897
- // We do escaping only when we actually got an error writing for the first time.
898
- // This is not perfect, but an optimization to avoid escaping for every single item.
899
-
900
- switch error->S.parseOrThrow(pgErrorMessageSchema) {
901
- | `current transaction is aborted, commands ignored until end of transaction block` => ()
902
- | `invalid byte sequence for encoding "UTF8": 0x00` =>
903
- // Since the transaction is aborted at this point,
904
- // we can't simply retry the function with escaped items,
905
- // so propagate the error, to restart the whole batch write.
906
- // Also, pass the failing table, to escape only its items.
907
- // TODO: Ideally all this should be done in the file,
908
- // so it'll be easier to work on PG specific logic.
909
- specificError.contents = Some(PgEncodingError({table: entityConfig.table}))
910
- | _ => specificError.contents = Some(exn->Utils.prettifyExn)
911
- | exception _ => ()
912
- }
913
- | S.Raised(_) => throw(normalizedExn) // But rethrow this one, since it's not a PG error
914
- | _ => ()
915
- }
916
-
917
- // Improtant: Don't rethrow here, since it'll result in
918
- // an unhandled rejected promise error.
919
- // That's fine not to throw, since sql->Postgres.beginSql
920
- // will fail anyways.
921
- }
1010
+ //
1011
+ // Important: Don't rethrow here, since it'll result in an unhandled
1012
+ // rejected promise error. That's fine not to throw, since
1013
+ // sql->Postgres.beginSql will fail anyways.
1014
+ | exn => classifyWriteError(~specificError, ~table=entityConfig.table, ~exn)
922
1015
  }
923
1016
  }
924
1017
  })
@@ -975,6 +1068,16 @@ let rec writeBatch = async (
975
1068
  setRawEvents,
976
1069
  ]->Belt.Array.concat(setEntities)
977
1070
 
1071
+ switch chainMetaData {
1072
+ | Some(chainsData) =>
1073
+ setOperations
1074
+ ->Array.push(sql =>
1075
+ sql->InternalTable.Chains.setMeta(~pgSchema, ~chainsData)->Utils.Promise.ignoreValue
1076
+ )
1077
+ ->ignore
1078
+ | None => ()
1079
+ }
1080
+
978
1081
  if shouldSaveHistory {
979
1082
  setOperations->Belt.Array.push(sql =>
980
1083
  sql->InternalTable.Checkpoints.insert(
@@ -1036,7 +1139,6 @@ let rec writeBatch = async (
1036
1139
  await writeBatch(
1037
1140
  sql,
1038
1141
  ~escapeTables,
1039
- ~rawEvents,
1040
1142
  ~batch,
1041
1143
  ~pgSchema,
1042
1144
  ~rollback,
@@ -1047,6 +1149,7 @@ let rec writeBatch = async (
1047
1149
  ~allEntities,
1048
1150
  ~updatedEntities,
1049
1151
  ~sinkPromise,
1152
+ ~chainMetaData,
1050
1153
  )
1051
1154
  }
1052
1155
  }
@@ -1631,13 +1734,13 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1631
1734
 
1632
1735
  let writeBatchMethod = async (
1633
1736
  ~batch,
1634
- ~rawEvents,
1635
1737
  ~rollback,
1636
1738
  ~isInReorgThreshold,
1637
1739
  ~config,
1638
1740
  ~allEntities,
1639
1741
  ~updatedEffectsCache,
1640
1742
  ~updatedEntities,
1743
+ ~chainMetaData,
1641
1744
  ) => {
1642
1745
  let pgUpdates = []
1643
1746
  let chUpdates = []
@@ -1676,7 +1779,6 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1676
1779
  await writeBatch(
1677
1780
  sql,
1678
1781
  ~batch,
1679
- ~rawEvents,
1680
1782
  ~pgSchema,
1681
1783
  ~rollback,
1682
1784
  ~isInReorgThreshold,
@@ -1686,6 +1788,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1686
1788
  ~updatedEffectsCache,
1687
1789
  ~updatedEntities=pgUpdates,
1688
1790
  ~sinkPromise,
1791
+ ~chainMetaData,
1689
1792
  )
1690
1793
  Prometheus.StorageWrite.increment(
1691
1794
  ~storage="postgres",
@@ -14,6 +14,7 @@ import * as Logging from "./Logging.res.mjs";
14
14
  import * as Internal from "./Internal.res.mjs";
15
15
  import Postgres from "postgres";
16
16
  import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
17
+ import * as EventUtils from "./EventUtils.res.mjs";
17
18
  import * as Prometheus from "./Prometheus.res.mjs";
18
19
  import * as Persistence from "./Persistence.res.mjs";
19
20
  import * as ChainFetcher from "./ChainFetcher.res.mjs";
@@ -329,19 +330,60 @@ function chunkArray(arr, chunkSize) {
329
330
  return chunks;
330
331
  }
331
332
 
332
- function removeInvalidUtf8InPlace(entities) {
333
- entities.forEach(item => Utils.Dict.forEachWithKey(item, (value, key) => {
334
- if (typeof value === "string") {
335
- item[key] = value.replaceAll("\x00", "");
336
- return;
337
- }
338
- }));
333
+ function removeInvalidUtf8DeepInPlace(value) {
334
+ if (typeof value === "string") {
335
+ return value.replaceAll("\x00", "");
336
+ } else if (typeof value === "object" && value !== null) {
337
+ Utils.Dict.forEachWithKey(value, (v, k) => {
338
+ value[k] = removeInvalidUtf8DeepInPlace(v);
339
+ });
340
+ return value;
341
+ } else {
342
+ return value;
343
+ }
344
+ }
345
+
346
+ function removeInvalidUtf8InPlace(items) {
347
+ items.forEach(item => {
348
+ removeInvalidUtf8DeepInPlace(item);
349
+ });
339
350
  }
340
351
 
341
352
  let pgErrorMessageSchema = S$RescriptSchema.object(s => s.f("message", S$RescriptSchema.string));
342
353
 
343
354
  let PgEncodingError = /* @__PURE__ */Primitive_exceptions.create("PgStorage.PgEncodingError");
344
355
 
356
+ function classifyWriteError(specificError, table, exn) {
357
+ let normalizedExn = Primitive_exceptions.internalToException(exn.RE_EXN_ID === "JsExn" || exn.RE_EXN_ID !== Persistence.StorageError ? exn : exn.reason);
358
+ if (normalizedExn.RE_EXN_ID === "JsExn") {
359
+ let val;
360
+ try {
361
+ val = S$RescriptSchema.parseOrThrow(normalizedExn._1, pgErrorMessageSchema);
362
+ } catch (exn$1) {
363
+ return;
364
+ }
365
+ switch (val) {
366
+ case "current transaction is aborted, commands ignored until end of transaction block" :
367
+ return;
368
+ case "invalid byte sequence for encoding \"UTF8\": 0x00" :
369
+ case "unsupported Unicode escape sequence" :
370
+ specificError.contents = {
371
+ RE_EXN_ID: PgEncodingError,
372
+ table: table
373
+ };
374
+ return;
375
+ default:
376
+ specificError.contents = Utils.prettifyExn(exn);
377
+ return;
378
+ }
379
+ } else {
380
+ if (normalizedExn.RE_EXN_ID !== S$RescriptSchema.Raised) {
381
+ return;
382
+ }
383
+ throw normalizedExn;
384
+ }
385
+ }
386
+
345
387
  let setQueryCache = new WeakMap();
346
388
 
347
389
  async function setOrThrow(sql, items, table, itemSchema, pgSchema) {
@@ -509,13 +551,76 @@ function executeSet(sql, items, dbFunction) {
509
551
  }
510
552
  }
511
553
 
512
- async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, escapeTables) {
554
+ function convertFieldsToJson(fields) {
555
+ if (fields !== undefined) {
556
+ return Utils.Dict.mapValues(fields, value => {
557
+ if (typeof value === "bigint") {
558
+ return value.toString();
559
+ } else {
560
+ return value;
561
+ }
562
+ });
563
+ } else {
564
+ return {};
565
+ }
566
+ }
567
+
568
+ function makeRawEvent(eventItem, config) {
569
+ let event = eventItem.event;
570
+ let block = event.block;
571
+ let logIndex = event.logIndex;
572
+ let blockNumber = eventItem.blockNumber;
573
+ let eventConfig = eventItem.eventConfig;
574
+ let eventId = EventUtils.packEventIndex(blockNumber, logIndex);
575
+ let blockFields = convertFieldsToJson(block);
576
+ let transactionFields = convertFieldsToJson(event.transaction);
577
+ config.ecosystem.cleanUpRawEventFieldsInPlace(blockFields);
578
+ let params = S$RescriptSchema.reverseConvertOrThrow(event.params, eventConfig.paramsRawEventSchema);
579
+ let params$1 = params === null ? "null" : params;
580
+ return {
581
+ chain_id: eventItem.chain,
582
+ event_id: eventId,
583
+ event_name: eventConfig.name,
584
+ contract_name: eventConfig.contractName,
585
+ block_number: blockNumber,
586
+ log_index: logIndex,
587
+ src_address: event.srcAddress,
588
+ block_hash: config.ecosystem.getId(block),
589
+ block_timestamp: eventItem.timestamp,
590
+ block_fields: blockFields,
591
+ transaction_fields: transactionFields,
592
+ params: params$1
593
+ };
594
+ }
595
+
596
+ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, chainMetaData, escapeTables) {
513
597
  try {
514
598
  let shouldSaveHistory = Config.shouldSaveHistory(config, isInReorgThreshold);
515
599
  let specificError = {
516
600
  contents: undefined
517
601
  };
518
- let setRawEvents = __x => executeSet(__x, rawEvents, (sql, items) => setOrThrow(sql, items, InternalTable.RawEvents.table, InternalTable.RawEvents.schema, pgSchema));
602
+ let rawEvents;
603
+ if (config.enableRawEvents) {
604
+ let rows = Belt_Array.keepMap(batch.items, item => {
605
+ if (item.kind === 0) {
606
+ return makeRawEvent(item, config);
607
+ }
608
+ });
609
+ if (escapeTables !== undefined && Primitive_option.valFromOption(escapeTables).has(InternalTable.RawEvents.table)) {
610
+ removeInvalidUtf8InPlace(rows);
611
+ }
612
+ rawEvents = rows;
613
+ } else {
614
+ rawEvents = [];
615
+ }
616
+ let setRawEvents = async sql => {
617
+ try {
618
+ return await executeSet(sql, rawEvents, (sql, items) => setOrThrow(sql, items, InternalTable.RawEvents.table, InternalTable.RawEvents.schema, pgSchema));
619
+ } catch (raw_exn) {
620
+ let exn = Primitive_exceptions.internalToException(raw_exn);
621
+ return classifyWriteError(specificError, InternalTable.RawEvents.table, exn);
622
+ }
623
+ };
519
624
  let setEntities = Belt_Array.map(updatedEntities, param => {
520
625
  let entityConfig = param.entityConfig;
521
626
  let entitiesToSet = [];
@@ -601,33 +706,7 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgTh
601
706
  return;
602
707
  } catch (raw_exn) {
603
708
  let exn = Primitive_exceptions.internalToException(raw_exn);
604
- let normalizedExn = Primitive_exceptions.internalToException(exn.RE_EXN_ID === "JsExn" || exn.RE_EXN_ID !== Persistence.StorageError ? exn : exn.reason);
605
- if (normalizedExn.RE_EXN_ID === "JsExn") {
606
- let val;
607
- try {
608
- val = S$RescriptSchema.parseOrThrow(normalizedExn._1, pgErrorMessageSchema);
609
- } catch (exn$1) {
610
- return;
611
- }
612
- switch (val) {
613
- case "current transaction is aborted, commands ignored until end of transaction block" :
614
- return;
615
- case "invalid byte sequence for encoding \"UTF8\": 0x00" :
616
- specificError.contents = {
617
- RE_EXN_ID: PgEncodingError,
618
- table: entityConfig.table
619
- };
620
- return;
621
- default:
622
- specificError.contents = Utils.prettifyExn(exn);
623
- return;
624
- }
625
- } else {
626
- if (normalizedExn.RE_EXN_ID !== S$RescriptSchema.Raised) {
627
- return;
628
- }
629
- throw normalizedExn;
630
- }
709
+ return classifyWriteError(specificError, entityConfig.table, exn);
631
710
  }
632
711
  };
633
712
  });
@@ -657,6 +736,9 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgTh
657
736
  }))),
658
737
  setRawEvents
659
738
  ], setEntities);
739
+ if (chainMetaData !== undefined) {
740
+ setOperations.push(sql => InternalTable.Chains.setMeta(sql, pgSchema, chainMetaData));
741
+ }
660
742
  if (shouldSaveHistory) {
661
743
  setOperations.push(sql => InternalTable.Checkpoints.insert(sql, pgSchema, batch.checkpointIds, batch.checkpointChainIds, batch.checkpointBlockNumbers, batch.checkpointBlockHashes, batch.checkpointEventsProcessed));
662
744
  }
@@ -687,7 +769,7 @@ async function writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgTh
687
769
  if (exn$1.RE_EXN_ID === PgEncodingError) {
688
770
  let escapeTables$1 = escapeTables !== undefined ? Primitive_option.valFromOption(escapeTables) : new Set();
689
771
  escapeTables$1.add(exn$1.table);
690
- return await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, Primitive_option.some(escapeTables$1));
772
+ return await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, updatedEntities, sinkPromise, chainMetaData, Primitive_option.some(escapeTables$1));
691
773
  }
692
774
  throw exn$1;
693
775
  }
@@ -1042,7 +1124,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1042
1124
  sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
1043
1125
  sql.unsafe(makeGetRollbackRestoredEntitiesQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
1044
1126
  ]);
1045
- let writeBatchMethod = async (batch, rawEvents, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities) => {
1127
+ let writeBatchMethod = async (batch, rollback, isInReorgThreshold, config, allEntities, updatedEffectsCache, updatedEntities, chainMetaData) => {
1046
1128
  let pgUpdates = [];
1047
1129
  let chUpdates = [];
1048
1130
  for (let i = 0, i_finish = updatedEntities.length; i < i_finish; ++i) {
@@ -1065,7 +1147,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
1065
1147
  sinkPromise = undefined;
1066
1148
  }
1067
1149
  let primaryTimerRef = Hrtime.makeTimer();
1068
- await writeBatch(sql, batch, rawEvents, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, undefined);
1150
+ await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, chainMetaData, undefined);
1069
1151
  return Prometheus.StorageWrite.increment("postgres", Hrtime.toSecondsFloat(Hrtime.timeSince(primaryTimerRef)));
1070
1152
  };
1071
1153
  let close = () => sql.end();
@@ -1162,9 +1244,11 @@ export {
1162
1244
  maxItemsPerQuery,
1163
1245
  makeTableBatchSetQuery,
1164
1246
  chunkArray,
1247
+ removeInvalidUtf8DeepInPlace,
1165
1248
  removeInvalidUtf8InPlace,
1166
1249
  pgErrorMessageSchema,
1167
1250
  PgEncodingError,
1251
+ classifyWriteError,
1168
1252
  setQueryCache,
1169
1253
  setOrThrow,
1170
1254
  makeSchemaTableNamesQuery,
@@ -1174,6 +1258,8 @@ export {
1174
1258
  deleteByIdsOrThrow,
1175
1259
  makeInsertDeleteUpdatesQuery,
1176
1260
  executeSet,
1261
+ convertFieldsToJson,
1262
+ makeRawEvent,
1177
1263
  writeBatch,
1178
1264
  makeGetRollbackRestoredEntitiesQuery,
1179
1265
  makeGetRollbackRemovedIdsQuery,
@@ -0,0 +1,32 @@
1
+ // Temporary, internal-only support for the unstable
2
+ // `indexer.~internalAndWillBeRemovedSoon_onRollbackCommit` API. The whole
3
+ // feature lives here plus two call sites: registration in `Main.res` and the
4
+ // fire on a successful rollback write in `InMemoryStore.res`. Delete those
5
+ // together with this module.
6
+ type args = {chainId: int, rollbackToBlock: int}
7
+ type callback = args => promise<unit>
8
+
9
+ let callbacks: array<callback> = []
10
+
11
+ let register = (callback: callback) => {
12
+ callbacks->Array.push(callback)
13
+ () =>
14
+ switch callbacks->Array.indexOf(callback) {
15
+ | -1 => ()
16
+ | index => callbacks->Array.splice(~start=index, ~remove=1, ~insert=[])
17
+ }
18
+ }
19
+
20
+ // Fired after a rollback diff is durably written, once per affected chain.
21
+ // `progressBlockNumberByChainId` is the last valid block per chain, taken from
22
+ // the in-memory store's rollback object. A throwing callback bubbles to the
23
+ // write loop's onError, crashing the indexer like a failed write.
24
+ let fire = async (~progressBlockNumberByChainId: dict<int>) => {
25
+ let _ = await progressBlockNumberByChainId
26
+ ->Dict.toArray
27
+ ->Array.flatMap(((chainIdKey, rollbackToBlock)) => {
28
+ let args = {chainId: chainIdKey->Int.fromString->Option.getUnsafe, rollbackToBlock}
29
+ callbacks->Array.map(callback => callback(args))
30
+ })
31
+ ->Promise.all
32
+ }
@@ -0,0 +1,35 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+
5
+ let callbacks = [];
6
+
7
+ function register(callback) {
8
+ callbacks.push(callback);
9
+ return () => {
10
+ let index = callbacks.indexOf(callback);
11
+ if (index !== -1) {
12
+ callbacks.splice(index, 1);
13
+ return;
14
+ }
15
+ };
16
+ }
17
+
18
+ async function fire(progressBlockNumberByChainId) {
19
+ await Promise.all(Object.entries(progressBlockNumberByChainId).flatMap(param => {
20
+ let args_chainId = Stdlib_Int.fromString(param[0], undefined);
21
+ let args_rollbackToBlock = param[1];
22
+ let args = {
23
+ chainId: args_chainId,
24
+ rollbackToBlock: args_rollbackToBlock
25
+ };
26
+ return callbacks.map(callback => callback(args));
27
+ }));
28
+ }
29
+
30
+ export {
31
+ callbacks,
32
+ register,
33
+ fire,
34
+ }
35
+ /* No side effect */
@@ -131,13 +131,13 @@ let makeStorage = (proxy: t): Persistence.storage => {
131
131
  },
132
132
  writeBatch: async (
133
133
  ~batch,
134
- ~rawEvents as _,
135
134
  ~rollback as _,
136
135
  ~isInReorgThreshold as _,
137
136
  ~config as _,
138
137
  ~allEntities as _,
139
138
  ~updatedEffectsCache as _,
140
139
  ~updatedEntities,
140
+ ~chainMetaData as _,
141
141
  ) => {
142
142
  // Encode entities to JSON for serialization across worker boundary
143
143
  let serializableEntities = updatedEntities->Array.map((
@@ -77,7 +77,7 @@ function makeStorage(proxy) {
77
77
  getRollbackTargetCheckpoint: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. Set rollbackOnReorg to false in config."),
78
78
  getRollbackProgressDiff: async param => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. Set rollbackOnReorg to false in config."),
79
79
  getRollbackData: async (param, param$1) => Stdlib_JsError.throwWithMessage("TestIndexer: Rollback is not supported. Set rollbackOnReorg to false in config."),
80
- writeBatch: async (batch, param, param$1, param$2, param$3, param$4, param$5, updatedEntities) => {
80
+ writeBatch: async (batch, param, param$1, param$2, param$3, param$4, updatedEntities, param$5) => {
81
81
  let serializableEntities = updatedEntities.map(param => {
82
82
  let entityConfig = param.entityConfig;
83
83
  let encodeChange = change => {