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.
- package/bin.mjs +10 -1
- package/index.d.ts +11 -0
- package/package.json +8 -20
- package/rescript.json +1 -4
- package/src/Config.res +78 -100
- package/src/Config.res.mjs +55 -64
- package/src/Core.res +67 -6
- package/src/Core.res.mjs +58 -4
- package/src/EventConfigBuilder.res +4 -10
- package/src/EventConfigBuilder.res.mjs +1 -3
- package/src/EventProcessing.res +6 -6
- package/src/EventProcessing.res.mjs +8 -6
- package/src/ExitOnCaughtUp.res +5 -1
- package/src/ExitOnCaughtUp.res.mjs +8 -3
- package/src/HandlerLoader.res +98 -36
- package/src/HandlerLoader.res.mjs +89 -24
- package/src/HandlerTypeCheck.mjs +14 -0
- package/src/HandlerTypeCheckWorker.mjs +68 -0
- package/src/Internal.res +1 -1
- package/src/LoadLayer.res +39 -7
- package/src/LoadLayer.res.mjs +65 -38
- package/src/LoadLayer.resi +28 -0
- package/src/Main.res +17 -12
- package/src/Main.res.mjs +9 -3
- package/src/NodeVersion.mjs +13 -0
- package/src/Persistence.res +79 -38
- package/src/Persistence.res.mjs +41 -10
- package/src/PgStorage.res +86 -39
- package/src/PgStorage.res.mjs +83 -32
- package/src/ResumePlan.res +172 -0
- package/src/ResumePlan.res.mjs +122 -0
- package/src/Supervisor.res +8 -8
- package/src/Supervisor.res.mjs +6 -1
- package/src/SyncWrites.res +86 -0
- package/src/SyncWrites.res.mjs +85 -0
- package/src/TestIndexer.res +14 -4
- package/src/TestIndexer.res.mjs +3 -2
- package/src/TsModuleHooks.mjs +121 -0
- package/src/UserContext.res +377 -62
- package/src/UserContext.res.mjs +297 -39
- package/src/Utils.res +0 -8
- package/src/Worker.res +8 -0
- package/src/Worker.res.mjs +11 -0
- package/src/bindings/NodeJs.res +8 -7
- package/src/bindings/NodeJs.res.mjs +0 -3
- package/src/bindings/Pino.res +2 -2
- package/src/db/InternalTable.res +52 -2
- package/src/db/InternalTable.res.mjs +32 -3
- package/src/db/Table.res +10 -0
- package/src/db/Table.res.mjs +22 -11
- package/src/sources/FuelHyperSync.res +2 -0
- package/src/sources/FuelHyperSync.res.mjs +1 -0
- package/src/sources/FuelHyperSync.resi +1 -0
- package/src/sources/FuelHyperSyncClient.res +25 -8
- package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
- package/src/sources/FuelHyperSyncSource.res +24 -22
- package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
- package/src/subgraph/abi-types.ts +63 -0
- package/src/subgraph/assemblyscript.ts +85 -0
- package/src/subgraph/blocks.ts +183 -0
- package/src/subgraph/calls.ts +149 -0
- package/src/subgraph/conformance.ts +83 -0
- package/src/subgraph/errors.ts +96 -0
- package/src/subgraph/graph-cli-worker.mjs +26 -0
- package/src/subgraph/graph-cli.ts +190 -0
- package/src/subgraph/graph-ts-enums.ts +28 -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 +2285 -0
- package/src/subgraph/hosts.ts +158 -0
- package/src/subgraph/rpc.ts +65 -0
- package/src/subgraph/runtime.ts +595 -0
- package/src/subgraph/scope.ts +68 -0
- package/src/tui/InitApi.res +85 -0
- package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
- package/src/tui/Tui.res +142 -249
- package/src/tui/Tui.res.mjs +123 -332
- package/src/Js.shim.ts +0 -11
- package/src/bindings/DateFns.res +0 -74
- package/src/bindings/DateFns.res.mjs +0 -22
- package/src/sources/FuelSDK.res +0 -38
- package/src/sources/FuelSDK.res.mjs +0 -27
- package/src/sources/vendored-fuel-abi-coder.js +0 -1792
- package/src/tui/bindings/Ink.res +0 -369
- package/src/tui/bindings/Ink.res.mjs +0 -66
- package/src/tui/bindings/Style.res +0 -123
- package/src/tui/bindings/Style.res.mjs +0 -2
- package/src/tui/components/BufferedProgressBar.res +0 -40
- package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
- package/src/tui/components/CustomHooks.res +0 -122
- package/src/tui/components/Messages.res +0 -40
- package/src/tui/components/Messages.res.mjs +0 -71
- package/src/tui/components/SyncETA.res +0 -179
- package/src/tui/components/SyncETA.res.mjs +0 -249
- package/src/tui/components/TuiData.res +0 -98
- package/src/tui/components/TuiData.res.mjs +0 -73
package/src/UserContext.res
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
->
|
|
165
|
-
~
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
648
|
+
sync: params.sync,
|
|
334
649
|
config: params.config,
|
|
335
650
|
entityConfig,
|
|
336
651
|
}
|