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/index.d.ts +12 -0
- package/package.json +6 -6
- package/src/Batch.res +7 -1
- package/src/Batch.res.mjs +2 -1
- package/src/ChainManager.res +3 -2
- package/src/ChainManager.res.mjs +3 -3
- package/src/Env.res +6 -0
- package/src/Env.res.mjs +3 -0
- package/src/EventProcessing.res +24 -122
- package/src/EventProcessing.res.mjs +24 -88
- package/src/GlobalState.res +31 -52
- package/src/GlobalState.res.mjs +54 -33
- package/src/GlobalStateManager.res +1 -3
- package/src/InMemoryStore.res +408 -110
- package/src/InMemoryStore.res.mjs +335 -84
- package/src/InMemoryTable.res +49 -30
- package/src/InMemoryTable.res.mjs +31 -39
- package/src/Internal.res +3 -0
- package/src/LoadLayer.res +9 -7
- package/src/LoadLayer.res.mjs +4 -10
- package/src/Main.res +31 -2
- package/src/Main.res.mjs +19 -5
- package/src/Persistence.res +6 -1
- package/src/PgStorage.res +171 -68
- package/src/PgStorage.res.mjs +125 -39
- package/src/RollbackCommit.res +32 -0
- package/src/RollbackCommit.res.mjs +35 -0
- package/src/TestIndexerProxyStorage.res +1 -1
- package/src/TestIndexerProxyStorage.res.mjs +1 -1
- package/src/Throttler.res +22 -15
- package/src/Throttler.res.mjs +19 -14
- package/src/UserContext.res +1 -0
- package/src/UserContext.res.mjs +3 -1
- package/src/bindings/NodeJs.res +1 -0
package/src/PgStorage.res
CHANGED
|
@@ -450,27 +450,64 @@ let chunkArray = (arr: array<'a>, ~chunkSize) => {
|
|
|
450
450
|
chunks
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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",
|
package/src/PgStorage.res.mjs
CHANGED
|
@@ -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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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 => {
|