envio 3.15.0 → 3.16.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 (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +10 -9
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +7 -0
  11. package/src/Bin.res.mjs +8 -0
  12. package/src/ChainFetching.res +450 -16
  13. package/src/ChainFetching.res.mjs +403 -32
  14. package/src/ChainState.res +124 -7
  15. package/src/ChainState.res.mjs +128 -67
  16. package/src/ChainState.resi +26 -0
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +31 -3
  20. package/src/Config.res.mjs +15 -4
  21. package/src/ContractRegisterContext.res +3 -6
  22. package/src/ContractRegisterContext.res.mjs +4 -2
  23. package/src/Core.res +95 -36
  24. package/src/Core.res.mjs +90 -1
  25. package/src/CrossChainState.res +12 -2
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +7 -4
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectCacheLookup.res +98 -0
  30. package/src/EffectCacheLookup.res.mjs +116 -0
  31. package/src/EffectState.res +10 -2
  32. package/src/EffectState.res.mjs +8 -4
  33. package/src/EffectState.resi +3 -1
  34. package/src/EntityId.res +2 -7
  35. package/src/EntityId.res.mjs +1 -8
  36. package/src/EntityTables.res +4 -1
  37. package/src/EntityTables.res.mjs +1 -1
  38. package/src/Env.res +20 -0
  39. package/src/Env.res.mjs +16 -0
  40. package/src/EventProcessing.res +38 -1
  41. package/src/EventProcessing.res.mjs +28 -1
  42. package/src/FetchState.res +408 -73
  43. package/src/FetchState.res.mjs +361 -91
  44. package/src/FinalizeBackfill.res +2 -4
  45. package/src/FinalizeBackfill.res.mjs +3 -4
  46. package/src/HandlerLoader.res +39 -15
  47. package/src/HandlerLoader.res.mjs +27 -8
  48. package/src/HandlerRegister.res +4 -0
  49. package/src/HandlerRegister.res.mjs +5 -2
  50. package/src/HandlerRegister.resi +2 -0
  51. package/src/InMemoryTable.res +17 -16
  52. package/src/InMemoryTable.res.mjs +14 -15
  53. package/src/IndexerState.res +28 -4
  54. package/src/IndexerState.res.mjs +19 -6
  55. package/src/IndexerState.resi +4 -0
  56. package/src/Internal.res +61 -49
  57. package/src/Internal.res.mjs +36 -80
  58. package/src/LoadLayer.res +54 -15
  59. package/src/LoadLayer.res.mjs +75 -45
  60. package/src/LoadLayer.resi +26 -0
  61. package/src/Main.res +14 -1
  62. package/src/Main.res.mjs +4 -3
  63. package/src/Persistence.res +3 -23
  64. package/src/Persistence.res.mjs +3 -3
  65. package/src/PreloadLookahead.res +202 -0
  66. package/src/PreloadLookahead.res.mjs +218 -0
  67. package/src/Rollback.res +0 -1
  68. package/src/Rollback.res.mjs +1 -1
  69. package/src/RunScope.res +3 -0
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +1 -1
  72. package/src/SimulateItems.res +42 -26
  73. package/src/SimulateItems.res.mjs +40 -16
  74. package/src/StagedRows.res +106 -0
  75. package/src/StagedRows.res.mjs +102 -0
  76. package/src/Staging.res +32 -14
  77. package/src/Staging.res.mjs +17 -20
  78. package/src/Storage.res +303 -948
  79. package/src/Storage.res.mjs +229 -708
  80. package/src/SubgraphLog.res +19 -0
  81. package/src/SubgraphLog.res.mjs +26 -0
  82. package/src/Supervisor.res +75 -56
  83. package/src/Supervisor.res.mjs +55 -26
  84. package/src/SyncWrites.res +107 -0
  85. package/src/SyncWrites.res.mjs +98 -0
  86. package/src/TestIndexer.res +22 -27
  87. package/src/TestIndexer.res.mjs +20 -17
  88. package/src/Throttler.res +15 -8
  89. package/src/Throttler.res.mjs +9 -6
  90. package/src/Throttler.resi +1 -1
  91. package/src/UserContext.res +419 -46
  92. package/src/UserContext.res.mjs +318 -30
  93. package/src/Utils.res +17 -6
  94. package/src/Utils.res.mjs +12 -6
  95. package/src/Writing.res +0 -1
  96. package/src/Writing.res.mjs +1 -1
  97. package/src/bindings/ClickHouse.res +0 -322
  98. package/src/bindings/ClickHouse.res.mjs +0 -338
  99. package/src/bindings/StorageClient.res +108 -174
  100. package/src/bindings/StorageClient.res.mjs +19 -8
  101. package/src/bindings/Vitest.res +0 -26
  102. package/src/bindings/Vitest.res.mjs +0 -3
  103. package/src/db/Table.res +24 -150
  104. package/src/db/Table.res.mjs +38 -272
  105. package/src/sources/AddressSet.res +2 -0
  106. package/src/sources/AddressStore.res +3 -0
  107. package/src/sources/CallSource.res +274 -0
  108. package/src/sources/CallSource.res.mjs +196 -0
  109. package/src/sources/ChainSources.res +2 -0
  110. package/src/sources/ChainSources.res.mjs +2 -2
  111. package/src/sources/EvmChain.res +10 -1
  112. package/src/sources/EvmChain.res.mjs +6 -3
  113. package/src/sources/EvmHyperSyncSource.res +2 -2
  114. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  115. package/src/sources/EvmRpcClient.res +13 -3
  116. package/src/sources/EvmRpcClient.res.mjs +4 -3
  117. package/src/sources/EvmRpcWs.res +4 -3
  118. package/src/sources/EvmRpcWs.res.mjs +2 -2
  119. package/src/sources/HeightFeed.res +31 -10
  120. package/src/sources/HeightFeed.res.mjs +10 -6
  121. package/src/sources/HeightStream.res +32 -17
  122. package/src/sources/HeightStream.res.mjs +10 -10
  123. package/src/sources/HyperSync.res +2 -1
  124. package/src/sources/HyperSync.res.mjs +2 -1
  125. package/src/sources/HyperSyncSSE.res +4 -6
  126. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  127. package/src/sources/RpcSource.res +10 -1
  128. package/src/sources/RpcSource.res.mjs +4 -3
  129. package/src/sources/Source.res +3 -0
  130. package/src/sources/SourceManager.res +84 -77
  131. package/src/sources/SourceManager.res.mjs +17 -20
  132. package/src/sources/SourceManager.resi +3 -4
  133. package/src/sources/StartBlockResolver.res +1 -0
  134. package/src/sources/StartBlockResolver.res.mjs +3 -2
  135. package/src/sources/SvmHyperSyncSource.res +2 -2
  136. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  137. package/src/subgraph/abi-types.ts +84 -0
  138. package/src/subgraph/aggregations.ts +745 -0
  139. package/src/subgraph/as-package.ts +143 -0
  140. package/src/subgraph/assemblyscript-regex.ts +67 -0
  141. package/src/subgraph/assemblyscript.ts +731 -0
  142. package/src/subgraph/blocks.ts +398 -0
  143. package/src/subgraph/calls-source.ts +702 -0
  144. package/src/subgraph/calls.ts +258 -0
  145. package/src/subgraph/conformance.ts +106 -0
  146. package/src/subgraph/errors.ts +85 -0
  147. package/src/subgraph/graft.ts +73 -0
  148. package/src/subgraph/graph-cli-worker.mjs +35 -0
  149. package/src/subgraph/graph-cli.ts +900 -0
  150. package/src/subgraph/graph-node-decimal.ts +199 -0
  151. package/src/subgraph/graph-ts-enums.ts +28 -0
  152. package/src/subgraph/graph-ts-types/VERSION +2 -0
  153. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  154. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  155. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  156. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  157. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  158. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  159. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  160. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  161. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  162. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  163. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  164. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  165. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  166. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  167. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  168. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  169. package/src/subgraph/graph-ts.ts +4339 -0
  170. package/src/subgraph/hosts.ts +554 -0
  171. package/src/subgraph/log.ts +28 -0
  172. package/src/subgraph/receipts.ts +184 -0
  173. package/src/subgraph/register-slots.ts +93 -0
  174. package/src/subgraph/rollup.ts +109 -0
  175. package/src/subgraph/rpc.ts +337 -0
  176. package/src/subgraph/runtime.ts +2059 -0
  177. package/src/subgraph/scope.ts +176 -0
  178. package/src/subgraph/stats.ts +42 -0
  179. package/src/subgraph/template-instances.ts +66 -0
  180. package/src/subgraph/toolchain.ts +327 -0
  181. package/src/subgraph/transient.ts +176 -0
  182. package/svm.schema.json +11 -2
  183. package/src/PgWriting.res +0 -36
  184. package/src/PgWriting.res.mjs +0 -34
  185. package/src/db/IndexDefinition.res +0 -47
  186. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -1,3 +1,91 @@
1
+ // Thrown by a sync op that can't be served from memory. Owned by envio so the
2
+ // subgraph runtime's replay loop can tell it apart from a real handler error.
3
+ // User code never swallows it: AssemblyScript has no try/catch, and the ops
4
+ // keep re-throwing it while the round stays aborted.
5
+ exception Suspend
6
+
7
+ let isSuspend = exn =>
8
+ switch exn {
9
+ | Suspend => true
10
+ | _ => false
11
+ }
12
+
13
+ // Every suspend throws this one value: `throw(Suspend)` builds a fresh Error,
14
+ // stack trace and all, and a mapping suspends several times per event.
15
+ let suspend: exn = Suspend
16
+
17
+ type roundStatus =
18
+ | Active
19
+ // Set by a suspended sync op. Every context access re-throws the stored
20
+ // error, so even a caught suspend stops the round at its next one.
21
+ | Aborted(exn)
22
+
23
+ // One `context.runSync` call. Its body is replayed from the top each time it
24
+ // suspends on a read that wasn't in memory yet.
25
+ type syncRun = {
26
+ run: RunScope.handlerRun,
27
+ mutable status: roundStatus,
28
+ // Ops scheduled by this round's suspended reads, awaited before the replay.
29
+ mutable pending: array<promise<unit>>,
30
+ // Effect outputs read from the in-memory table during this call, reused by
31
+ // its later rounds.
32
+ memo: dict<Internal.effectOutput>,
33
+ // This round's writes, held until it completes.
34
+ mutable writes: SyncWrites.t,
35
+ // Preload reads this round answered as absent while their load was in flight.
36
+ mutable speculated: int,
37
+ }
38
+
39
+ // The round running right now. A round's body runs synchronously from start to
40
+ // end, so no other run's code can observe it.
41
+ let currentSync: ref<option<syncRun>> = ref(None)
42
+
43
+ let syncOf = (run: RunScope.handlerRun) =>
44
+ switch currentSync.contents {
45
+ | Some(sync) if sync.run === run => Some(sync)
46
+ | _ => None
47
+ }
48
+
49
+ // The handler run a `context.${prop}` access belongs to, refused while its
50
+ // round is aborted.
51
+ let currentRunOrThrow = (~prop) => {
52
+ let run = RunScope.currentHandlerRunOrThrow(~prop)
53
+ switch run->syncOf {
54
+ | Some({status: Aborted(exn)}) => throw(exn)
55
+ | _ => ()
56
+ }
57
+ run
58
+ }
59
+
60
+ let syncOrThrow = (run, ~prop) =>
61
+ switch run->syncOf {
62
+ | Some(sync) => sync
63
+ | None => JsError.throwWithMessage(`context.${prop} is only available inside context.runSync.`)
64
+ }
65
+
66
+ // Fires the async op behind a sync miss, records it for the replay and aborts
67
+ // the round.
68
+ let scheduleAndSuspend = (sync: syncRun, promise: promise<'a>): 'b => {
69
+ sync.pending->Array.push(promise->Utils.Promise.ignoreValue)
70
+ sync.status = Aborted(suspend)
71
+ throw(suspend)
72
+ }
73
+
74
+ // Preload reads past a miss: the read answers as absent and the round runs on,
75
+ // so the reads after it that don't depend on it join this round's batch rather
76
+ // than taking a round each. Nothing preload computes is kept, and the round is
77
+ // replayed once its reads land. Bounded, for a mapping that loops on a read.
78
+ let maxSpeculatedReads = 32
79
+
80
+ let missOrSuspend = (sync: syncRun, promise: promise<'a>, ~absent: 'b): 'b =>
81
+ if sync.run.isPreload && sync.speculated < maxSpeculatedReads {
82
+ sync.pending->Array.push(promise->Utils.Promise.ignoreValue)
83
+ sync.speculated = sync.speculated + 1
84
+ absent
85
+ } else {
86
+ sync->scheduleAndSuspend(promise)
87
+ }
88
+
1
89
  // What every handler run of an indexer shares. The run itself — its item,
2
90
  // checkpoint and whether it's a preload — is read from the RunScope it was
3
91
  // entered with, so one context object serves every run, concurrent ones and
@@ -58,7 +146,7 @@ let isCrossChain = (params: params, effect: Internal.effect) =>
58
146
 
59
147
  // A chain-scoped effect always resolves against the chain of the handler run
60
148
  // that triggered the call, even several effects deep.
61
- let rec callEffect = (
149
+ let rec prepareEffectCall = (
62
150
  params: params,
63
151
  ~run: RunScope.handlerRun,
64
152
  ~caller: option<Internal.effect>,
@@ -88,23 +176,73 @@ let rec callEffect = (
88
176
  ~callEffect=(child, input) => params->callEffect(~run, ~caller=Some(effect), child, input),
89
177
  )
90
178
  effectContext->RunScope.attachEffectRun({caller: run, effectName: effect.name})
179
+ let effectArgs: Internal.effectArgs = {
180
+ input,
181
+ context: effectContext,
182
+ cacheKey: input
183
+ ->S.reverseConvertOrThrow(effect.input)
184
+ ->Utils.Hash.makeOrThrow
185
+ ->Utils.Hash.boundCacheKey,
186
+ chainId: handlerChainId,
187
+ checkpointId: run.checkpointId,
188
+ }
189
+ (scope, effectArgs)
190
+ }
191
+
192
+ and callEffect = (params: params, ~run: RunScope.handlerRun, ~caller, effect, input) => {
193
+ let (scope, effectArgs) = params->prepareEffectCall(~run, ~caller, effect, input)
91
194
  LoadLayer.loadEffect(
92
195
  ~loadManager=params.loadManager,
93
196
  ~persistence=params.persistence,
94
197
  ~effect,
95
- ~effectArgs={
96
- input,
97
- context: effectContext,
98
- cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
99
- chainId: handlerChainId,
100
- checkpointId: run.checkpointId,
101
- },
198
+ ~effectArgs,
102
199
  ~scope,
103
200
  ~indexerState=params.indexerState,
104
201
  ~shouldGroup=run.isPreload,
105
202
  )
106
203
  }
107
204
 
205
+ let effectMemoKey = (~effect: Internal.effect, ~scope: Internal.chainScope, ~cacheKey) =>
206
+ switch scope {
207
+ | CrossChain => `${effect.name}.${cacheKey}`
208
+ | Chain(chainId) => `${effect.name}.${chainId->ChainId.toString}.${cacheKey}`
209
+ }
210
+
211
+ // An effect's output when it's in memory; otherwise the round suspends on it.
212
+ let effectSync = (params: params, effect: Internal.effect, input: Internal.effectInput) => {
213
+ let run = currentRunOrThrow(~prop="effectSync")
214
+ let sync = run->syncOrThrow(~prop="effectSync")
215
+ let (scope, effectArgs) = params->prepareEffectCall(~run, ~caller=None, effect, input)
216
+ let memoKey = effectMemoKey(~effect, ~scope, ~cacheKey=effectArgs.cacheKey)
217
+ switch sync.memo->Utils.Dict.dangerouslyGetNonOption(memoKey) {
218
+ | Some(output) => output
219
+ | None =>
220
+ let inMemTable = params.indexerState->InMemoryStore.getEffectInMemTable(~effect, ~scope)
221
+ if inMemTable->InMemoryStore.hasEffectOutput(effectArgs.cacheKey) {
222
+ let output = inMemTable->InMemoryStore.getEffectOutputUnsafe(effectArgs.cacheKey)
223
+ sync.memo->Dict.set(memoKey, output)
224
+ output
225
+ } else if sync.speculated > 0 {
226
+ // The input may be computed from a speculated read: an effect can cost
227
+ // an RPC call, so it waits to be asked with the real values.
228
+ sync.status = Aborted(suspend)
229
+ throw(suspend)
230
+ } else {
231
+ sync->scheduleAndSuspend(
232
+ LoadLayer.loadEffect(
233
+ ~loadManager=params.loadManager,
234
+ ~persistence=params.persistence,
235
+ ~effect,
236
+ ~effectArgs,
237
+ ~scope,
238
+ ~indexerState=params.indexerState,
239
+ ~shouldGroup=run.isPreload,
240
+ ),
241
+ )
242
+ }
243
+ }
244
+ }
245
+
108
246
  // A TTL drops an entity's rows whenever ClickHouse merges them, so for one kept
109
247
  // there alone a read would turn on merge timing rather than on the events.
110
248
  let expiresUnread = (entityConfig: Internal.entityConfig) =>
@@ -129,8 +267,15 @@ type entityContext = {
129
267
  deleteUnsafe: EntityId.t => unit,
130
268
  }
131
269
 
270
+ type syncEntityContext = {
271
+ getSync: string => option<Internal.entity>,
272
+ getWhereSync: dict<dict<unknown>> => array<Internal.entity>,
273
+ getInBlockSync: string => option<Internal.entity>,
274
+ }
275
+
132
276
  let makeEntityContext = (params: params, ~entityConfig: Internal.entityConfig) => {
133
277
  let prop = entityConfig.name
278
+ let entityName = entityConfig.name
134
279
  // The handler context is always chain-scoped, so a per-chain entity resolves
135
280
  // to the chain the handler runs on and a cross-chain one to the shared
136
281
  // partition.
@@ -138,7 +283,7 @@ let makeEntityContext = (params: params, ~entityConfig: Internal.entityConfig) =
138
283
  entityConfig->InMemoryStore.entityScope(~chainId=run.item->Internal.getItemChainId)
139
284
 
140
285
  let loadById = entityId => {
141
- let run = RunScope.currentHandlerRunOrThrow(~prop)
286
+ let run = currentRunOrThrow(~prop)
142
287
  LoadLayer.loadById(
143
288
  ~loadManager=params.loadManager,
144
289
  ~persistence=params.persistence,
@@ -150,35 +295,34 @@ let makeEntityContext = (params: params, ~entityConfig: Internal.entityConfig) =
150
295
  )
151
296
  }
152
297
 
153
- let write = (run, change: Change.t<Internal.entity>) => {
154
- let scope = run->scopeOf
155
- params.indexerState
156
- ->InMemoryStore.getInMemTable(~entityConfig, ~scope)
157
- ->InMemoryTable.Entity.set(
158
- ~committedCheckpointId=params.indexerState->IndexerState.committedCheckpointIdFor(~scope),
159
- change,
160
- )
161
- }
298
+ // A preload run's writes are no-ops: its run proper makes them. Inside a sync
299
+ // round they are held until the round completes, a preload's only for the
300
+ // batch's later preloads to read (PreloadLookahead), never in the store.
301
+ let write = (run: RunScope.handlerRun, change: Change.t<Internal.entity>) =>
302
+ switch (run->syncOf, run.isPreload) {
303
+ | (Some(sync), false) =>
304
+ sync.writes->SyncWrites.record(~entityConfig, ~scope=run->scopeOf, change)
305
+ | (Some(sync), true) if PreloadLookahead.isActive() =>
306
+ sync.writes->SyncWrites.record(~entityConfig, ~scope=run->scopeOf, change)
307
+ | (None, false) =>
308
+ params.indexerState->SyncWrites.write(~entityConfig, ~scope=run->scopeOf, change)
309
+ | (_, true) => ()
310
+ }
162
311
 
163
- // A preload run's writes are no-ops: its run proper makes them.
164
312
  let set = (entity: Internal.entity) => {
165
- let run = RunScope.currentHandlerRunOrThrow(~prop)
166
- if !run.isPreload {
167
- run->write(
168
- Set({
169
- entityId: entity.id->EntityId.unsafeOfString,
170
- checkpointId: run.checkpointId,
171
- entity,
172
- }),
173
- )
174
- }
313
+ let run = currentRunOrThrow(~prop)
314
+ run->write(
315
+ Set({
316
+ entityId: entity.id->EntityId.unsafeOfString,
317
+ checkpointId: run.checkpointId,
318
+ entity,
319
+ }),
320
+ )
175
321
  }
176
322
 
177
323
  let deleteUnsafe = entityId => {
178
- let run = RunScope.currentHandlerRunOrThrow(~prop)
179
- if !run.isPreload {
180
- run->write(Delete({entityId, checkpointId: run.checkpointId}))
181
- }
324
+ let run = currentRunOrThrow(~prop)
325
+ run->write(Delete({entityId, checkpointId: run.checkpointId}))
182
326
  }
183
327
 
184
328
  let entityContext: entityContext = if entityConfig->expiresUnread {
@@ -194,7 +338,7 @@ let makeEntityContext = (params: params, ~entityConfig: Internal.entityConfig) =
194
338
  {
195
339
  get: loadById,
196
340
  getWhere: filter => {
197
- let run = RunScope.currentHandlerRunOrThrow(~prop)
341
+ let run = currentRunOrThrow(~prop)
198
342
  LoadLayer.loadByFilter(
199
343
  ~loadManager=params.loadManager,
200
344
  ~persistence=params.persistence,
@@ -231,11 +375,207 @@ let makeEntityContext = (params: params, ~entityConfig: Internal.entityConfig) =
231
375
  }
232
376
  }
233
377
 
234
- Utils.StrictObject.make(
235
- Utils.StrictObject.makePrototype(op =>
236
- JsError.throwWithMessage(`Invalid context.${entityConfig.name}.${op} operation.`)
237
- ),
238
- )->Utils.StrictObject.assign(entityContext)
378
+ let object =
379
+ Utils.StrictObject.make(
380
+ Utils.StrictObject.makePrototype(op =>
381
+ JsError.throwWithMessage(`Invalid context.${entityConfig.name}.${op} operation.`)
382
+ ),
383
+ )->Utils.StrictObject.assign(entityContext)
384
+
385
+ // The subgraph runtime's synchronous reads: answered from memory, or the
386
+ // round suspends on the load and is replayed once it lands.
387
+ if Env.subgraphMode.contents {
388
+ let syncEntityContext: syncEntityContext = {
389
+ getSync: entityId => {
390
+ let run = currentRunOrThrow(~prop)
391
+ let sync = run->syncOrThrow(~prop=`${prop}.getSync`)
392
+ let scope = run->scopeOf
393
+ // The lookahead keys an entity as its writes do, a Bytes id by its hex.
394
+ let entityKey = entityConfig.table.entityIdToKey(entityId->EntityId.unsafeOfString)
395
+ switch sync.writes->SyncWrites.getById(~entityConfig, ~entityId) {
396
+ | Some(entity) => entity
397
+ | None =>
398
+ switch run.isPreload
399
+ ? PreloadLookahead.overlayGet(~item=run.item, ~entityName, ~scope, ~entityId=entityKey)
400
+ : None {
401
+ | Some(entity) => entity
402
+ | None =>
403
+ switch LoadLayer.getByIdInMemory(
404
+ ~entityConfig,
405
+ ~scope,
406
+ ~indexerState=params.indexerState,
407
+ ~entityId,
408
+ ) {
409
+ | Some(entity) =>
410
+ if run.isPreload && entity->Option.isNone {
411
+ PreloadLookahead.recordAbsent(
412
+ ~item=run.item,
413
+ ~entityName,
414
+ ~scope,
415
+ ~entityId=entityKey,
416
+ )
417
+ }
418
+ entity
419
+ | None =>
420
+ sync->missOrSuspend(
421
+ ~absent=None,
422
+ LoadLayer.loadById(
423
+ ~loadManager=params.loadManager,
424
+ ~persistence=params.persistence,
425
+ ~entityConfig,
426
+ ~scope,
427
+ ~indexerState=params.indexerState,
428
+ ~shouldGroup=run.isPreload,
429
+ ~entityId,
430
+ ),
431
+ )
432
+ }
433
+ }
434
+ }
435
+ },
436
+ getWhereSync: filter => {
437
+ let run = currentRunOrThrow(~prop)
438
+ let sync = run->syncOrThrow(~prop=`${prop}.getWhereSync`)
439
+ let scope = run->scopeOf
440
+ let filter = filter->EntityFilter.parseOrThrow(~entityName, ~table=entityConfig.table)
441
+ switch LoadLayer.getByFilterInMemory(
442
+ ~entityConfig,
443
+ ~scope,
444
+ ~indexerState=params.indexerState,
445
+ ~filter,
446
+ ) {
447
+ | Some(entities) => sync.writes->SyncWrites.overlayFilter(entities, ~entityConfig, ~filter)
448
+ | None =>
449
+ sync->missOrSuspend(
450
+ ~absent=[],
451
+ LoadLayer.loadByParsedFilter(
452
+ ~loadManager=params.loadManager,
453
+ ~persistence=params.persistence,
454
+ ~entityConfig,
455
+ ~scope,
456
+ ~indexerState=params.indexerState,
457
+ ~shouldGroup=run.isPreload,
458
+ ~filter,
459
+ ),
460
+ )
461
+ }
462
+ },
463
+ // Never suspends: the in-memory table spans the whole batch, so a change
464
+ // only counts as "in this block" when it was written at this run's
465
+ // checkpoint.
466
+ getInBlockSync: entityId => {
467
+ let run = currentRunOrThrow(~prop)
468
+ let sync = run->syncOrThrow(~prop=`${prop}.getInBlockSync`)
469
+ switch sync.writes->SyncWrites.getById(~entityConfig, ~entityId) {
470
+ | Some(entity) => entity
471
+ | None =>
472
+ let inMemTable =
473
+ params.indexerState->InMemoryStore.getInMemTable(~entityConfig, ~scope=run->scopeOf)
474
+ switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(
475
+ inMemTable.entityIdToKey(entityId->EntityId.unsafeOfString),
476
+ ) {
477
+ | Some(change) if change->Change.getCheckpointId == run.checkpointId =>
478
+ change->InMemoryTable.Entity.mapChangeToEntity
479
+ | _ => None
480
+ }
481
+ }
482
+ },
483
+ }
484
+ object->Utils.StrictObject.assign(
485
+ syncEntityContext->(Utils.magic: syncEntityContext => entityContext),
486
+ )
487
+ } else {
488
+ object
489
+ }
490
+ }
491
+
492
+ // Deterministic mappings always make progress, so the cap only exists to turn
493
+ // a non-deterministic one into a clear error instead of a hang.
494
+ let maxSyncRounds = 10000
495
+
496
+ let rec runSyncRound = async (params: params, sync: syncRun, fn: unit => unit, ~round) => {
497
+ if round > maxSyncRounds {
498
+ JsError.throwWithMessage(
499
+ `The handler suspended on a synchronous read too many times: gave up after ${maxSyncRounds->Int.toString} rounds. This usually means the code isn't deterministic across reruns.`,
500
+ )
501
+ }
502
+ let run = sync.run
503
+ sync.status = Active
504
+ sync.pending = []
505
+ sync.speculated = 0
506
+ sync.writes = SyncWrites.make()
507
+ let startedAt = PreloadLookahead.roundStarted(~isPreload=run.isPreload)
508
+
509
+ let previous = currentSync.contents
510
+ currentSync := Some(sync)
511
+ let outcome = try {
512
+ fn()
513
+ Ok()
514
+ } catch {
515
+ | exn => Error(exn)
516
+ }
517
+ currentSync := previous
518
+ PreloadLookahead.roundEnded(~item=run.item, ~startedAt)
519
+ let writes = sync.writes
520
+
521
+ // Neither a suspended round nor a failed one keeps what it wrote.
522
+ sync.writes = SyncWrites.make()
523
+
524
+ let suspended = switch outcome {
525
+ | Ok() =>
526
+ switch sync.status {
527
+ // The body caught the suspend and returned anyway, so it didn't see the
528
+ // reads it asked for: it is replayed like any other suspended round.
529
+ | Aborted(_) => true
530
+ // Ran on reads it was told were absent.
531
+ | Active if sync.speculated > 0 => true
532
+ | Active =>
533
+ if run.isPreload {
534
+ writes->SyncWrites.forEachChange((~entityConfig, ~scope, change) =>
535
+ PreloadLookahead.overlaySet(
536
+ ~item=run.item,
537
+ ~entityName=entityConfig.name,
538
+ ~scope,
539
+ ~entityId=entityConfig.table.entityIdToKey(change->Change.getEntityId),
540
+ ~entity=change->InMemoryTable.Entity.mapChangeToEntity,
541
+ )
542
+ )
543
+ } else {
544
+ writes->SyncWrites.commit(~indexerState=params.indexerState)
545
+ }
546
+ false
547
+ }
548
+ // A round handed a speculated absence may well throw on it: the replay that
549
+ // sees the real value decides whether the error is the handler's.
550
+ | Error(exn) if exn->isSuspend || sync.speculated > 0 => true
551
+ | Error(exn) =>
552
+ // The body threw its own error after scheduling a read. Those ops are
553
+ // nobody's result now, and an unhandled rejection would replace the error
554
+ // the handler actually raised.
555
+ sync.pending->Array.forEach(promise => promise->Utils.Promise.catchResolve(_ => ())->ignore)
556
+ sync.pending = []
557
+ throw(exn)
558
+ }
559
+
560
+ if suspended {
561
+ // A round only suspends by scheduling the read it missed.
562
+ let pending = sync.pending
563
+ sync.pending = []
564
+ let errors = []
565
+ let _ = await pending
566
+ ->Array.map(promise =>
567
+ promise->Promise.catch(exn => {
568
+ errors->Array.push(exn)
569
+ Promise.resolve()
570
+ })
571
+ )
572
+ ->Promise.all
573
+ switch errors->Array.get(0) {
574
+ | Some(exn) => throw(exn)
575
+ | None => ()
576
+ }
577
+ await params->runSyncRound(sync, fn, ~round=round + 1)
578
+ }
239
579
  }
240
580
 
241
581
  // One context for every handler run of an indexer.
@@ -251,7 +591,7 @@ let makeHandlerContext = (params: params): Internal.handlerContext => {
251
591
  context
252
592
  ->Utils.Object.defineProperty(
253
593
  name,
254
- {enumerable: true, get: () => get(RunScope.currentHandlerRunOrThrow(~prop=name))},
594
+ {enumerable: true, get: () => get(currentRunOrThrow(~prop=name))},
255
595
  )
256
596
  ->ignore
257
597
 
@@ -263,12 +603,7 @@ let makeHandlerContext = (params: params): Internal.handlerContext => {
263
603
  // Reads the run when called, so a kept `context.effect` is refused once its
264
604
  // handler resolved, like a kept entity accessor.
265
605
  let effect = (effect, input) =>
266
- params->callEffect(
267
- ~run=RunScope.currentHandlerRunOrThrow(~prop="effect"),
268
- ~caller=None,
269
- effect,
270
- input,
271
- )
606
+ params->callEffect(~run=currentRunOrThrow(~prop="effect"), ~caller=None, effect, input)
272
607
  context
273
608
  ->Utils.Object.definePropertyWithValue(
274
609
  "effect",
@@ -282,6 +617,44 @@ let makeHandlerContext = (params: params): Internal.handlerContext => {
282
617
  },
283
618
  )
284
619
  ->ignore
620
+ if Env.subgraphMode.contents {
621
+ context
622
+ ->Utils.Object.definePropertyWithValue(
623
+ "effectSync",
624
+ {
625
+ enumerable: true,
626
+ value: ((effect, input) => params->effectSync(effect, input))->(
627
+ Utils.magic: ((Internal.effect, Internal.effectInput) => Internal.effectOutput) => unknown
628
+ ),
629
+ },
630
+ )
631
+ ->ignore
632
+ context
633
+ ->Utils.Object.definePropertyWithValue(
634
+ "runSync",
635
+ {
636
+ enumerable: true,
637
+ value: (
638
+ fn => {
639
+ let run = currentRunOrThrow(~prop="runSync")
640
+ params->runSyncRound(
641
+ {
642
+ run,
643
+ status: Active,
644
+ pending: [],
645
+ memo: Dict.make(),
646
+ writes: SyncWrites.make(),
647
+ speculated: 0,
648
+ },
649
+ fn,
650
+ ~round=1,
651
+ )
652
+ }
653
+ )->(Utils.magic: ((unit => unit) => promise<unit>) => unknown),
654
+ },
655
+ )
656
+ ->ignore
657
+ }
285
658
  define("isPreload", run => run.isPreload->(Utils.magic: bool => unknown))
286
659
  define("chain", run =>
287
660
  params.chains