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.
Files changed (143) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +41 -8
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +61 -44
  10. package/src/Config.res.mjs +56 -19
  11. package/src/Core.res +19 -0
  12. package/src/Core.res.mjs +11 -0
  13. package/src/CrossChainState.res +15 -23
  14. package/src/CrossChainState.res.mjs +10 -18
  15. package/src/CrossChainState.resi +4 -1
  16. package/src/Ecosystem.res +7 -2
  17. package/src/Envio.res +6 -4
  18. package/src/EventConfigBuilder.res +169 -26
  19. package/src/EventConfigBuilder.res.mjs +124 -15
  20. package/src/EventProcessing.res +6 -6
  21. package/src/EventProcessing.res.mjs +8 -6
  22. package/src/HandlerLoader.res +20 -8
  23. package/src/HandlerLoader.res.mjs +11 -2
  24. package/src/HandlerRegister.res +21 -4
  25. package/src/HandlerRegister.res.mjs +24 -5
  26. package/src/InMemoryStore.res +9 -7
  27. package/src/InMemoryStore.res.mjs +3 -3
  28. package/src/IndexerState.res +43 -6
  29. package/src/IndexerState.res.mjs +36 -7
  30. package/src/IndexerState.resi +4 -4
  31. package/src/Internal.res +16 -17
  32. package/src/Metrics.res +72 -6
  33. package/src/Metrics.res.mjs +30 -2
  34. package/src/Persistence.res +36 -30
  35. package/src/Persistence.res.mjs +1 -20
  36. package/src/PgStorage.res +104 -56
  37. package/src/PgStorage.res.mjs +57 -35
  38. package/src/PruneStaleHistory.res +85 -52
  39. package/src/PruneStaleHistory.res.mjs +63 -37
  40. package/src/Rollback.res +68 -45
  41. package/src/Rollback.res.mjs +38 -19
  42. package/src/RollbackCommit.res +7 -9
  43. package/src/RollbackCommit.res.mjs +5 -6
  44. package/src/SafeCheckpointTracking.res +11 -11
  45. package/src/SafeCheckpointTracking.res.mjs +7 -6
  46. package/src/SimulateItems.res +33 -18
  47. package/src/SimulateItems.res.mjs +26 -22
  48. package/src/Sink.res +38 -26
  49. package/src/Sink.res.mjs +21 -16
  50. package/src/TestIndexer.res +5 -5
  51. package/src/TestIndexer.res.mjs +1 -1
  52. package/src/UserContext.res +358 -51
  53. package/src/UserContext.res.mjs +271 -35
  54. package/src/Utils.res +170 -0
  55. package/src/Utils.res.mjs +153 -0
  56. package/src/Writing.res +2 -2
  57. package/src/Writing.res.mjs +1 -1
  58. package/src/bindings/ClickHouse.res +366 -698
  59. package/src/bindings/ClickHouse.res.mjs +371 -421
  60. package/src/bindings/ClickHouseSink.res +337 -0
  61. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  62. package/src/bindings/EventSource.res +8 -2
  63. package/src/bindings/NodeJs.res +6 -0
  64. package/src/bindings/Vitest.res +23 -0
  65. package/src/bindings/Vitest.res.mjs +3 -0
  66. package/src/bindings/WebSocket.res +9 -10
  67. package/src/db/CheckpointBounds.res +53 -0
  68. package/src/db/CheckpointBounds.res.mjs +44 -0
  69. package/src/db/EntityFilter.res +36 -17
  70. package/src/db/EntityFilter.res.mjs +25 -14
  71. package/src/db/EntityHistory.res +47 -22
  72. package/src/db/EntityHistory.res.mjs +19 -12
  73. package/src/db/InternalTable.res +84 -35
  74. package/src/db/InternalTable.res.mjs +56 -23
  75. package/src/db/RollbackFloors.res +55 -0
  76. package/src/db/RollbackFloors.res.mjs +92 -0
  77. package/src/db/Table.res +22 -4
  78. package/src/db/Table.res.mjs +30 -9
  79. package/src/sources/Evm.res +2 -0
  80. package/src/sources/Evm.res.mjs +2 -0
  81. package/src/sources/EvmHyperSyncSource.res +3 -3
  82. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  83. package/src/sources/EvmRpcWs.res +124 -0
  84. package/src/sources/EvmRpcWs.res.mjs +117 -0
  85. package/src/sources/Fuel.res +2 -0
  86. package/src/sources/Fuel.res.mjs +2 -0
  87. package/src/sources/FuelHyperSyncSource.res +1 -1
  88. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  89. package/src/sources/HeightFeed.res +568 -0
  90. package/src/sources/HeightFeed.res.mjs +452 -0
  91. package/src/sources/HeightStream.res +257 -0
  92. package/src/sources/HeightStream.res.mjs +194 -0
  93. package/src/sources/HyperSync.res +5 -0
  94. package/src/sources/HyperSync.res.mjs +3 -0
  95. package/src/sources/HyperSync.resi +4 -0
  96. package/src/sources/HyperSyncSSE.res +49 -0
  97. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  98. package/src/sources/RpcSource.res +1 -1
  99. package/src/sources/RpcSource.res.mjs +2 -2
  100. package/src/sources/Source.res +54 -1
  101. package/src/sources/Source.res.mjs +25 -0
  102. package/src/sources/SourceManager.res +220 -227
  103. package/src/sources/SourceManager.res.mjs +139 -164
  104. package/src/sources/SourceManager.resi +8 -0
  105. package/src/sources/Svm.res +2 -0
  106. package/src/sources/Svm.res.mjs +2 -0
  107. package/src/sources/SvmHyperSyncClient.res +70 -127
  108. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  109. package/src/sources/SvmHyperSyncSource.res +18 -15
  110. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  111. package/src/subgraph/blocks.ts +176 -0
  112. package/src/subgraph/calls.ts +217 -0
  113. package/src/subgraph/conformance.ts +102 -0
  114. package/src/subgraph/division.ts +133 -0
  115. package/src/subgraph/errors.ts +90 -0
  116. package/src/subgraph/graph-ts-types/VERSION +2 -0
  117. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  118. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  119. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  120. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  121. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  122. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  123. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  124. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  125. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  126. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  127. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  128. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  129. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  130. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  131. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  132. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  133. package/src/subgraph/graph-ts.ts +1948 -0
  134. package/src/subgraph/hosts.ts +148 -0
  135. package/src/subgraph/runtime.ts +863 -0
  136. package/src/subgraph/scope.ts +66 -0
  137. package/svm.schema.json +6 -81
  138. package/src/MemoryStorage.res +0 -729
  139. package/src/MemoryStorage.res.mjs +0 -586
  140. package/src/sources/HyperSyncHeightStream.res +0 -129
  141. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  142. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  143. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -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 initEffect(params) {
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 makeCaller = caller => ((effect, input) => {
54
- let scope = isCrossChain(effect) ? "crossChain" : handlerChainId;
55
- if (caller !== undefined && isCrossChain(caller) && !isCrossChain(effect)) {
56
- 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\`).`);
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 tmp;
59
- tmp = scope === "crossChain" ? undefined : Primitive_option.some(scope);
60
- let effectContext = new EffectContext(params, tmp, effect.name, effect.defaultShouldCache, makeCaller(effect));
61
- let effectArgs_cacheKey = Utils.Hash.makeOrThrow(S$RescriptSchema.reverseConvertOrThrow(input, effect.input));
62
- let effectArgs_checkpointId = params.checkpointId;
63
- let effectArgs = {
64
- input: input,
65
- context: effectContext,
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 => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
103
- type: "SET",
104
- entityId: entity.id,
105
- entity: entity,
106
- checkpointId: params.checkpointId
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 => InMemoryTable.Entity.set(InMemoryStore.getInMemTable(params.indexerState, params.entityConfig, entityScope(params)), IndexerState.committedCheckpointId(params.indexerState), {
114
- type: "DELETE",
115
- entityId: entityId,
116
- checkpointId: params.checkpointId
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
- let handlerTraps_get = (params, prop) => {
168
- if (params.isResolved) {
169
- ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.` + prop + ` after the handler is resolved. Make sure you didn't miss an await in the handler.`));
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
- isResolved: params.isResolved,
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