envio 3.5.0-alpha.1 → 3.5.0-alpha.3
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 +10 -6
- package/package.json +6 -6
- package/src/BatchProcessing.res +27 -0
- package/src/BatchProcessing.res.mjs +12 -0
- package/src/ChainFetching.res +5 -5
- package/src/ChainFetching.res.mjs +3 -4
- package/src/ChainState.res +48 -25
- package/src/ChainState.res.mjs +23 -29
- package/src/ChainState.resi +2 -6
- package/src/Change.res +3 -3
- package/src/Config.res +2 -6
- package/src/Config.res.mjs +2 -12
- package/src/Core.res +3 -0
- package/src/CrossChainState.res +33 -10
- package/src/CrossChainState.res.mjs +21 -6
- package/src/CrossChainState.resi +3 -0
- package/src/EntityId.res +15 -0
- package/src/EntityId.res.mjs +9 -0
- package/src/EventConfigBuilder.res +1 -58
- package/src/EventConfigBuilder.res.mjs +1 -33
- package/src/FetchState.res +510 -460
- package/src/FetchState.res.mjs +413 -428
- package/src/FinalizeBackfill.res +34 -0
- package/src/FinalizeBackfill.res.mjs +26 -0
- package/src/HandlerRegister.res +45 -0
- package/src/HandlerRegister.res.mjs +43 -0
- package/src/InMemoryStore.res +11 -4
- package/src/InMemoryTable.res +26 -24
- package/src/InMemoryTable.res.mjs +19 -18
- package/src/IndexerState.res +71 -2
- package/src/IndexerState.res.mjs +67 -3
- package/src/IndexerState.resi +5 -0
- package/src/Internal.res +17 -8
- package/src/LoadLayer.res +9 -1
- package/src/LoadLayer.res.mjs +1 -0
- package/src/LogSelection.res +2 -1
- package/src/Metrics.res +37 -6
- package/src/Metrics.res.mjs +5 -1
- package/src/Persistence.res +12 -1
- package/src/PgStorage.res +411 -57
- package/src/PgStorage.res.mjs +262 -41
- package/src/SimulateItems.res +2 -2
- package/src/TestIndexer.res +9 -5
- package/src/TestIndexer.res.mjs +5 -2
- package/src/UserContext.res +3 -3
- package/src/Writing.res +12 -2
- package/src/Writing.res.mjs +13 -2
- package/src/bindings/ClickHouse.res +5 -3
- package/src/bindings/ClickHouse.res.mjs +2 -5
- package/src/bindings/Vitest.res +22 -1
- package/src/bindings/Vitest.res.mjs +15 -0
- package/src/db/EntityFilter.res +0 -4
- package/src/db/EntityFilter.res.mjs +1 -8
- package/src/db/EntityHistory.res +16 -7
- package/src/db/EntityHistory.res.mjs +7 -6
- package/src/db/IndexRegistry.res +219 -0
- package/src/db/IndexRegistry.res.mjs +195 -0
- package/src/db/InternalTable.res +7 -0
- package/src/db/InternalTable.res.mjs +7 -0
- package/src/db/Table.res +53 -22
- package/src/db/Table.res.mjs +56 -18
- package/src/sources/AddressSet.res +48 -0
- package/src/sources/AddressSet.res.mjs +11 -0
- package/src/sources/AddressStore.res +152 -0
- package/src/sources/AddressStore.res.mjs +97 -0
- package/src/sources/EvmChain.res +3 -0
- package/src/sources/EvmChain.res.mjs +4 -2
- package/src/sources/EvmHyperSyncSource.res +6 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcClient.res +6 -3
- package/src/sources/EvmRpcClient.res.mjs +3 -3
- package/src/sources/FuelHyperSync.res +4 -3
- package/src/sources/FuelHyperSync.res.mjs +3 -3
- package/src/sources/FuelHyperSync.resi +2 -1
- package/src/sources/FuelHyperSyncClient.res +12 -6
- package/src/sources/FuelHyperSyncClient.res.mjs +2 -2
- package/src/sources/FuelHyperSyncSource.res +7 -4
- package/src/sources/FuelHyperSyncSource.res.mjs +3 -3
- package/src/sources/HyperSync.res +6 -4
- package/src/sources/HyperSync.res.mjs +2 -3
- package/src/sources/HyperSync.resi +1 -1
- package/src/sources/HyperSyncClient.res +18 -9
- package/src/sources/HyperSyncClient.res.mjs +4 -4
- package/src/sources/RpcSource.res +15 -10
- package/src/sources/RpcSource.res.mjs +3 -4
- package/src/sources/SimulateSource.res +26 -11
- package/src/sources/SimulateSource.res.mjs +13 -5
- package/src/sources/Source.res +4 -2
- package/src/sources/SourceManager.res +2 -3
- package/src/sources/SourceManager.res.mjs +2 -3
- package/src/sources/Svm.res +1 -2
- package/src/sources/Svm.res.mjs +1 -1
- package/src/sources/SvmHyperSyncClient.res +12 -3
- package/src/sources/SvmHyperSyncClient.res.mjs +2 -2
- package/src/sources/SvmHyperSyncSource.res +10 -5
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/src/IndexingAddresses.res +0 -151
- package/src/IndexingAddresses.res.mjs +0 -130
- package/src/IndexingAddresses.resi +0 -39
package/src/PgStorage.res.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import * as Config from "./Config.res.mjs";
|
|
|
10
10
|
import * as Hasura from "./Hasura.res.mjs";
|
|
11
11
|
import * as Schema from "./db/Schema.res.mjs";
|
|
12
12
|
import * as Logging from "./Logging.res.mjs";
|
|
13
|
+
import * as EntityId from "./EntityId.res.mjs";
|
|
13
14
|
import * as Internal from "./Internal.res.mjs";
|
|
14
15
|
import Postgres from "postgres";
|
|
15
16
|
import * as ChainState from "./ChainState.res.mjs";
|
|
@@ -17,6 +18,7 @@ import * as Performance from "./bindings/Performance.res.mjs";
|
|
|
17
18
|
import * as Persistence from "./Persistence.res.mjs";
|
|
18
19
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
19
20
|
import * as EntityHistory from "./db/EntityHistory.res.mjs";
|
|
21
|
+
import * as IndexRegistry from "./db/IndexRegistry.res.mjs";
|
|
20
22
|
import * as InternalTable from "./db/InternalTable.res.mjs";
|
|
21
23
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
22
24
|
import * as Child_process from "child_process";
|
|
@@ -71,13 +73,87 @@ function makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema) {
|
|
|
71
73
|
return `CREATE INDEX IF NOT EXISTS "` + indexName + `" ON "` + pgSchema + `"."` + tableName + `"(` + index + `);`;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function
|
|
76
|
+
function makeCreateTableIndexesQuery(table, pgSchema) {
|
|
75
77
|
let tableName = table.tableName;
|
|
76
78
|
let createIndex = indexField => makeCreateIndexQuery(tableName, [indexField], pgSchema);
|
|
77
79
|
let createCompositeIndex = indexFields => makeCreateCompositeIndexQuery(tableName, indexFields, pgSchema);
|
|
78
|
-
let
|
|
79
|
-
let
|
|
80
|
-
return
|
|
80
|
+
let singleIndexes = Table.getSingleIndexes(table);
|
|
81
|
+
let compositeIndexes = Table.getCompositeIndexes(table);
|
|
82
|
+
return singleIndexes.map(createIndex).join("\n") + compositeIndexes.map(createCompositeIndex).join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function schemaIndexDescription(param) {
|
|
86
|
+
return param.tableName + "_" + param.indexFields.map(f => f.fieldName + directionToIndexName(f.direction)).join("_");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function schemaIndexName(schemaIndex) {
|
|
90
|
+
return IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function schemaIndexKey(param) {
|
|
94
|
+
return IndexRegistry.makeKey(param.tableName, IndexRegistry.fromIndexFields(param.indexFields), IndexRegistry.btree);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeCreateSchemaIndexQuery(schemaIndex, pgSchema) {
|
|
98
|
+
let columns = schemaIndex.indexFields.map(f => `"` + f.fieldName + `"` + directionToSql(f.direction)).join(", ");
|
|
99
|
+
return `CREATE INDEX IF NOT EXISTS "` + IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex)) + `" ON "` + pgSchema + `"."` + schemaIndex.tableName + `"(` + columns + `);`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatSeconds(timeRef) {
|
|
103
|
+
return (Math.round(Performance.secondsSince(timeRef) * 100) / 100).toString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let slowOnLargeDatabaseNotice = "This can take a long time on a large database.";
|
|
107
|
+
|
|
108
|
+
function makeReindexQuery(pgSchema, indexName) {
|
|
109
|
+
return `REINDEX INDEX "` + pgSchema + `"."` + indexName + `";`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nameCollisionError(indexName, pgSchema) {
|
|
113
|
+
return new Error(`Cannot create the index "` + indexName + `" in schema "` + pgSchema + `". An index of that name already exists and PostgreSQL reports it as invalid, but it isn't one the indexer created — it covers different columns, or it is a unique or partial index — so it is left untouched. Drop or repair it by hand, then restart the indexer.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeSingleColumnSchemaIndex(tableName, column) {
|
|
117
|
+
return {
|
|
118
|
+
tableName: tableName,
|
|
119
|
+
indexFields: [{
|
|
120
|
+
fieldName: column,
|
|
121
|
+
direction: "Asc"
|
|
122
|
+
}]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getSchemaIndexes(entities) {
|
|
127
|
+
let derivedSchema = Schema.make(entities.map(e => e.table));
|
|
128
|
+
let all = [];
|
|
129
|
+
entities.forEach(param => {
|
|
130
|
+
let table = param.table;
|
|
131
|
+
Table.getSingleIndexes(table).forEach(column => {
|
|
132
|
+
all.push(makeSingleColumnSchemaIndex(table.tableName, column));
|
|
133
|
+
});
|
|
134
|
+
Table.getCompositeIndexes(table).forEach(indexFields => {
|
|
135
|
+
all.push({
|
|
136
|
+
tableName: table.tableName,
|
|
137
|
+
indexFields: indexFields
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
entities.forEach(param => {
|
|
142
|
+
Table.getDerivedFromFields(param.table).forEach(derivedFromField => {
|
|
143
|
+
let column = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(derivedSchema, derivedFromField));
|
|
144
|
+
all.push(makeSingleColumnSchemaIndex(derivedFromField.derivedFromEntity, column));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
let seen = new Set();
|
|
148
|
+
return all.filter(schemaIndex => {
|
|
149
|
+
let key = schemaIndexKey(schemaIndex);
|
|
150
|
+
if (seen.has(key)) {
|
|
151
|
+
return false;
|
|
152
|
+
} else {
|
|
153
|
+
seen.add(key);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
81
157
|
}
|
|
82
158
|
|
|
83
159
|
function makeCreateTableQuery(table, pgSchema, isNumericArrayAsText) {
|
|
@@ -157,7 +233,7 @@ function getEntityHistory(entityConfig) {
|
|
|
157
233
|
checkpointIdField,
|
|
158
234
|
actionField
|
|
159
235
|
]), undefined);
|
|
160
|
-
let setChangeSchema = EntityHistory.makeSetUpdateSchema(entityConfig.schema);
|
|
236
|
+
let setChangeSchema = EntityHistory.makeSetUpdateSchema(Table.getIdSchema(entityConfig.table), entityConfig.schema);
|
|
161
237
|
let cache_setChangeSchemaRows = S$RescriptSchema.array(setChangeSchema);
|
|
162
238
|
let cache$1 = {
|
|
163
239
|
table: table,
|
|
@@ -168,11 +244,12 @@ function getEntityHistory(entityConfig) {
|
|
|
168
244
|
return cache$1;
|
|
169
245
|
}
|
|
170
246
|
|
|
171
|
-
function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigsOpt, entitiesOpt, enumsOpt, isEmptyPgSchemaOpt) {
|
|
247
|
+
function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigsOpt, entitiesOpt, enumsOpt, isEmptyPgSchemaOpt, deferSchemaIndexesOpt) {
|
|
172
248
|
let chainConfigs = chainConfigsOpt !== undefined ? chainConfigsOpt : [];
|
|
173
249
|
let entities = entitiesOpt !== undefined ? entitiesOpt : [];
|
|
174
250
|
let enums = enumsOpt !== undefined ? enumsOpt : [];
|
|
175
251
|
let isEmptyPgSchema = isEmptyPgSchemaOpt !== undefined ? isEmptyPgSchemaOpt : false;
|
|
252
|
+
let deferSchemaIndexes = deferSchemaIndexesOpt !== undefined ? deferSchemaIndexesOpt : false;
|
|
176
253
|
let generalTables = [
|
|
177
254
|
InternalTable.Chains.table,
|
|
178
255
|
InternalTable.EnvioInfo.table,
|
|
@@ -180,13 +257,12 @@ function makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfi
|
|
|
180
257
|
InternalTable.RawEvents.table
|
|
181
258
|
];
|
|
182
259
|
let allTables = generalTables.slice();
|
|
183
|
-
let allEntityTables = [];
|
|
184
260
|
entities.forEach(entityConfig => {
|
|
185
|
-
allEntityTables.push(entityConfig.table);
|
|
186
261
|
allTables.push(entityConfig.table);
|
|
187
262
|
allTables.push(getEntityHistory(entityConfig).table);
|
|
188
263
|
});
|
|
189
|
-
let
|
|
264
|
+
let schemaIndexes = getSchemaIndexes(entities);
|
|
265
|
+
IndexRegistry.validateIndexNamesOrThrow(schemaIndexes.map(schemaIndexDescription));
|
|
190
266
|
let query = {
|
|
191
267
|
contents: (
|
|
192
268
|
isEmptyPgSchema && pgSchema === "public" ? `CREATE SCHEMA IF NOT EXISTS "` + pgSchema + `";\n` : `DROP SCHEMA IF EXISTS "` + pgSchema + `" CASCADE;
|
|
@@ -201,19 +277,11 @@ GRANT ALL ON SCHEMA "` + pgSchema + `" TO public;`)
|
|
|
201
277
|
allTables.forEach(table => {
|
|
202
278
|
query.contents = query.contents + "\n" + makeCreateTableQuery(table, pgSchema, isHasuraEnabled);
|
|
203
279
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
query.contents = query.contents + "\n" + indices;
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
entities.forEach(entity => {
|
|
212
|
-
Table.getDerivedFromFields(entity.table).forEach(derivedFromField => {
|
|
213
|
-
let indexField = Utils.unwrapResultExn(Schema.getDerivedFromPgFieldName(derivedSchema, derivedFromField));
|
|
214
|
-
query.contents = query.contents + "\n" + makeCreateIndexQuery(derivedFromField.derivedFromEntity, [indexField], pgSchema);
|
|
280
|
+
if (!deferSchemaIndexes) {
|
|
281
|
+
schemaIndexes.forEach(schemaIndex => {
|
|
282
|
+
query.contents = query.contents + "\n" + makeCreateSchemaIndexQuery(schemaIndex, pgSchema);
|
|
215
283
|
});
|
|
216
|
-
}
|
|
284
|
+
}
|
|
217
285
|
query.contents = query.contents + "\n" + InternalTable.Views.makeMetaViewQuery(pgSchema);
|
|
218
286
|
query.contents = query.contents + "\n" + InternalTable.Views.makeChainMetadataViewQuery(pgSchema);
|
|
219
287
|
let initialChainsValuesQuery = InternalTable.Chains.makeInitialValuesQuery(pgSchema, chainConfigs);
|
|
@@ -292,8 +360,8 @@ function makeDeleteByIdQuery(pgSchema, tableName) {
|
|
|
292
360
|
return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = $1;`;
|
|
293
361
|
}
|
|
294
362
|
|
|
295
|
-
function makeDeleteByIdsQuery(pgSchema, tableName) {
|
|
296
|
-
return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1
|
|
363
|
+
function makeDeleteByIdsQuery(pgSchema, tableName, idPgType) {
|
|
364
|
+
return `DELETE FROM "` + pgSchema + `"."` + tableName + `" WHERE id = ANY($1::` + idPgType + `[]);`;
|
|
297
365
|
}
|
|
298
366
|
|
|
299
367
|
function makeLoadAllQuery(pgSchema, tableName) {
|
|
@@ -547,9 +615,10 @@ async function getConnectedPsqlExec(pgUser, pgHost, pgDatabase, pgPort, containe
|
|
|
547
615
|
}
|
|
548
616
|
|
|
549
617
|
async function deleteByIdsOrThrow(sql, pgSchema, ids, table) {
|
|
618
|
+
let idsJson = Table.encodeIdsToJson(table, ids);
|
|
550
619
|
try {
|
|
551
620
|
await (
|
|
552
|
-
ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName), [
|
|
621
|
+
ids.length !== 1 ? sql.unsafe(makeDeleteByIdsQuery(pgSchema, table.tableName, Table.getIdPgFieldType(table, pgSchema)), [idsJson], {prepare: true}) : sql.unsafe(makeDeleteByIdQuery(pgSchema, table.tableName), idsJson, {prepare: true})
|
|
553
622
|
);
|
|
554
623
|
return;
|
|
555
624
|
} catch (raw_exn) {
|
|
@@ -586,9 +655,10 @@ function makeInsertDeleteUpdatesQuery(entityConfig, pgSchema) {
|
|
|
586
655
|
});
|
|
587
656
|
let selectPartsStr = selectParts.join(", ");
|
|
588
657
|
let checkpointIdPgType = Table.getPgFieldType(EntityHistory.checkpointIdFieldType, pgSchema, false, false, false);
|
|
658
|
+
let idPgType = Table.getIdPgFieldType(entityConfig.table, pgSchema);
|
|
589
659
|
return `INSERT INTO "` + pgSchema + `"."` + historyTableName + `" (` + allHistoryFieldNamesStr + `)
|
|
590
660
|
SELECT ` + selectPartsStr + `
|
|
591
|
-
FROM UNNEST($1
|
|
661
|
+
FROM UNNEST($1::` + idPgType + `[], $2::` + checkpointIdPgType + `[]) AS u(` + Table.idFieldName + `, ` + EntityHistory.checkpointIdFieldName + `)`;
|
|
592
662
|
}
|
|
593
663
|
|
|
594
664
|
function executeSet(sql, items, dbFunction) {
|
|
@@ -648,15 +718,16 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
|
|
|
648
718
|
let orderedIds = [];
|
|
649
719
|
param.changes.forEach(change => {
|
|
650
720
|
let entityId = change.entityId;
|
|
651
|
-
|
|
721
|
+
let entityKey = EntityId.toKey(entityId);
|
|
722
|
+
if (Stdlib_Option.isNone(latestChangeById[entityKey])) {
|
|
652
723
|
orderedIds.push(entityId);
|
|
653
724
|
}
|
|
654
|
-
latestChangeById[
|
|
725
|
+
latestChangeById[entityKey] = change;
|
|
655
726
|
if (!shouldSaveHistory) {
|
|
656
727
|
return;
|
|
657
728
|
}
|
|
658
729
|
if (change.checkpointId === diffCheckpointId) {
|
|
659
|
-
idsWithDiff.add(
|
|
730
|
+
idsWithDiff.add(entityKey);
|
|
660
731
|
return;
|
|
661
732
|
}
|
|
662
733
|
if (change.type === "SET") {
|
|
@@ -668,13 +739,14 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
|
|
|
668
739
|
});
|
|
669
740
|
let backfillHistoryIds = new Set();
|
|
670
741
|
orderedIds.forEach(entityId => {
|
|
671
|
-
let
|
|
742
|
+
let entityKey = EntityId.toKey(entityId);
|
|
743
|
+
let match = latestChangeById[entityKey];
|
|
672
744
|
if (match.type === "SET") {
|
|
673
745
|
entitiesToSet.push(match.entity);
|
|
674
746
|
} else {
|
|
675
747
|
idsToDelete.push(match.entityId);
|
|
676
748
|
}
|
|
677
|
-
if (shouldSaveHistory && !idsWithDiff.has(
|
|
749
|
+
if (shouldSaveHistory && !idsWithDiff.has(entityKey)) {
|
|
678
750
|
backfillHistoryIds.add(entityId);
|
|
679
751
|
return;
|
|
680
752
|
}
|
|
@@ -685,11 +757,11 @@ async function writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, co
|
|
|
685
757
|
let promises = [];
|
|
686
758
|
if (shouldSaveHistory) {
|
|
687
759
|
if (backfillHistoryIds.size !== 0) {
|
|
688
|
-
await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.
|
|
760
|
+
await EntityHistory.backfillHistory(sql, pgSchema, entityConfig.table, entityConfig.index, Array.from(backfillHistoryIds));
|
|
689
761
|
}
|
|
690
762
|
if (Utils.$$Array.notEmpty(batchDeleteCheckpointIds)) {
|
|
691
763
|
promises.push(sql.unsafe(makeInsertDeleteUpdatesQuery(entityConfig, pgSchema), [
|
|
692
|
-
batchDeleteEntityIds,
|
|
764
|
+
Table.encodeIdsToJson(entityConfig.table, batchDeleteEntityIds),
|
|
693
765
|
Utils.$$BigInt.arrayToStringArray(batchDeleteCheckpointIds)
|
|
694
766
|
], {prepare: true}));
|
|
695
767
|
}
|
|
@@ -822,10 +894,12 @@ function makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema) {
|
|
|
822
894
|
)`;
|
|
823
895
|
}
|
|
824
896
|
|
|
825
|
-
let rollbackRowStateSchema = S$RescriptSchema.object(s => [
|
|
826
|
-
s.f(Table.idFieldName,
|
|
897
|
+
let rollbackRowStateSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.object(s => [
|
|
898
|
+
s.f(Table.idFieldName, Table.getIdSchema(table)),
|
|
827
899
|
s.f(EntityHistory.changeFieldName, EntityHistory.RowAction.schema)
|
|
828
|
-
]);
|
|
900
|
+
]));
|
|
901
|
+
|
|
902
|
+
let rollbackRemovedIdsSchema = Utils.$$WeakMap.memoize(table => S$RescriptSchema.array(S$RescriptSchema.object(s => s.f(Table.idFieldName, Table.getIdSchema(table)))));
|
|
829
903
|
|
|
830
904
|
function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isHasuraEnabled, sink, onInitialize, onNewTables) {
|
|
831
905
|
let containerName = "envio-postgres";
|
|
@@ -843,6 +917,23 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
843
917
|
env: psqlExecOptions_env
|
|
844
918
|
};
|
|
845
919
|
let cacheDirPath = Path.resolve(".envio", "cache");
|
|
920
|
+
let storageName = "postgres";
|
|
921
|
+
let indexRegistry = IndexRegistry.make();
|
|
922
|
+
let reloadIndexRegistry = async () => {
|
|
923
|
+
let rows = S$RescriptSchema.parseOrThrow(await sql.unsafe(IndexRegistry.makeCatalogQuery(pgSchema)), IndexRegistry.catalogRowsSchema);
|
|
924
|
+
let invalidIndexNames = IndexRegistry.reload(indexRegistry, rows);
|
|
925
|
+
if (invalidIndexNames.length !== 0) {
|
|
926
|
+
Logging.warn({
|
|
927
|
+
storage: storageName,
|
|
928
|
+
msg: `Ignoring invalid PostgreSQL indexes in schema "` + pgSchema + `". They are left untouched — drop and recreate them manually if you need them.`,
|
|
929
|
+
indexes: invalidIndexNames
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return Logging.info({
|
|
933
|
+
storage: storageName,
|
|
934
|
+
msg: `Found ` + IndexRegistry.size(indexRegistry).toString() + ` existing indexes in the indexer schema.`
|
|
935
|
+
});
|
|
936
|
+
};
|
|
846
937
|
let isInitialized = async () => Utils.$$Array.notEmpty(await sql.unsafe(`SELECT table_schema FROM information_schema.tables WHERE table_schema = '` + pgSchema + `' AND (table_name = '` + "event_sync_state" + `' OR table_name = '` + InternalTable.Chains.table.tableName + `');`));
|
|
847
938
|
let scanCacheDir = async () => {
|
|
848
939
|
let topEntries;
|
|
@@ -962,7 +1053,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
|
|
|
962
1053
|
if (sink !== undefined) {
|
|
963
1054
|
await sink.initialize(chainConfigs, chEntities, enums);
|
|
964
1055
|
}
|
|
965
|
-
let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames));
|
|
1056
|
+
let queries = makeInitializeTransaction(pgSchema, pgUser, isHasuraEnabled, chainConfigs, pgEntities, enums, Utils.$$Array.isEmpty(schemaTableNames), true);
|
|
966
1057
|
await sql.begin(async sql => {
|
|
967
1058
|
await Promise.all(queries.map(query => sql.unsafe(query)));
|
|
968
1059
|
return await InternalTable.EnvioInfo.write(sql, pgSchema, envioInfo);
|
|
@@ -988,6 +1079,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
988
1079
|
]);
|
|
989
1080
|
}
|
|
990
1081
|
let cache = await restoreEffectCache(true);
|
|
1082
|
+
await reloadIndexRegistry();
|
|
991
1083
|
if (onInitialize !== undefined) {
|
|
992
1084
|
await onInitialize();
|
|
993
1085
|
}
|
|
@@ -1038,6 +1130,121 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1038
1130
|
};
|
|
1039
1131
|
}
|
|
1040
1132
|
};
|
|
1133
|
+
let filterColumns = (table, filters) => {
|
|
1134
|
+
let queryFields = Table.queryFields(table);
|
|
1135
|
+
let columns = [];
|
|
1136
|
+
let seen = new Set();
|
|
1137
|
+
let collect = filter => {
|
|
1138
|
+
if (filter.operator === "and") {
|
|
1139
|
+
filter.filters.forEach(collect);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
let match = queryFields[filter.fieldName];
|
|
1143
|
+
if (match === undefined) {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
let pgDbFieldName = match.pgDbFieldName;
|
|
1147
|
+
if (!seen.has(pgDbFieldName)) {
|
|
1148
|
+
seen.add(pgDbFieldName);
|
|
1149
|
+
columns.push(pgDbFieldName);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
filters.forEach(collect);
|
|
1154
|
+
return columns;
|
|
1155
|
+
};
|
|
1156
|
+
let makeEnsureIndexQuery = schemaIndex => {
|
|
1157
|
+
let indexName = IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
|
|
1158
|
+
let match = IndexRegistry.getInvalid(indexRegistry, indexName);
|
|
1159
|
+
if (match === undefined) {
|
|
1160
|
+
return makeCreateSchemaIndexQuery(schemaIndex, pgSchema);
|
|
1161
|
+
}
|
|
1162
|
+
if (match.isRepairable) {
|
|
1163
|
+
if (match.key === schemaIndexKey(schemaIndex)) {
|
|
1164
|
+
return makeReindexQuery(pgSchema, indexName);
|
|
1165
|
+
}
|
|
1166
|
+
throw nameCollisionError(indexName, pgSchema);
|
|
1167
|
+
}
|
|
1168
|
+
throw nameCollisionError(indexName, pgSchema);
|
|
1169
|
+
};
|
|
1170
|
+
let isRepair = schemaIndex => Stdlib_Option.isSome(IndexRegistry.getInvalid(indexRegistry, IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex))));
|
|
1171
|
+
let ensureQueryIndexes = async (table, filters) => {
|
|
1172
|
+
let missing = Stdlib_Array.filterMap(filterColumns(table, filters), column => {
|
|
1173
|
+
let schemaIndex = makeSingleColumnSchemaIndex(table.tableName, column);
|
|
1174
|
+
if (IndexRegistry.has(indexRegistry, schemaIndexKey(schemaIndex))) {
|
|
1175
|
+
return;
|
|
1176
|
+
} else {
|
|
1177
|
+
return schemaIndex;
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
if (Utils.$$Array.notEmpty(missing)) {
|
|
1181
|
+
await Promise.all(missing.map(schemaIndex => {
|
|
1182
|
+
let indexName = IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex));
|
|
1183
|
+
return IndexRegistry.ensure(indexRegistry, schemaIndexKey(schemaIndex), table.tableName, async () => {
|
|
1184
|
+
let query = makeEnsureIndexQuery(schemaIndex);
|
|
1185
|
+
let verb = isRepair(schemaIndex) ? "Repairing invalid index" : "Creating index";
|
|
1186
|
+
Logging.info({
|
|
1187
|
+
storage: storageName,
|
|
1188
|
+
msg: verb + ` "` + indexName + `" to serve a getWhere query on "` + table.tableName + `". Writes to the table are paused until it completes. ` + slowOnLargeDatabaseNotice
|
|
1189
|
+
});
|
|
1190
|
+
let timeRef = Performance.now();
|
|
1191
|
+
await sql.unsafe(query);
|
|
1192
|
+
IndexRegistry.clearInvalidName(indexRegistry, indexName);
|
|
1193
|
+
return Logging.info({
|
|
1194
|
+
storage: storageName,
|
|
1195
|
+
msg: `Index "` + indexName + `" is ready after ` + formatSeconds(timeRef) + `s. Resuming indexing.`
|
|
1196
|
+
});
|
|
1197
|
+
}).catch(exn => Logging.warn({
|
|
1198
|
+
storage: storageName,
|
|
1199
|
+
msg: `Failed to create the index "` + indexName + `" for a getWhere query. The query runs without it.`,
|
|
1200
|
+
err: Utils.prettifyExn(exn)
|
|
1201
|
+
}));
|
|
1202
|
+
}));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
let finalizeBackfill = async (entities, chainIds, readyAt) => {
|
|
1207
|
+
let schemaIndexes = getSchemaIndexes(entities.filter(e => e.storage.postgres));
|
|
1208
|
+
IndexRegistry.validateIndexNamesOrThrow(schemaIndexes.map(schemaIndexDescription));
|
|
1209
|
+
let missing = schemaIndexes.filter(schemaIndex => !IndexRegistry.has(indexRegistry, schemaIndexKey(schemaIndex)));
|
|
1210
|
+
let repaired = missing.filter(isRepair);
|
|
1211
|
+
if (Utils.$$Array.notEmpty(repaired)) {
|
|
1212
|
+
Logging.warn({
|
|
1213
|
+
storage: storageName,
|
|
1214
|
+
msg: `PostgreSQL reports ` + repaired.length.toString() + ` existing indexes as invalid, so they can't serve queries. Rebuilding them.`,
|
|
1215
|
+
indexes: repaired.map(schemaIndexName)
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (missing.length !== 0) {
|
|
1219
|
+
Logging.info({
|
|
1220
|
+
storage: storageName,
|
|
1221
|
+
msg: `Creating the ` + missing.length.toString() + ` remaining schema indexes before the indexer reports ready. Writes are paused until they are committed. ` + slowOnLargeDatabaseNotice,
|
|
1222
|
+
indexes: missing.map(schemaIndexName)
|
|
1223
|
+
});
|
|
1224
|
+
} else {
|
|
1225
|
+
Logging.info({
|
|
1226
|
+
storage: storageName,
|
|
1227
|
+
msg: `All ` + schemaIndexes.length.toString() + ` schema indexes are already in place. Marking the indexer ready.`
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
let indexQueries = missing.map(makeEnsureIndexQuery);
|
|
1231
|
+
let timeRef = Performance.now();
|
|
1232
|
+
await sql.begin(async sql => {
|
|
1233
|
+
for (let idx = 0, idx_finish = indexQueries.length; idx < idx_finish; ++idx) {
|
|
1234
|
+
await sql.unsafe(indexQueries[idx]);
|
|
1235
|
+
}
|
|
1236
|
+
return await sql.unsafe(InternalTable.Chains.makeSetReadyAtQuery(pgSchema), [
|
|
1237
|
+
readyAt,
|
|
1238
|
+
chainIds
|
|
1239
|
+
], {prepare: true});
|
|
1240
|
+
});
|
|
1241
|
+
missing.forEach(schemaIndex => IndexRegistry.add(indexRegistry, schemaIndexKey(schemaIndex)));
|
|
1242
|
+
repaired.forEach(schemaIndex => IndexRegistry.clearInvalidName(indexRegistry, IndexRegistry.truncateIndexName(schemaIndexDescription(schemaIndex))));
|
|
1243
|
+
return Logging.info({
|
|
1244
|
+
storage: storageName,
|
|
1245
|
+
msg: `Committed ` + missing.length.toString() + ` schema indexes and the ready timestamp in ` + formatSeconds(timeRef) + `s.`
|
|
1246
|
+
});
|
|
1247
|
+
};
|
|
1041
1248
|
let setOrThrow$1 = (items, table, itemSchema) => setOrThrow(sql, items, table, itemSchema, pgSchema);
|
|
1042
1249
|
let setEffectCacheOrThrow = async (table, itemSchema, items, initialize) => {
|
|
1043
1250
|
if (initialize) {
|
|
@@ -1114,6 +1321,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1114
1321
|
sql.unsafe(InternalTable.Checkpoints.makeGetReorgCheckpointsQuery(pgSchema)),
|
|
1115
1322
|
InternalTable.EnvioInfo.read(sql, pgSchema)
|
|
1116
1323
|
]);
|
|
1324
|
+
await reloadIndexRegistry();
|
|
1117
1325
|
let checkpointId = BigInt(match[2][0].id);
|
|
1118
1326
|
let reorgCheckpoints = match[3].map(raw => ({
|
|
1119
1327
|
id: BigInt(raw.id),
|
|
@@ -1147,10 +1355,10 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1147
1355
|
sql.unsafe(makeGetRollbackRemovedIdsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true}),
|
|
1148
1356
|
sql.unsafe(makeGetRollbackPreTargetRowsQuery(entityConfig, pgSchema), [rollbackTargetCheckpointId.toString()], {prepare: true})
|
|
1149
1357
|
]);
|
|
1150
|
-
let removedIds = match[0]
|
|
1358
|
+
let removedIds = S$RescriptSchema.parseOrThrow(match[0], rollbackRemovedIdsSchema(entityConfig.table));
|
|
1151
1359
|
let restoredEntitiesResult = [];
|
|
1152
1360
|
match[1].forEach(row => {
|
|
1153
|
-
let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema);
|
|
1361
|
+
let match = S$RescriptSchema.parseOrThrow(row, rollbackRowStateSchema(entityConfig.table));
|
|
1154
1362
|
if (match[1] === "SET") {
|
|
1155
1363
|
restoredEntitiesResult.push(row);
|
|
1156
1364
|
return;
|
|
@@ -1186,15 +1394,17 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::int[],$3::
|
|
|
1186
1394
|
}
|
|
1187
1395
|
let primaryTimerRef = Performance.now();
|
|
1188
1396
|
await writeBatch(sql, batch, pgSchema, rollback, isInReorgThreshold, config, allEntities, setEffectCacheOrThrow, updatedEffectsCache, pgUpdates, sinkPromise, chainMetaData, undefined);
|
|
1189
|
-
return onWrite(
|
|
1397
|
+
return onWrite(storageName, Performance.secondsSince(primaryTimerRef));
|
|
1190
1398
|
};
|
|
1191
1399
|
let close = () => sql.end();
|
|
1192
1400
|
return {
|
|
1193
|
-
name:
|
|
1401
|
+
name: storageName,
|
|
1194
1402
|
isInitialized: isInitialized,
|
|
1195
1403
|
initialize: initialize,
|
|
1196
1404
|
resumeInitialState: resumeInitialState,
|
|
1197
1405
|
loadOrThrow: loadOrThrow,
|
|
1406
|
+
ensureQueryIndexes: ensureQueryIndexes,
|
|
1407
|
+
finalizeBackfill: finalizeBackfill,
|
|
1198
1408
|
dumpEffectCache: dumpEffectCache,
|
|
1199
1409
|
reset: reset,
|
|
1200
1410
|
setChainMeta: setChainMeta,
|
|
@@ -1264,7 +1474,17 @@ export {
|
|
|
1264
1474
|
directionToSql,
|
|
1265
1475
|
directionToIndexName,
|
|
1266
1476
|
makeCreateCompositeIndexQuery,
|
|
1267
|
-
|
|
1477
|
+
makeCreateTableIndexesQuery,
|
|
1478
|
+
schemaIndexDescription,
|
|
1479
|
+
schemaIndexName,
|
|
1480
|
+
schemaIndexKey,
|
|
1481
|
+
makeCreateSchemaIndexQuery,
|
|
1482
|
+
formatSeconds,
|
|
1483
|
+
slowOnLargeDatabaseNotice,
|
|
1484
|
+
makeReindexQuery,
|
|
1485
|
+
nameCollisionError,
|
|
1486
|
+
makeSingleColumnSchemaIndex,
|
|
1487
|
+
getSchemaIndexes,
|
|
1268
1488
|
makeCreateTableQuery,
|
|
1269
1489
|
entityHistoryCache,
|
|
1270
1490
|
getEntityHistory,
|
|
@@ -1297,6 +1517,7 @@ export {
|
|
|
1297
1517
|
makeGetRollbackPreTargetRowsQuery,
|
|
1298
1518
|
makeGetRollbackRemovedIdsQuery,
|
|
1299
1519
|
rollbackRowStateSchema,
|
|
1520
|
+
rollbackRemovedIdsSchema,
|
|
1300
1521
|
make,
|
|
1301
1522
|
makeStorageFromEnv,
|
|
1302
1523
|
makePersistenceFromConfig,
|
package/src/SimulateItems.res
CHANGED
|
@@ -343,8 +343,8 @@ let parse = (
|
|
|
343
343
|
// run here the way real routing does (one item per registration).
|
|
344
344
|
// Registrations are built the same way `HandlerRegister.finishRegistration`
|
|
345
345
|
// does at startup (not stubs), so the address filter and `where` behave
|
|
346
|
-
// identically to real indexing — the dead-input tracker relies on
|
|
347
|
-
//
|
|
346
|
+
// identically to real indexing — the dead-input tracker relies on the
|
|
347
|
+
// source's address gate actually dropping unrouted items. Drop registrations
|
|
348
348
|
// whose `where` excludes this chain (they wouldn't be fetched live), and
|
|
349
349
|
// ignore the bare handler-less fallback so a missing handler surfaces below
|
|
350
350
|
// instead of silently running nothing.
|
package/src/TestIndexer.res
CHANGED
|
@@ -22,7 +22,7 @@ type t<'processConfig> = {process: 'processConfig => promise<processResult>}
|
|
|
22
22
|
|
|
23
23
|
type entityChange = {
|
|
24
24
|
sets: array<unknown>,
|
|
25
|
-
deleted: array<
|
|
25
|
+
deleted: array<EntityId.t>,
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
type testIndexerState = {
|
|
@@ -136,11 +136,12 @@ let handleWriteBatch = (
|
|
|
136
136
|
switch change {
|
|
137
137
|
| Set({entityId, entity, checkpointId}) =>
|
|
138
138
|
// The store keeps decoded entities so load comparisons (bigint /
|
|
139
|
-
// BigDecimal) work on real values.
|
|
140
|
-
|
|
139
|
+
// BigDecimal) work on real values. Ids are keyed by their string form
|
|
140
|
+
// since they may be string/int/bigint.
|
|
141
|
+
entityDict->Dict.set(entityId->EntityId.toKey, entity)
|
|
141
142
|
entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
|
|
142
143
|
| Delete({entityId, checkpointId}) =>
|
|
143
|
-
Dict.delete(entityDict->Obj.magic, entityId)
|
|
144
|
+
Dict.delete(entityDict->Obj.magic, entityId->EntityId.toKey)
|
|
144
145
|
entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -197,7 +198,7 @@ let handleWriteBatch = (
|
|
|
197
198
|
entityObj->Dict.set("sets", sets->(Utils.magic: array<unknown> => unknown))
|
|
198
199
|
}
|
|
199
200
|
if deleted->Array.length > 0 {
|
|
200
|
-
entityObj->Dict.set("deleted", deleted->(Utils.magic: array<
|
|
201
|
+
entityObj->Dict.set("deleted", deleted->(Utils.magic: array<EntityId.t> => unknown))
|
|
201
202
|
}
|
|
202
203
|
// Match the capitalized entity accessor the generated change types expose.
|
|
203
204
|
change->Dict.set(
|
|
@@ -484,6 +485,9 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
|
|
|
484
485
|
state
|
|
485
486
|
->handleLoad(~tableName=table.tableName, ~filter)
|
|
486
487
|
->(Utils.magic: array<Internal.entity> => array<unknown>),
|
|
488
|
+
// The in-memory storage has no indexes to build, and it's always ready.
|
|
489
|
+
ensureQueryIndexes: async (~table as _, ~filters as _) => (),
|
|
490
|
+
finalizeBackfill: async (~entities as _, ~chainIds as _, ~readyAt as _) => (),
|
|
487
491
|
writeBatch: async (
|
|
488
492
|
~batch,
|
|
489
493
|
~rollback as _,
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import * as Utils from "./Utils.res.mjs";
|
|
|
5
5
|
import * as Config from "./Config.res.mjs";
|
|
6
6
|
import * as Logging from "./Logging.res.mjs";
|
|
7
7
|
import * as ChainMap from "./ChainMap.res.mjs";
|
|
8
|
+
import * as EntityId from "./EntityId.res.mjs";
|
|
8
9
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
9
10
|
import * as IndexerLoop from "./IndexerLoop.res.mjs";
|
|
10
11
|
import * as Persistence from "./Persistence.res.mjs";
|
|
@@ -102,12 +103,12 @@ function handleWriteBatch(state, updatedEntities, checkpointIds, checkpointChain
|
|
|
102
103
|
let processChange = change => {
|
|
103
104
|
if (change.type === "SET") {
|
|
104
105
|
let entity = change.entity;
|
|
105
|
-
entityDict[change.entityId] = entity;
|
|
106
|
+
entityDict[EntityId.toKey(change.entityId)] = entity;
|
|
106
107
|
entityChangeFor(change.checkpointId).sets.push(entity);
|
|
107
108
|
return;
|
|
108
109
|
}
|
|
109
110
|
let entityId = change.entityId;
|
|
110
|
-
Stdlib_Dict.$$delete(entityDict, entityId);
|
|
111
|
+
Stdlib_Dict.$$delete(entityDict, EntityId.toKey(entityId));
|
|
111
112
|
entityChangeFor(change.checkpointId).deleted.push(entityId);
|
|
112
113
|
};
|
|
113
114
|
param.changes.forEach(processChange);
|
|
@@ -314,6 +315,8 @@ function makeInMemoryStorage(state) {
|
|
|
314
315
|
initialize: async (param, param$1, param$2, param$3) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
|
|
315
316
|
resumeInitialState: async () => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
|
|
316
317
|
loadOrThrow: async (filter, table) => handleLoad(state, table.tableName, filter),
|
|
318
|
+
ensureQueryIndexes: async (param, param$1) => {},
|
|
319
|
+
finalizeBackfill: async (param, param$1, param$2) => {},
|
|
317
320
|
dumpEffectCache: async () => {},
|
|
318
321
|
reset: async () => {},
|
|
319
322
|
setChainMeta: async param => {},
|
package/src/UserContext.res
CHANGED
|
@@ -144,7 +144,7 @@ let getWhereHandler = (params: entityContextParams, filter: dict<dict<unknown>>)
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
let noopSet = (_entity: Internal.entity) => ()
|
|
147
|
-
let noopDeleteUnsafe = (_entityId:
|
|
147
|
+
let noopDeleteUnsafe = (_entityId: EntityId.t) => ()
|
|
148
148
|
|
|
149
149
|
// Reads against ClickHouse-only entities have no Postgres table to hit;
|
|
150
150
|
// surface a friendly error instead of letting the SQL layer fail with
|
|
@@ -168,7 +168,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
168
168
|
->InMemoryTable.Entity.set(
|
|
169
169
|
~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointId,
|
|
170
170
|
Set({
|
|
171
|
-
entityId: entity.id,
|
|
171
|
+
entityId: entity.id->EntityId.unsafeOfString,
|
|
172
172
|
checkpointId: params.checkpointId,
|
|
173
173
|
entity,
|
|
174
174
|
}),
|
|
@@ -282,7 +282,7 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
|
|
|
282
282
|
}),
|
|
283
283
|
)
|
|
284
284
|
}
|
|
285
|
-
}->(Utils.magic: (
|
|
285
|
+
}->(Utils.magic: (EntityId.t => unit) => unknown)
|
|
286
286
|
| _ =>
|
|
287
287
|
JsError.throwWithMessage(`Invalid context.${params.entityConfig.name}.${prop} operation.`)
|
|
288
288
|
}
|
package/src/Writing.res
CHANGED
|
@@ -237,7 +237,7 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
|
|
|
237
237
|
|
|
238
238
|
// Blocks until the store holds fewer than keepLatestChangesLimit changes,
|
|
239
239
|
// freeing committed changes first and awaiting commits as a last resort.
|
|
240
|
-
let rec
|
|
240
|
+
let rec awaitCapacityLoop = async (state: IndexerState.t) => {
|
|
241
241
|
// After a failed write nothing will free capacity, so bail instead of waiting
|
|
242
242
|
// on a commit that won't come (the error already went to onError).
|
|
243
243
|
if !(state->IndexerState.hasFailedWrite) && state->getChangesCount >= keepLatestChangesLimit {
|
|
@@ -259,11 +259,21 @@ let rec awaitCapacity = async (state: IndexerState.t) => {
|
|
|
259
259
|
) {
|
|
260
260
|
state->schedule
|
|
261
261
|
await state->waitForCommit
|
|
262
|
-
await state->
|
|
262
|
+
await state->awaitCapacityLoop
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
// Only the over-limit path is a real stall. Timing every call would charge each
|
|
268
|
+
// batch the microtask hop of awaiting an already-resolved promise, which reads
|
|
269
|
+
// as storage backpressure that isn't there.
|
|
270
|
+
let awaitCapacity = async (state: IndexerState.t) =>
|
|
271
|
+
if state->getChangesCount >= keepLatestChangesLimit {
|
|
272
|
+
let timeRef = Performance.now()
|
|
273
|
+
await state->awaitCapacityLoop
|
|
274
|
+
state->IndexerState.recordStalledOnStorageWrite(~seconds=timeRef->Performance.secondsSince)
|
|
275
|
+
}
|
|
276
|
+
|
|
267
277
|
// Awaits until everything processed is persisted. On a failed write we stop
|
|
268
278
|
// draining (onError already surfaced it) rather than throw.
|
|
269
279
|
let rec flush = async (state: IndexerState.t) => {
|