envio 3.9.0 → 3.10.0-subgraph
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/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +41 -8
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +61 -44
- package/src/Config.res.mjs +56 -19
- package/src/Core.res +19 -0
- package/src/Core.res.mjs +11 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +8 -6
- package/src/HandlerLoader.res +20 -8
- package/src/HandlerLoader.res.mjs +11 -2
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/UserContext.res +358 -51
- package/src/UserContext.res.mjs +271 -35
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/src/subgraph/blocks.ts +176 -0
- package/src/subgraph/calls.ts +217 -0
- package/src/subgraph/conformance.ts +102 -0
- package/src/subgraph/division.ts +133 -0
- package/src/subgraph/errors.ts +90 -0
- package/src/subgraph/graph-ts-types/VERSION +2 -0
- package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
- package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
- package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
- package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
- package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
- package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
- package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
- package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
- package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
- package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
- package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
- package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
- package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
- package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
- package/src/subgraph/graph-ts-types/index.d.ts +102 -0
- package/src/subgraph/graph-ts.ts +1948 -0
- package/src/subgraph/hosts.ts +148 -0
- package/src/subgraph/runtime.ts +863 -0
- package/src/subgraph/scope.ts +66 -0
- package/svm.schema.json +6 -81
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
package/src/UserContext.res.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Utils from "./Utils.res.mjs";
|
|
4
|
+
import * as ChainId from "./ChainId.res.mjs";
|
|
4
5
|
import * as Logging from "./Logging.res.mjs";
|
|
5
6
|
import * as Internal from "./Internal.res.mjs";
|
|
6
7
|
import * as Ecosystem from "./Ecosystem.res.mjs";
|
|
@@ -12,8 +13,70 @@ import * as InMemoryStore from "./InMemoryStore.res.mjs";
|
|
|
12
13
|
import * as InMemoryTable from "./InMemoryTable.res.mjs";
|
|
13
14
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
14
15
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
16
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
15
17
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
16
18
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
19
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
20
|
+
|
|
21
|
+
let Suspend = /* @__PURE__ */Primitive_exceptions.create("UserContext.Suspend");
|
|
22
|
+
|
|
23
|
+
function isSuspend(exn) {
|
|
24
|
+
return exn.RE_EXN_ID === Suspend;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeSyncState() {
|
|
28
|
+
return {
|
|
29
|
+
status: "Active",
|
|
30
|
+
pending: undefined,
|
|
31
|
+
memo: undefined
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPending(sync) {
|
|
36
|
+
let pending = sync.pending;
|
|
37
|
+
if (pending !== undefined) {
|
|
38
|
+
return pending;
|
|
39
|
+
}
|
|
40
|
+
let pending$1 = [];
|
|
41
|
+
sync.pending = pending$1;
|
|
42
|
+
return pending$1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getMemo(sync) {
|
|
46
|
+
let memo = sync.memo;
|
|
47
|
+
if (memo !== undefined) {
|
|
48
|
+
return memo;
|
|
49
|
+
}
|
|
50
|
+
let memo$1 = {};
|
|
51
|
+
sync.memo = memo$1;
|
|
52
|
+
return memo$1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkStatusOrThrow(params, access) {
|
|
56
|
+
let exn = params.sync.status;
|
|
57
|
+
if (typeof exn !== "object") {
|
|
58
|
+
if (exn === "Active") {
|
|
59
|
+
return;
|
|
60
|
+
} else {
|
|
61
|
+
return ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access ` + access + ` after the handler is resolved. Make sure you didn't miss an await in the handler.`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw exn._0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scheduleAndSuspend(params, promise) {
|
|
68
|
+
getPending(params.sync).push(promise);
|
|
69
|
+
params.sync.status = {
|
|
70
|
+
TAG: "Aborted",
|
|
71
|
+
_0: {
|
|
72
|
+
RE_EXN_ID: Suspend
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
throw {
|
|
76
|
+
RE_EXN_ID: Suspend,
|
|
77
|
+
Error: new Error()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
17
80
|
|
|
18
81
|
let paramsByThis = new WeakMap();
|
|
19
82
|
|
|
@@ -47,28 +110,69 @@ var EffectContext = function(params, chainId, effectName, defaultShouldCache, ca
|
|
|
47
110
|
EffectContext.prototype = effectContextPrototype;
|
|
48
111
|
;
|
|
49
112
|
|
|
50
|
-
function
|
|
51
|
-
let handlerChainId = Internal.getItemChainId(params.item);
|
|
113
|
+
function prepareEffectCall(params, effect, input, caller) {
|
|
52
114
|
let isCrossChain = effect => Stdlib_Option.getOr(effect.crossChain, params.config.defaultCrossChain);
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
115
|
+
let scope = isCrossChain(effect) ? "crossChain" : Internal.getItemChainId(params.item);
|
|
116
|
+
if (caller !== undefined && isCrossChain(caller) && !isCrossChain(effect)) {
|
|
117
|
+
Stdlib_JsError.throwWithMessage(`The cross-chain effect "` + caller.name + `" cannot call the chain-scoped effect "` + effect.name + `", because a cross-chain effect isn't tied to a single chain. Make "` + effect.name + `" cross-chain (\`crossChain: true\`), or make "` + caller.name + `" chain-scoped (\`crossChain: false\`).`);
|
|
118
|
+
}
|
|
119
|
+
let tmp;
|
|
120
|
+
tmp = scope === "crossChain" ? undefined : Primitive_option.some(scope);
|
|
121
|
+
let effectContext = new EffectContext(params, tmp, effect.name, effect.defaultShouldCache, (nested, nestedInput) => callEffectAsync(params, nested, nestedInput, effect));
|
|
122
|
+
let effectArgs_cacheKey = Utils.Hash.makeOrThrow(S$RescriptSchema.reverseConvertOrThrow(input, effect.input));
|
|
123
|
+
let effectArgs_checkpointId = params.checkpointId;
|
|
124
|
+
let effectArgs = {
|
|
125
|
+
input: input,
|
|
126
|
+
context: effectContext,
|
|
127
|
+
cacheKey: effectArgs_cacheKey,
|
|
128
|
+
checkpointId: effectArgs_checkpointId
|
|
129
|
+
};
|
|
130
|
+
return [
|
|
131
|
+
scope,
|
|
132
|
+
effectArgs
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function callEffectAsync(params, effect, input, caller) {
|
|
137
|
+
let match = prepareEffectCall(params, effect, input, caller);
|
|
138
|
+
return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, match[1], match[0], params.indexerState, params.isPreload, params.item, params.config.ecosystem);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function initEffect(params) {
|
|
142
|
+
return (effect, input) => {
|
|
143
|
+
checkStatusOrThrow(params, "context.effect");
|
|
144
|
+
return callEffectAsync(params, effect, input, undefined);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function effectMemoKey(effect, scope, cacheKey) {
|
|
149
|
+
if (scope === "crossChain") {
|
|
150
|
+
return effect.name + `.` + cacheKey;
|
|
151
|
+
} else {
|
|
152
|
+
return effect.name + `.` + ChainId.toString(scope) + `.` + cacheKey;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function initEffectSync(params) {
|
|
157
|
+
return (effect, input) => {
|
|
158
|
+
checkStatusOrThrow(params, "context.effectSync");
|
|
159
|
+
let match = prepareEffectCall(params, effect, input, undefined);
|
|
160
|
+
let effectArgs = match[1];
|
|
161
|
+
let scope = match[0];
|
|
162
|
+
let memo = getMemo(params.sync);
|
|
163
|
+
let memoKey = effectMemoKey(effect, scope, effectArgs.cacheKey);
|
|
164
|
+
let output = memo[memoKey];
|
|
165
|
+
if (output !== undefined) {
|
|
166
|
+
return Primitive_option.valFromOption(output);
|
|
57
167
|
}
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
cacheKey: effectArgs_cacheKey,
|
|
67
|
-
checkpointId: effectArgs_checkpointId
|
|
68
|
-
};
|
|
69
|
-
return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
|
|
70
|
-
});
|
|
71
|
-
return makeCaller(undefined);
|
|
168
|
+
let inMemTable = InMemoryStore.getEffectInMemTable(params.indexerState, effect, scope);
|
|
169
|
+
if (!InMemoryStore.hasEffectOutput(inMemTable, effectArgs.cacheKey)) {
|
|
170
|
+
return scheduleAndSuspend(params, LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem));
|
|
171
|
+
}
|
|
172
|
+
let output$1 = InMemoryStore.getEffectOutputUnsafe(inMemTable, effectArgs.cacheKey);
|
|
173
|
+
memo[memoKey] = output$1;
|
|
174
|
+
return output$1;
|
|
175
|
+
};
|
|
72
176
|
}
|
|
73
177
|
|
|
74
178
|
function entityScope(params) {
|
|
@@ -97,24 +201,83 @@ function throwClickHouseReadOnly(entityConfig, op) {
|
|
|
97
201
|
return Stdlib_JsError.throwWithMessage(`context.` + entityConfig.name + `.` + op + `() is unavailable: ClickHouse storage is currently write-only. Follow Envio releases to be notified when ClickHouse supports both reads and writes from handlers.`);
|
|
98
202
|
}
|
|
99
203
|
|
|
204
|
+
function getSyncHandler(params, entityId) {
|
|
205
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params));
|
|
206
|
+
if (entityId in inMemTable.latestEntityChangeById) {
|
|
207
|
+
return InMemoryTable.Entity.getUnsafe(inMemTable)(entityId);
|
|
208
|
+
} else {
|
|
209
|
+
return scheduleAndSuspend(params, LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getWhereSyncHandler(params, filter) {
|
|
214
|
+
let entityConfig = params.entityConfig;
|
|
215
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, entityConfig, entityScope(params));
|
|
216
|
+
let hasIndex = InMemoryTable.Entity.hasIndex(inMemTable);
|
|
217
|
+
let getOnIndex = InMemoryTable.Entity.getUnsafeOnIndex(inMemTable);
|
|
218
|
+
let filters = EntityFilter.parseGetWhereOrThrow(filter, entityConfig.name, entityConfig.table);
|
|
219
|
+
let missing = [];
|
|
220
|
+
let entities = [];
|
|
221
|
+
filters.forEach(filter => {
|
|
222
|
+
let filterKey = EntityFilter.toString(filter);
|
|
223
|
+
if (hasIndex(filterKey)) {
|
|
224
|
+
entities.push(...getOnIndex(filterKey));
|
|
225
|
+
} else {
|
|
226
|
+
missing.push(LoadLayer.loadByFilter(params.loadManager, params.persistence, entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, filter));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
if (Utils.$$Array.notEmpty(missing)) {
|
|
230
|
+
let pending = getPending(params.sync);
|
|
231
|
+
missing.forEach(promise => {
|
|
232
|
+
pending.push(promise);
|
|
233
|
+
});
|
|
234
|
+
params.sync.status = {
|
|
235
|
+
TAG: "Aborted",
|
|
236
|
+
_0: {
|
|
237
|
+
RE_EXN_ID: Suspend
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
throw {
|
|
241
|
+
RE_EXN_ID: Suspend,
|
|
242
|
+
Error: new Error()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return entities;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getInBlockSyncHandler(params, entityId) {
|
|
249
|
+
let inMemTable = InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params));
|
|
250
|
+
let change = inMemTable.latestEntityChangeById[entityId];
|
|
251
|
+
if (change !== undefined && change.checkpointId === params.checkpointId) {
|
|
252
|
+
return InMemoryTable.Entity.mapChangeToEntity(change);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
100
256
|
let entityTraps_get = (params, prop) => {
|
|
257
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.` + prop);
|
|
101
258
|
let isClickHouseOnly = !params.entityConfig.storage.postgres;
|
|
102
|
-
let set = params.isPreload ? noopSet : entity =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
259
|
+
let set = params.isPreload ? noopSet : entity => {
|
|
260
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.set`);
|
|
261
|
+
InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
262
|
+
type: "SET",
|
|
263
|
+
entityId: entity.id,
|
|
264
|
+
entity: entity,
|
|
265
|
+
checkpointId: params.checkpointId
|
|
266
|
+
});
|
|
267
|
+
};
|
|
108
268
|
switch (prop) {
|
|
109
269
|
case "deleteUnsafe" :
|
|
110
270
|
if (params.isPreload) {
|
|
111
271
|
return noopDeleteUnsafe;
|
|
112
272
|
} else {
|
|
113
|
-
return entityId =>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
273
|
+
return entityId => {
|
|
274
|
+
checkStatusOrThrow(params, `context.` + params.entityConfig.name + `.deleteUnsafe`);
|
|
275
|
+
InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
|
|
276
|
+
type: "DELETE",
|
|
277
|
+
entityId: entityId,
|
|
278
|
+
checkpointId: params.checkpointId
|
|
279
|
+
});
|
|
280
|
+
};
|
|
118
281
|
}
|
|
119
282
|
case "get" :
|
|
120
283
|
if (isClickHouseOnly) {
|
|
@@ -122,6 +285,8 @@ let entityTraps_get = (params, prop) => {
|
|
|
122
285
|
} else {
|
|
123
286
|
return entityId => LoadLayer.loadById(params.loadManager, params.persistence, params.entityConfig, entityScope(params), params.indexerState, params.isPreload, params.item, params.config.ecosystem, entityId);
|
|
124
287
|
}
|
|
288
|
+
case "getInBlockSync" :
|
|
289
|
+
return entityId => getInBlockSyncHandler(params, entityId);
|
|
125
290
|
case "getOrCreate" :
|
|
126
291
|
if (isClickHouseOnly) {
|
|
127
292
|
return _entity => throwClickHouseReadOnly(params.entityConfig, "getOrCreate");
|
|
@@ -147,12 +312,16 @@ let entityTraps_get = (params, prop) => {
|
|
|
147
312
|
}
|
|
148
313
|
});
|
|
149
314
|
}
|
|
315
|
+
case "getSync" :
|
|
316
|
+
return entityId => getSyncHandler(params, entityId);
|
|
150
317
|
case "getWhere" :
|
|
151
318
|
if (isClickHouseOnly) {
|
|
152
319
|
return _filter => throwClickHouseReadOnly(params.entityConfig, "getWhere");
|
|
153
320
|
} else {
|
|
154
321
|
return filter => getWhereHandler(params, filter);
|
|
155
322
|
}
|
|
323
|
+
case "getWhereSync" :
|
|
324
|
+
return filter => getWhereSyncHandler(params, filter);
|
|
156
325
|
case "set" :
|
|
157
326
|
return set;
|
|
158
327
|
default:
|
|
@@ -164,16 +333,63 @@ let entityTraps = {
|
|
|
164
333
|
get: entityTraps_get
|
|
165
334
|
};
|
|
166
335
|
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
336
|
+
async function runSyncRound(params, fn, round) {
|
|
337
|
+
if (round > 10000) {
|
|
338
|
+
Stdlib_JsError.throwWithMessage(`The handler suspended on a synchronous read too many times: gave up after ` + (10000).toString() + ` rounds. This usually means the code isn't deterministic across reruns.`);
|
|
339
|
+
}
|
|
340
|
+
params.sync.status = "Active";
|
|
341
|
+
params.sync.pending = undefined;
|
|
342
|
+
let suspended;
|
|
343
|
+
try {
|
|
344
|
+
fn();
|
|
345
|
+
suspended = false;
|
|
346
|
+
} catch (raw_exn) {
|
|
347
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
348
|
+
if (exn.RE_EXN_ID === Suspend) {
|
|
349
|
+
suspended = true;
|
|
350
|
+
} else {
|
|
351
|
+
let pending = params.sync.pending;
|
|
352
|
+
if (pending !== undefined) {
|
|
353
|
+
params.sync.pending = undefined;
|
|
354
|
+
pending.forEach(promise => {
|
|
355
|
+
Utils.$$Promise.silentCatch(promise);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
throw exn;
|
|
359
|
+
}
|
|
170
360
|
}
|
|
361
|
+
let pending$1 = params.sync.pending;
|
|
362
|
+
if (pending$1 === undefined) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
params.sync.pending = undefined;
|
|
366
|
+
if (suspended) {
|
|
367
|
+
let errors = [];
|
|
368
|
+
await Promise.all(pending$1.map(promise => Stdlib_Promise.$$catch(promise, exn => {
|
|
369
|
+
errors.push(exn);
|
|
370
|
+
return Promise.resolve();
|
|
371
|
+
})));
|
|
372
|
+
let exn$1 = errors[0];
|
|
373
|
+
if (exn$1 !== undefined) {
|
|
374
|
+
throw exn$1;
|
|
375
|
+
}
|
|
376
|
+
return await runSyncRound(params, fn, round + 1 | 0);
|
|
377
|
+
}
|
|
378
|
+
pending$1.forEach(promise => {
|
|
379
|
+
Utils.$$Promise.silentCatch(promise);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let handlerTraps_get = (params, prop) => {
|
|
384
|
+
checkStatusOrThrow(params, `context.` + prop);
|
|
171
385
|
switch (prop) {
|
|
172
386
|
case "chain" :
|
|
173
387
|
let chainId = Internal.getItemChainId(params.item);
|
|
174
388
|
return params.chains[chainId];
|
|
175
389
|
case "effect" :
|
|
176
390
|
return initEffect(params);
|
|
391
|
+
case "effectSync" :
|
|
392
|
+
return initEffectSync(params);
|
|
177
393
|
case "isPreload" :
|
|
178
394
|
return params.isPreload;
|
|
179
395
|
case "log" :
|
|
@@ -182,6 +398,8 @@ let handlerTraps_get = (params, prop) => {
|
|
|
182
398
|
} else {
|
|
183
399
|
return Ecosystem.getItemUserLogger(params.item, params.config.ecosystem);
|
|
184
400
|
}
|
|
401
|
+
case "runSync" :
|
|
402
|
+
return fn => runSyncRound(params, fn, 1);
|
|
185
403
|
default:
|
|
186
404
|
let entityConfig = params.config.userEntitiesByName[prop];
|
|
187
405
|
if (entityConfig !== undefined) {
|
|
@@ -194,7 +412,7 @@ let handlerTraps_get = (params, prop) => {
|
|
|
194
412
|
isPreload: params.isPreload,
|
|
195
413
|
chains: params.chains,
|
|
196
414
|
config: params.config,
|
|
197
|
-
|
|
415
|
+
sync: params.sync,
|
|
198
416
|
entityConfig: entityConfig
|
|
199
417
|
}, entityTraps);
|
|
200
418
|
} else {
|
|
@@ -211,16 +429,34 @@ function getHandlerContext(params) {
|
|
|
211
429
|
return new Proxy(params, handlerTraps);
|
|
212
430
|
}
|
|
213
431
|
|
|
432
|
+
let maxSyncRounds = 10000;
|
|
433
|
+
|
|
214
434
|
export {
|
|
435
|
+
Suspend,
|
|
436
|
+
isSuspend,
|
|
437
|
+
makeSyncState,
|
|
438
|
+
getPending,
|
|
439
|
+
getMemo,
|
|
440
|
+
checkStatusOrThrow,
|
|
441
|
+
scheduleAndSuspend,
|
|
215
442
|
paramsByThis,
|
|
216
443
|
effectContextPrototype,
|
|
444
|
+
prepareEffectCall,
|
|
445
|
+
callEffectAsync,
|
|
217
446
|
initEffect,
|
|
447
|
+
effectMemoKey,
|
|
448
|
+
initEffectSync,
|
|
218
449
|
entityScope,
|
|
219
450
|
getWhereHandler,
|
|
220
451
|
noopSet,
|
|
221
452
|
noopDeleteUnsafe,
|
|
222
453
|
throwClickHouseReadOnly,
|
|
454
|
+
getSyncHandler,
|
|
455
|
+
getWhereSyncHandler,
|
|
456
|
+
getInBlockSyncHandler,
|
|
223
457
|
entityTraps,
|
|
458
|
+
maxSyncRounds,
|
|
459
|
+
runSyncRound,
|
|
224
460
|
handlerTraps,
|
|
225
461
|
getHandlerContext,
|
|
226
462
|
}
|
package/src/Utils.res
CHANGED
|
@@ -18,6 +18,33 @@ external importPathWithJson: (
|
|
|
18
18
|
"default": JSON.t,
|
|
19
19
|
}> = "import"
|
|
20
20
|
|
|
21
|
+
// Half the delay fixed, half spread across it. Every indexer pointed at one
|
|
22
|
+
// provider loses its stream, or gives up on a quiet chain, in the same instant
|
|
23
|
+
// that provider blinks, and putting them all back on the same schedule is how a
|
|
24
|
+
// blip becomes a stampede.
|
|
25
|
+
let jitter = delay => delay / 2 + (Math.random() *. (delay / 2)->Int.toFloat)->Float.toInt
|
|
26
|
+
|
|
27
|
+
// Clearing a pending timer and forgetting it are one action. A ref left holding
|
|
28
|
+
// a spent id reads as a timer still pending, and the next clear looks like it
|
|
29
|
+
// did something.
|
|
30
|
+
let clearTimeoutRef = timeoutId => {
|
|
31
|
+
switch timeoutId.contents {
|
|
32
|
+
| Some(id) => clearTimeout(id)
|
|
33
|
+
| None => ()
|
|
34
|
+
}
|
|
35
|
+
timeoutId := None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Keeps the doubling from overflowing on something that has been failing for
|
|
39
|
+
// days. Every caller's delay reaches its cap long before this.
|
|
40
|
+
let maxBackoffExponent = 20
|
|
41
|
+
|
|
42
|
+
let expBackoff = (~base, ~exp, ~maxMillis) =>
|
|
43
|
+
Pervasives.min(
|
|
44
|
+
base * Math.pow(2.0, ~exp=Pervasives.min(exp, maxBackoffExponent)->Int.toFloat)->Float.toInt,
|
|
45
|
+
maxMillis,
|
|
46
|
+
)
|
|
47
|
+
|
|
21
48
|
let delay = milliseconds =>
|
|
22
49
|
Promise.make((resolve, _) => {
|
|
23
50
|
let _interval = setTimeout(_ => {
|
|
@@ -97,6 +124,15 @@ module Dict = {
|
|
|
97
124
|
@get_index
|
|
98
125
|
external dangerouslyGetNonOption: (dict<'a>, string) => option<'a> = ""
|
|
99
126
|
|
|
127
|
+
let incrementBy = (dict, key, count) =>
|
|
128
|
+
dict->Dict.set(
|
|
129
|
+
key,
|
|
130
|
+
switch dict->dangerouslyGetNonOption(key) {
|
|
131
|
+
| Some(current) => current + count
|
|
132
|
+
| None => count
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
100
136
|
let getOrInsertEmptyDict = (dict, key) => {
|
|
101
137
|
switch dict->dangerouslyGetNonOption(key) {
|
|
102
138
|
| Some(d) => d
|
|
@@ -253,6 +289,10 @@ module Array = {
|
|
|
253
289
|
|
|
254
290
|
let immutableEmpty: array<unknown> = []
|
|
255
291
|
|
|
292
|
+
/** Render names for an error message: `"a", "b"`. */
|
|
293
|
+
let quotedJoin = (names: array<string>) =>
|
|
294
|
+
names->Array.map(name => `"${name}"`)->Array.joinUnsafe(", ")
|
|
295
|
+
|
|
256
296
|
@send
|
|
257
297
|
external forEachAsync: (array<'a>, 'a => promise<unit>) => unit = "forEach"
|
|
258
298
|
|
|
@@ -491,9 +531,120 @@ let unwrapResultExn = res =>
|
|
|
491
531
|
|
|
492
532
|
external queueMicrotask: (unit => unit) => unit = "queueMicrotask"
|
|
493
533
|
|
|
534
|
+
module Bytes = {
|
|
535
|
+
let asUint8Array: unknown => option<Uint8Array.t> = %raw(`(value) =>
|
|
536
|
+
value instanceof Uint8Array
|
|
537
|
+
? value.constructor === Uint8Array
|
|
538
|
+
? value
|
|
539
|
+
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
540
|
+
: undefined`)
|
|
541
|
+
|
|
542
|
+
let toHex = (bytes: Uint8Array.t) =>
|
|
543
|
+
NodeJs.Buffer.fromArrayBuffer(
|
|
544
|
+
bytes->TypedArray.buffer,
|
|
545
|
+
~byteOffset=bytes->TypedArray.byteOffset,
|
|
546
|
+
~length=bytes->TypedArray.byteLength,
|
|
547
|
+
)->NodeJs.Buffer.toHex
|
|
548
|
+
|
|
549
|
+
@get_index external byteAt: (Uint8Array.t, int) => int = ""
|
|
550
|
+
|
|
551
|
+
// A bytea[] parameter as the array literal Postgres parses itself. postgres.js
|
|
552
|
+
// serializes an array parameter with the element serializer of the type the
|
|
553
|
+
// server describes, and the bytea one expects a Uint8Array — yet it types the
|
|
554
|
+
// array after its first element, so an array of Uint8Arrays binds as a single
|
|
555
|
+
// bytea. Text binds untyped and reaches the server's array parser as it is.
|
|
556
|
+
//
|
|
557
|
+
// Nests, since an `in` filter over a list column carries one array per
|
|
558
|
+
// candidate value. A sub-array is written unquoted, which is how Postgres
|
|
559
|
+
// spells a dimension rather than an element.
|
|
560
|
+
let rec toPgArrayLiteral = (values: array<unknown>) =>
|
|
561
|
+
"{" ++
|
|
562
|
+
values
|
|
563
|
+
->Array.map(value =>
|
|
564
|
+
switch value->(magic: unknown => Nullable.t<unknown>)->Nullable.toOption {
|
|
565
|
+
| None => "NULL"
|
|
566
|
+
| Some(value) =>
|
|
567
|
+
switch value->asUint8Array {
|
|
568
|
+
| Some(bytes) => `"\\\\x${bytes->toHex}"`
|
|
569
|
+
| None =>
|
|
570
|
+
if value->Array.isArray {
|
|
571
|
+
value->(magic: unknown => array<unknown>)->toPgArrayLiteral
|
|
572
|
+
} else {
|
|
573
|
+
JsError.throwWithMessage("Expected Uint8Array")
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
->Array.join(",") ++ "}"
|
|
579
|
+
|
|
580
|
+
// Bytewise, the order Postgres sorts a bytea in.
|
|
581
|
+
let compare = (a: Uint8Array.t, b: Uint8Array.t) => {
|
|
582
|
+
let aLength = a->TypedArray.length
|
|
583
|
+
let bLength = b->TypedArray.length
|
|
584
|
+
let rec loop = index =>
|
|
585
|
+
if index === aLength || index === bLength {
|
|
586
|
+
Int.compare(aLength, bLength)
|
|
587
|
+
} else {
|
|
588
|
+
switch Int.compare(a->byteAt(index), b->byteAt(index)) {
|
|
589
|
+
| 0. => loop(index + 1)
|
|
590
|
+
| order => order
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
loop(0)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
494
597
|
module Schema = {
|
|
495
598
|
let variantTag = S.union([S.string, S.object(s => s.field("TAG", S.string))])
|
|
496
599
|
|
|
600
|
+
// A ReScript `option` is `undefined` at runtime, so `S.null`'s serializer
|
|
601
|
+
// treats only `undefined` as the empty case — a genuine `null`, which a JS
|
|
602
|
+
// handler writes for a field it doesn't set, reaches the value serializer
|
|
603
|
+
// and crashes it (`null.toString()` for a BigInt column). Collapse both to
|
|
604
|
+
// `None` before the value serializer sees them.
|
|
605
|
+
//
|
|
606
|
+
// Revisit on the Sury v11 migration: if its nullable handles a `null` value
|
|
607
|
+
// on the serialize side, this wrapper goes away.
|
|
608
|
+
let nullTolerant = schema =>
|
|
609
|
+
S.null(schema)->S.transform(_ => {
|
|
610
|
+
parser: value => value,
|
|
611
|
+
serializer: value => value->(magic: option<'a> => Nullable.t<'a>)->Nullable.toOption,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
// Postgres hands a bytea back as a Buffer, while handlers are promised the
|
|
615
|
+
// plain Uint8Array of the entity type — Buffer's `slice` and `toString` behave
|
|
616
|
+
// differently.
|
|
617
|
+
let bytes = S.custom("Bytes", s => {
|
|
618
|
+
parser: unknown =>
|
|
619
|
+
switch unknown->Bytes.asUint8Array {
|
|
620
|
+
| Some(bytes) => bytes
|
|
621
|
+
| None => s.fail("Expected Uint8Array")
|
|
622
|
+
},
|
|
623
|
+
serializer: (bytes: Uint8Array.t) => bytes,
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
// A bytea[] value: a list column, or the values an `in` filter compares
|
|
627
|
+
// against — one array per candidate when the column is itself a list, which
|
|
628
|
+
// the literal writer nests. Only a column is ever read back, so the parser
|
|
629
|
+
// takes the one level a bytea[] column returns.
|
|
630
|
+
let bytesArray = S.custom("BytesArray", s => {
|
|
631
|
+
parser: unknown =>
|
|
632
|
+
if unknown->Array.isArray {
|
|
633
|
+
unknown
|
|
634
|
+
->(magic: unknown => array<unknown>)
|
|
635
|
+
->Array.map(item =>
|
|
636
|
+
switch item->Bytes.asUint8Array {
|
|
637
|
+
| Some(bytes) => bytes
|
|
638
|
+
| None => s.fail("Expected Uint8Array")
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
} else {
|
|
642
|
+
s.fail("Expected array of Uint8Array")
|
|
643
|
+
},
|
|
644
|
+
serializer: (values: array<Uint8Array.t>) =>
|
|
645
|
+
values->(magic: array<Uint8Array.t> => array<unknown>)->Bytes.toPgArrayLiteral,
|
|
646
|
+
})
|
|
647
|
+
|
|
497
648
|
// Don't use S.unknown, since it's not serializable to json
|
|
498
649
|
// In a nutshell, this is completely unsafe.
|
|
499
650
|
let dbDate =
|
|
@@ -501,6 +652,19 @@ module Schema = {
|
|
|
501
652
|
->(magic: S.t<JSON.t> => S.t<Date.t>)
|
|
502
653
|
->S.preprocess(_ => {serializer: date => date->magic->Date.toISOString})
|
|
503
654
|
|
|
655
|
+
// JSON `null` is a document, and Postgres stores it as one. Reaching the
|
|
656
|
+
// ClickHouse sink as a JS `null` it would instead read as a field the handler
|
|
657
|
+
// never set, which a String column has no way to hold — so it travels as the
|
|
658
|
+
// text it would have been serialized to. Every other document is left for the
|
|
659
|
+
// sink to serialize.
|
|
660
|
+
let clickHouseJson = S.json(~validate=false)->S.preprocess(_ => {
|
|
661
|
+
serializer: value =>
|
|
662
|
+
switch value->(magic: unknown => Nullable.t<unknown>)->Nullable.toOption {
|
|
663
|
+
| None => "null"->magic
|
|
664
|
+
| Some(json) => json
|
|
665
|
+
},
|
|
666
|
+
})
|
|
667
|
+
|
|
504
668
|
// ClickHouse expects timestamps as numbers (milliseconds), not ISO strings
|
|
505
669
|
let clickHouseDate =
|
|
506
670
|
S.json(~validate=false)
|
|
@@ -761,6 +925,12 @@ let prettifyExn = exn => {
|
|
|
761
925
|
}
|
|
762
926
|
}
|
|
763
927
|
|
|
928
|
+
let exnMessage = exn =>
|
|
929
|
+
switch exn->JsExn.anyToExnInternal {
|
|
930
|
+
| JsExn(jsExn) => jsExn->JsExn.message
|
|
931
|
+
| _ => None
|
|
932
|
+
}
|
|
933
|
+
|
|
764
934
|
module EnvioPackage = {
|
|
765
935
|
type t = {version: string}
|
|
766
936
|
|