envio 3.13.0 → 3.14.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 (111) hide show
  1. package/bin.mjs +10 -1
  2. package/index.d.ts +11 -0
  3. package/package.json +8 -20
  4. package/rescript.json +1 -4
  5. package/src/Config.res +78 -100
  6. package/src/Config.res.mjs +55 -64
  7. package/src/Core.res +67 -6
  8. package/src/Core.res.mjs +58 -4
  9. package/src/EventConfigBuilder.res +4 -10
  10. package/src/EventConfigBuilder.res.mjs +1 -3
  11. package/src/EventProcessing.res +6 -6
  12. package/src/EventProcessing.res.mjs +8 -6
  13. package/src/ExitOnCaughtUp.res +5 -1
  14. package/src/ExitOnCaughtUp.res.mjs +8 -3
  15. package/src/HandlerLoader.res +98 -36
  16. package/src/HandlerLoader.res.mjs +89 -24
  17. package/src/HandlerTypeCheck.mjs +14 -0
  18. package/src/HandlerTypeCheckWorker.mjs +68 -0
  19. package/src/Internal.res +1 -1
  20. package/src/LoadLayer.res +39 -7
  21. package/src/LoadLayer.res.mjs +65 -38
  22. package/src/LoadLayer.resi +28 -0
  23. package/src/Main.res +17 -12
  24. package/src/Main.res.mjs +9 -3
  25. package/src/NodeVersion.mjs +13 -0
  26. package/src/Persistence.res +79 -38
  27. package/src/Persistence.res.mjs +41 -10
  28. package/src/PgStorage.res +86 -39
  29. package/src/PgStorage.res.mjs +83 -32
  30. package/src/ResumePlan.res +172 -0
  31. package/src/ResumePlan.res.mjs +122 -0
  32. package/src/Supervisor.res +8 -8
  33. package/src/Supervisor.res.mjs +6 -1
  34. package/src/SyncWrites.res +86 -0
  35. package/src/SyncWrites.res.mjs +85 -0
  36. package/src/TestIndexer.res +14 -4
  37. package/src/TestIndexer.res.mjs +3 -2
  38. package/src/TsModuleHooks.mjs +121 -0
  39. package/src/UserContext.res +377 -62
  40. package/src/UserContext.res.mjs +297 -39
  41. package/src/Utils.res +0 -8
  42. package/src/Worker.res +8 -0
  43. package/src/Worker.res.mjs +11 -0
  44. package/src/bindings/NodeJs.res +8 -7
  45. package/src/bindings/NodeJs.res.mjs +0 -3
  46. package/src/bindings/Pino.res +2 -2
  47. package/src/db/InternalTable.res +52 -2
  48. package/src/db/InternalTable.res.mjs +32 -3
  49. package/src/db/Table.res +10 -0
  50. package/src/db/Table.res.mjs +22 -11
  51. package/src/sources/FuelHyperSync.res +2 -0
  52. package/src/sources/FuelHyperSync.res.mjs +1 -0
  53. package/src/sources/FuelHyperSync.resi +1 -0
  54. package/src/sources/FuelHyperSyncClient.res +25 -8
  55. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  56. package/src/sources/FuelHyperSyncSource.res +24 -22
  57. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  58. package/src/subgraph/abi-types.ts +63 -0
  59. package/src/subgraph/assemblyscript.ts +85 -0
  60. package/src/subgraph/blocks.ts +183 -0
  61. package/src/subgraph/calls.ts +149 -0
  62. package/src/subgraph/conformance.ts +83 -0
  63. package/src/subgraph/errors.ts +96 -0
  64. package/src/subgraph/graph-cli-worker.mjs +26 -0
  65. package/src/subgraph/graph-cli.ts +190 -0
  66. package/src/subgraph/graph-ts-enums.ts +28 -0
  67. package/src/subgraph/graph-ts-types/VERSION +2 -0
  68. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  69. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  70. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  71. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  72. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  73. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  74. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  75. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  76. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  77. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  78. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  79. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  80. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  81. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  82. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  83. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  84. package/src/subgraph/graph-ts.ts +2285 -0
  85. package/src/subgraph/hosts.ts +158 -0
  86. package/src/subgraph/rpc.ts +65 -0
  87. package/src/subgraph/runtime.ts +595 -0
  88. package/src/subgraph/scope.ts +68 -0
  89. package/src/tui/InitApi.res +85 -0
  90. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  91. package/src/tui/Tui.res +142 -249
  92. package/src/tui/Tui.res.mjs +123 -332
  93. package/src/Js.shim.ts +0 -11
  94. package/src/bindings/DateFns.res +0 -74
  95. package/src/bindings/DateFns.res.mjs +0 -22
  96. package/src/sources/FuelSDK.res +0 -38
  97. package/src/sources/FuelSDK.res.mjs +0 -27
  98. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  99. package/src/tui/bindings/Ink.res +0 -369
  100. package/src/tui/bindings/Ink.res.mjs +0 -66
  101. package/src/tui/bindings/Style.res +0 -123
  102. package/src/tui/bindings/Style.res.mjs +0 -2
  103. package/src/tui/components/BufferedProgressBar.res +0 -40
  104. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  105. package/src/tui/components/CustomHooks.res +0 -122
  106. package/src/tui/components/Messages.res +0 -40
  107. package/src/tui/components/Messages.res.mjs +0 -71
  108. package/src/tui/components/SyncETA.res +0 -179
  109. package/src/tui/components/SyncETA.res.mjs +0 -249
  110. package/src/tui/components/TuiData.res +0 -98
  111. package/src/tui/components/TuiData.res.mjs +0 -73
@@ -1,3 +1,38 @@
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 traps
4
+ // keep re-throwing it while the context stays aborted.
5
+ exception Suspend
6
+
7
+ let isSuspend = exn =>
8
+ switch exn {
9
+ | Suspend => true
10
+ | _ => false
11
+ }
12
+
13
+ type contextStatus =
14
+ | Active
15
+ // Set by a suspended sync op. Every trap access and every op closure
16
+ // re-throws the stored error, so even a caught suspend stops the handler
17
+ // at its next context interaction.
18
+ | Aborted(exn)
19
+ | Resolved
20
+
21
+ // Held by reference so the entity sub-proxies, which copy the rest of the
22
+ // params by value, observe the same lifecycle as the handler context.
23
+ type syncState = {
24
+ mutable status: contextStatus,
25
+ // Ops scheduled by this round's suspended reads, awaited before the replay.
26
+ mutable pending: option<array<promise<unit>>>,
27
+ // Effect outputs read from the in-memory table during this handler
28
+ // invocation, reused by its later replay rounds.
29
+ mutable memo: option<dict<Internal.effectOutput>>,
30
+ // Only while a synchronous round runs: its writes, held until it completes.
31
+ mutable writes: option<SyncWrites.t>,
32
+ }
33
+
34
+ let makeSyncState = () => {status: Active, pending: None, memo: None, writes: None}
35
+
1
36
  type contextParams = {
2
37
  item: Internal.item,
3
38
  checkpointId: Internal.checkpointId,
@@ -7,7 +42,45 @@ type contextParams = {
7
42
  isPreload: bool,
8
43
  chains: Internal.chains,
9
44
  config: Config.t,
10
- mutable isResolved: bool,
45
+ sync: syncState,
46
+ }
47
+
48
+ let getPending = (sync: syncState) =>
49
+ switch sync.pending {
50
+ | Some(pending) => pending
51
+ | None =>
52
+ let pending = []
53
+ sync.pending = Some(pending)
54
+ pending
55
+ }
56
+
57
+ let getMemo = (sync: syncState) =>
58
+ switch sync.memo {
59
+ | Some(memo) => memo
60
+ | None =>
61
+ let memo = Dict.make()
62
+ sync.memo = Some(memo)
63
+ memo
64
+ }
65
+
66
+ let checkStatusOrThrow = (params: contextParams, ~access: string) =>
67
+ switch params.sync.status {
68
+ | Active => ()
69
+ | Aborted(exn) => throw(exn)
70
+ | Resolved =>
71
+ Utils.Error.make(
72
+ `Impossible to access ${access} after the handler is resolved. Make sure you didn't miss an await in the handler.`,
73
+ )->ErrorHandling.mkLogAndRaise(
74
+ ~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
75
+ )
76
+ }
77
+
78
+ // Fires the async op behind a sync miss, records it for the replay loop and
79
+ // aborts the context.
80
+ let scheduleAndSuspend = (params: contextParams, promise: promise<'a>): 'b => {
81
+ params.sync->getPending->Array.push(promise->Utils.Promise.ignoreValue)
82
+ params.sync.status = Aborted(Suspend)
83
+ throw(Suspend)
11
84
  }
12
85
 
13
86
  // We don't want to expose the params to the user
@@ -58,8 +131,15 @@ external makeEffectContext: (
58
131
  ~callEffect: (Internal.effect, Internal.effectInput) => promise<Internal.effectOutput>,
59
132
  ) => Internal.effectContext = "EffectContext"
60
133
 
61
- let initEffect = (params: contextParams) => {
62
- let handlerChainId = params.item->Internal.getItemChainId
134
+ // Builds the scope and args a call to `effect` resolves against. Split out of
135
+ // `initEffect` so the sync caller derives the same cache key and scope without
136
+ // duplicating the nested-caller rules.
137
+ let rec prepareEffectCall = (
138
+ params: contextParams,
139
+ ~effect: Internal.effect,
140
+ ~input: Internal.effectInput,
141
+ ~caller: option<Internal.effect>,
142
+ ) => {
63
143
  // An effect that didn't state a scope follows the config: cross-chain by
64
144
  // default, per-chain under `disable_default_cross_chain`.
65
145
  let isCrossChain = (effect: Internal.effect) =>
@@ -67,51 +147,103 @@ let initEffect = (params: contextParams) => {
67
147
  // A chain-scoped effect always resolves against the chain of the handler that
68
148
  // triggered the call, even several effects deep, so the chain id is captured
69
149
  // once from the item and reused for the whole nested-call tree.
70
- let rec makeCaller = (~caller: option<Internal.effect>) => {
71
- (effect: Internal.effect, input: Internal.effectInput) => {
72
- let scope = effect->isCrossChain ? Internal.CrossChain : Internal.Chain(handlerChainId)
73
-
74
- switch caller {
75
- | Some(callerEffect) if callerEffect->isCrossChain && !(effect->isCrossChain) =>
76
- // A cross-chain effect isn't tied to a single chain, so it has no chain
77
- // to resolve a chain-scoped child against. Reject before any cache work.
78
- JsError.throwWithMessage(
79
- `The cross-chain effect "${callerEffect.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 "${callerEffect.name}" chain-scoped (\`crossChain: false\`).`,
80
- )
81
- | _ => ()
82
- }
150
+ let scope =
151
+ effect->isCrossChain
152
+ ? Internal.CrossChain
153
+ : Internal.Chain(params.item->Internal.getItemChainId)
83
154
 
84
- let effectContext = makeEffectContext(
85
- params,
86
- ~chainId=switch scope {
87
- | Internal.Chain(chainId) => Some(chainId)
88
- | Internal.CrossChain => None
89
- },
90
- ~effectName=effect.name,
91
- ~defaultShouldCache=effect.defaultShouldCache,
92
- ~callEffect=makeCaller(~caller=Some(effect)),
93
- )
94
- let effectArgs: Internal.effectArgs = {
95
- input,
96
- context: effectContext,
97
- cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
98
- chainId: handlerChainId,
99
- checkpointId: params.checkpointId,
155
+ switch caller {
156
+ | Some(callerEffect) if callerEffect->isCrossChain && !(effect->isCrossChain) =>
157
+ // A cross-chain effect isn't tied to a single chain, so it has no chain
158
+ // to resolve a chain-scoped child against. Reject before any cache work.
159
+ JsError.throwWithMessage(
160
+ `The cross-chain effect "${callerEffect.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 "${callerEffect.name}" chain-scoped (\`crossChain: false\`).`,
161
+ )
162
+ | _ => ()
163
+ }
164
+
165
+ let effectContext = makeEffectContext(
166
+ params,
167
+ ~chainId=switch scope {
168
+ | Internal.Chain(chainId) => Some(chainId)
169
+ | Internal.CrossChain => None
170
+ },
171
+ ~effectName=effect.name,
172
+ ~defaultShouldCache=effect.defaultShouldCache,
173
+ // Nested calls made by the effect handler itself stay async: only the
174
+ // handler that started the sync run needs a sync answer.
175
+ ~callEffect=(nested, nestedInput) =>
176
+ params->callEffectAsync(~effect=nested, ~input=nestedInput, ~caller=Some(effect)),
177
+ )
178
+ let effectArgs: Internal.effectArgs = {
179
+ input,
180
+ context: effectContext,
181
+ cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
182
+ chainId: params.item->Internal.getItemChainId,
183
+ checkpointId: params.checkpointId,
184
+ }
185
+ (scope, effectArgs)
186
+ }
187
+
188
+ and callEffectAsync = (params: contextParams, ~effect, ~input, ~caller) => {
189
+ let (scope, effectArgs) = params->prepareEffectCall(~effect, ~input, ~caller)
190
+ LoadLayer.loadEffect(
191
+ ~loadManager=params.loadManager,
192
+ ~persistence=params.persistence,
193
+ ~effect,
194
+ ~effectArgs,
195
+ ~scope,
196
+ ~indexerState=params.indexerState,
197
+ ~shouldGroup=params.isPreload,
198
+ ~item=params.item,
199
+ ~ecosystem=params.config.ecosystem,
200
+ )
201
+ }
202
+
203
+ let initEffect = (params: contextParams) => {
204
+ (effect: Internal.effect, input: Internal.effectInput) => {
205
+ params->checkStatusOrThrow(~access="context.effect")
206
+ params->callEffectAsync(~effect, ~input, ~caller=None)
207
+ }
208
+ }
209
+
210
+ let effectMemoKey = (~effect: Internal.effect, ~scope: Internal.chainScope, ~cacheKey) =>
211
+ switch scope {
212
+ | CrossChain => `${effect.name}.${cacheKey}`
213
+ | Chain(chainId) => `${effect.name}.${chainId->ChainId.toString}.${cacheKey}`
214
+ }
215
+
216
+ let initEffectSync = (params: contextParams) => {
217
+ (effect: Internal.effect, input: Internal.effectInput) => {
218
+ params->checkStatusOrThrow(~access="context.effectSync")
219
+ let (scope, effectArgs) = params->prepareEffectCall(~effect, ~input, ~caller=None)
220
+ let memo = params.sync->getMemo
221
+ let memoKey = effectMemoKey(~effect, ~scope, ~cacheKey=effectArgs.cacheKey)
222
+ switch memo->Utils.Dict.dangerouslyGetNonOption(memoKey) {
223
+ | Some(output) => output
224
+ | None =>
225
+ let inMemTable = params.indexerState->InMemoryStore.getEffectInMemTable(~effect, ~scope)
226
+ if inMemTable->InMemoryStore.hasEffectOutput(effectArgs.cacheKey) {
227
+ let output = inMemTable->InMemoryStore.getEffectOutputUnsafe(effectArgs.cacheKey)
228
+ memo->Dict.set(memoKey, output)
229
+ output
230
+ } else {
231
+ params->scheduleAndSuspend(
232
+ LoadLayer.loadEffect(
233
+ ~loadManager=params.loadManager,
234
+ ~persistence=params.persistence,
235
+ ~effect,
236
+ ~effectArgs,
237
+ ~scope,
238
+ ~indexerState=params.indexerState,
239
+ ~shouldGroup=params.isPreload,
240
+ ~item=params.item,
241
+ ~ecosystem=params.config.ecosystem,
242
+ ),
243
+ )
100
244
  }
101
- LoadLayer.loadEffect(
102
- ~loadManager=params.loadManager,
103
- ~persistence=params.persistence,
104
- ~effect,
105
- ~effectArgs,
106
- ~scope,
107
- ~indexerState=params.indexerState,
108
- ~shouldGroup=params.isPreload,
109
- ~item=params.item,
110
- ~ecosystem=params.config.ecosystem,
111
- )
112
245
  }
113
246
  }
114
- makeCaller(~caller=None)
115
247
  }
116
248
 
117
249
  type entityContextParams = {
@@ -148,21 +280,117 @@ let throwClickHouseReadOnly = (entityConfig: Internal.entityConfig, op: string)
148
280
  `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.`,
149
281
  )
150
282
 
283
+ let writeChange = (params: entityContextParams, change) => {
284
+ let scope = params->entityScope
285
+ switch params.sync.writes {
286
+ | Some(writes) => writes->SyncWrites.record(~entityConfig=params.entityConfig, ~scope, change)
287
+ | None => params.indexerState->SyncWrites.write(~entityConfig=params.entityConfig, ~scope, change)
288
+ }
289
+ }
290
+
291
+ let getRoundWrite = (params: entityContextParams, entityId) =>
292
+ switch params.sync.writes {
293
+ | Some(writes) => writes->SyncWrites.getById(~entityConfig=params.entityConfig, ~entityId)
294
+ | None => None
295
+ }
296
+
297
+ let getSyncHandler = (params: entityContextParams, entityId: string) =>
298
+ switch params->getRoundWrite(entityId) {
299
+ | Some(entity) => entity
300
+ | None =>
301
+ switch LoadLayer.getByIdInMemory(
302
+ ~entityConfig=params.entityConfig,
303
+ ~scope=params->entityScope,
304
+ ~indexerState=params.indexerState,
305
+ ~entityId,
306
+ ) {
307
+ | Some(entity) => entity
308
+ | None =>
309
+ (params :> contextParams)->scheduleAndSuspend(
310
+ LoadLayer.loadById(
311
+ ~loadManager=params.loadManager,
312
+ ~persistence=params.persistence,
313
+ ~entityConfig=params.entityConfig,
314
+ ~scope=params->entityScope,
315
+ ~indexerState=params.indexerState,
316
+ ~shouldGroup=params.isPreload,
317
+ ~item=params.item,
318
+ ~ecosystem=params.config.ecosystem,
319
+ ~entityId,
320
+ ),
321
+ )
322
+ }
323
+ }
324
+
325
+ let getWhereSyncHandler = (params: entityContextParams, filter: dict<dict<unknown>>) => {
326
+ let entityConfig = params.entityConfig
327
+ let filter =
328
+ filter->EntityFilter.parseOrThrow(~entityName=entityConfig.name, ~table=entityConfig.table)
329
+ switch LoadLayer.getByFilterInMemory(
330
+ ~entityConfig,
331
+ ~scope=params->entityScope,
332
+ ~indexerState=params.indexerState,
333
+ ~filter,
334
+ ) {
335
+ | Some(entities) =>
336
+ switch params.sync.writes {
337
+ | Some(writes) => writes->SyncWrites.overlayFilter(entities, ~entityConfig, ~filter)
338
+ | None => entities
339
+ }
340
+ | None =>
341
+ (params :> contextParams)->scheduleAndSuspend(
342
+ LoadLayer.loadByParsedFilter(
343
+ ~loadManager=params.loadManager,
344
+ ~persistence=params.persistence,
345
+ ~entityConfig,
346
+ ~scope=params->entityScope,
347
+ ~indexerState=params.indexerState,
348
+ ~shouldGroup=params.isPreload,
349
+ ~item=params.item,
350
+ ~ecosystem=params.config.ecosystem,
351
+ ~filter,
352
+ ),
353
+ )
354
+ }
355
+ }
356
+
357
+ // Never suspends: the in-memory table spans the whole batch, so a change only
358
+ // counts as "in this block" when it was written at this handler's checkpoint.
359
+ let getInBlockSyncHandler = (params: entityContextParams, entityId: string) =>
360
+ switch params->getRoundWrite(entityId) {
361
+ | Some(entity) => entity
362
+ | None =>
363
+ let inMemTable =
364
+ params.indexerState->InMemoryStore.getInMemTable(
365
+ ~entityConfig=params.entityConfig,
366
+ ~scope=params->entityScope,
367
+ )
368
+ switch inMemTable.latestEntityChangeById->Utils.Dict.dangerouslyGetNonOption(entityId) {
369
+ | Some(change) if change->Change.getCheckpointId == params.checkpointId =>
370
+ change->InMemoryTable.Entity.mapChangeToEntity
371
+ | _ => None
372
+ }
373
+ }
374
+
151
375
  let entityTraps: Utils.Proxy.traps<entityContextParams> = {
152
376
  get: (~target as params, ~prop: unknown) => {
153
377
  let prop = prop->(Utils.magic: unknown => string)
154
378
 
379
+ (params :> contextParams)->checkStatusOrThrow(
380
+ ~access=`context.${params.entityConfig.name}.${prop}`,
381
+ )
382
+
155
383
  let isClickHouseOnly = !params.entityConfig.storage.postgres
156
- let scope = params->entityScope
157
- let committedCheckpointId = params.indexerState->IndexerState.committedCheckpointIdFor(~scope)
158
384
 
159
385
  let set = params.isPreload
160
386
  ? noopSet
161
387
  : (entity: Internal.entity) => {
162
- params.indexerState
163
- ->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig, ~scope)
164
- ->InMemoryTable.Entity.set(
165
- ~committedCheckpointId,
388
+ // The check lives inside the closure too: a handler that grabbed
389
+ // `context.X.set` before a suspend must not keep writing.
390
+ (params :> contextParams)->checkStatusOrThrow(
391
+ ~access=`context.${params.entityConfig.name}.set`,
392
+ )
393
+ params->writeChange(
166
394
  Set({
167
395
  entityId: entity.id->EntityId.unsafeOfString,
168
396
  checkpointId: params.checkpointId,
@@ -172,6 +400,20 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
172
400
  }
173
401
 
174
402
  switch prop {
403
+ | "getSync" =>
404
+ (entityId => params->getSyncHandler(entityId))->(
405
+ Utils.magic: (string => option<Internal.entity>) => unknown
406
+ )
407
+
408
+ | "getWhereSync" =>
409
+ (
410
+ filter => params->getWhereSyncHandler(filter->(Utils.magic: unknown => dict<dict<unknown>>))
411
+ )->(Utils.magic: (unknown => array<Internal.entity>) => unknown)
412
+ | "getInBlockSync" =>
413
+ (entityId => params->getInBlockSyncHandler(entityId))->(
414
+ Utils.magic: (string => option<Internal.entity>) => unknown
415
+ )
416
+
175
417
  | "get" =>
176
418
  if isClickHouseOnly {
177
419
  ((_entityId: string) => throwClickHouseReadOnly(params.entityConfig, "get"))->(
@@ -271,10 +513,10 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
271
513
  noopDeleteUnsafe
272
514
  } else {
273
515
  entityId => {
274
- params.indexerState
275
- ->InMemoryStore.getInMemTable(~entityConfig=params.entityConfig, ~scope)
276
- ->InMemoryTable.Entity.set(
277
- ~committedCheckpointId,
516
+ (params :> contextParams)->checkStatusOrThrow(
517
+ ~access=`context.${params.entityConfig.name}.deleteUnsafe`,
518
+ )
519
+ params->writeChange(
278
520
  Delete({
279
521
  entityId,
280
522
  checkpointId: params.checkpointId,
@@ -288,17 +530,90 @@ let entityTraps: Utils.Proxy.traps<entityContextParams> = {
288
530
  },
289
531
  }
290
532
 
533
+ // Deterministic mappings always make progress, so the cap only exists to turn
534
+ // a non-deterministic one into a clear error instead of a hang.
535
+ let maxSyncRounds = 10000
536
+
537
+ // Runs a synchronous body, replaying it from the top each time it suspends on
538
+ // a read that wasn't in memory yet.
539
+ let rec runSyncRound = async (params: contextParams, fn: unit => unit, ~round) => {
540
+ if round > maxSyncRounds {
541
+ JsError.throwWithMessage(
542
+ `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.`,
543
+ )
544
+ }
545
+ params.sync.status = Active
546
+ params.sync.pending = None
547
+ let writes = SyncWrites.make()
548
+ params.sync.writes = Some(writes)
549
+
550
+ let suspended = switch fn() {
551
+ | () =>
552
+ params.sync.writes = None
553
+ switch params.sync.status {
554
+ // The body caught the suspend and returned anyway, so it didn't see the
555
+ // reads it asked for: it is replayed like any other suspended round.
556
+ | Aborted(_) => true
557
+ | Active | Resolved =>
558
+ writes->SyncWrites.commit(~indexerState=params.indexerState)
559
+ false
560
+ }
561
+ | exception exn =>
562
+ // Neither a suspended round nor a failed one keeps what it wrote.
563
+ params.sync.writes = None
564
+ if exn->isSuspend {
565
+ true
566
+ } else {
567
+ // The body threw its own error after scheduling a read. Those ops are
568
+ // nobody's result now, and an unhandled rejection would replace the
569
+ // error the handler actually raised.
570
+ switch params.sync.pending {
571
+ | None => ()
572
+ | Some(pending) => {
573
+ params.sync.pending = None
574
+ pending->Array.forEach(promise => promise->Utils.Promise.silentCatch->ignore)
575
+ }
576
+ }
577
+ throw(exn)
578
+ }
579
+ }
580
+
581
+ if suspended {
582
+ // A round only suspends by scheduling the read it missed.
583
+ let pending = params.sync.pending->Option.getOr([])
584
+ params.sync.pending = None
585
+ let errors = []
586
+ let _ = await pending
587
+ ->Array.map(promise =>
588
+ promise->Promise.catch(exn => {
589
+ errors->Array.push(exn)
590
+ Promise.resolve()
591
+ })
592
+ )
593
+ ->Promise.all
594
+ switch errors->Array.get(0) {
595
+ | Some(exn) => throw(exn)
596
+ | None => ()
597
+ }
598
+ await params->runSyncRound(fn, ~round=round + 1)
599
+ }
600
+ }
601
+
291
602
  let handlerTraps: Utils.Proxy.traps<contextParams> = {
292
603
  get: (~target as params, ~prop: unknown) => {
293
604
  let prop = prop->(Utils.magic: unknown => string)
294
- if params.isResolved {
295
- Utils.Error.make(
296
- `Impossible to access context.${prop} after the handler is resolved. Make sure you didn't miss an await in the handler.`,
297
- )->ErrorHandling.mkLogAndRaise(
298
- ~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
299
- )
300
- }
605
+ params->checkStatusOrThrow(~access=`context.${prop}`)
301
606
  switch prop {
607
+ | "effectSync" =>
608
+ initEffectSync((params :> contextParams))->(
609
+ Utils.magic: ((Internal.effect, Internal.effectInput) => Internal.effectOutput) => unknown
610
+ )
611
+
612
+ | "runSync" =>
613
+ (fn => params->runSyncRound(fn, ~round=1))->(
614
+ Utils.magic: ((unit => unit) => promise<unit>) => unknown
615
+ )
616
+
302
617
  | "log" =>
303
618
  (
304
619
  params.isPreload
@@ -330,7 +645,7 @@ let handlerTraps: Utils.Proxy.traps<contextParams> = {
330
645
  persistence: params.persistence,
331
646
  checkpointId: params.checkpointId,
332
647
  chains: params.chains,
333
- isResolved: params.isResolved,
648
+ sync: params.sync,
334
649
  config: params.config,
335
650
  entityConfig,
336
651
  }