envio 3.14.1 → 3.15.0-sidecars.2

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 (140) hide show
  1. package/evm.schema.json +96 -0
  2. package/fuel.schema.json +96 -0
  3. package/index.d.ts +129 -0
  4. package/index.js +2 -0
  5. package/package.json +24 -10
  6. package/sidecar.d.ts +1 -0
  7. package/sidecar.js +1 -0
  8. package/src/Batch.res +78 -7
  9. package/src/Batch.res.mjs +52 -13
  10. package/src/Bin.res +12 -0
  11. package/src/Bin.res.mjs +12 -0
  12. package/src/ChainState.res +32 -4
  13. package/src/ChainState.res.mjs +68 -45
  14. package/src/ChainState.resi +4 -0
  15. package/src/Config.res +45 -0
  16. package/src/Config.res.mjs +32 -0
  17. package/src/Core.res +46 -1
  18. package/src/Ecosystem.res +10 -0
  19. package/src/Ecosystem.res.mjs +21 -9
  20. package/src/Env.res +22 -1
  21. package/src/Env.res.mjs +27 -2
  22. package/src/EventProcessing.res +56 -0
  23. package/src/EventProcessing.res.mjs +135 -76
  24. package/src/ExitOnCaughtUp.res +8 -0
  25. package/src/ExitOnCaughtUp.res.mjs +12 -7
  26. package/src/FetchState.res +2 -0
  27. package/src/FetchState.res.mjs +16 -5
  28. package/src/HandlerLoader.res +2 -0
  29. package/src/HandlerLoader.res.mjs +2 -0
  30. package/src/HandlerRegister.res +48 -1
  31. package/src/HandlerRegister.res.mjs +40 -1
  32. package/src/HandlerRegister.resi +4 -0
  33. package/src/Hasura.res +52 -0
  34. package/src/Hasura.res.mjs +40 -2
  35. package/src/IndexerState.res +109 -1
  36. package/src/IndexerState.res.mjs +111 -7
  37. package/src/IndexerState.resi +6 -0
  38. package/src/Internal.res +51 -0
  39. package/src/Internal.res.mjs +12 -4
  40. package/src/Main.res +51 -0
  41. package/src/Main.res.mjs +34 -4
  42. package/src/Metrics.res +186 -0
  43. package/src/Metrics.res.mjs +153 -1
  44. package/src/Persistence.res +10 -13
  45. package/src/Persistence.res.mjs +4 -5
  46. package/src/PgStorage.res +906 -2160
  47. package/src/PgStorage.res.mjs +669 -1274
  48. package/src/PgWriting.res +36 -0
  49. package/src/PgWriting.res.mjs +34 -0
  50. package/src/PruneStaleHistory.res +1 -6
  51. package/src/PruneStaleHistory.res.mjs +1 -2
  52. package/src/Reading.res +238 -0
  53. package/src/Reading.res.mjs +197 -0
  54. package/src/Server.res +34 -2
  55. package/src/Server.res.mjs +28 -2
  56. package/src/SimulateDeadInputTracker.res +2 -3
  57. package/src/SimulateDeadInputTracker.res.mjs +6 -4
  58. package/src/Staging.res +43 -22
  59. package/src/Staging.res.mjs +17 -11
  60. package/src/Supervisor.res +46 -8
  61. package/src/Supervisor.res.mjs +48 -11
  62. package/src/TestIndexer.res +63 -6
  63. package/src/TestIndexer.res.mjs +45 -6
  64. package/src/UserContext.res +152 -0
  65. package/src/UserContext.res.mjs +138 -0
  66. package/src/Utils.res +13 -46
  67. package/src/Utils.res.mjs +6 -27
  68. package/src/Worker.res +8 -1
  69. package/src/Worker.res.mjs +8 -2
  70. package/src/Writing.res +25 -0
  71. package/src/Writing.res.mjs +6 -1
  72. package/src/bindings/ClickHouseSink.res +1 -1
  73. package/src/bindings/ClickHouseSink.res.mjs +1 -1
  74. package/src/bindings/Express.res +9 -0
  75. package/src/bindings/NodeJs.res +6 -0
  76. package/src/bindings/PgClient.res +360 -0
  77. package/src/bindings/PgClient.res.mjs +53 -0
  78. package/src/db/CheckpointSequence.res +0 -44
  79. package/src/db/CheckpointSequence.res.mjs +0 -37
  80. package/src/db/EntityFilter.res +3 -2
  81. package/src/db/EntityFilter.res.mjs +10 -2
  82. package/src/db/EntityHistory.res +0 -181
  83. package/src/db/EntityHistory.res.mjs +1 -100
  84. package/src/db/IndexDefinition.res +21 -105
  85. package/src/db/IndexDefinition.res.mjs +40 -111
  86. package/src/db/InternalTable.res +5 -820
  87. package/src/db/InternalTable.res.mjs +8 -649
  88. package/src/db/LoadCondition.res +31 -0
  89. package/src/db/LoadCondition.res.mjs +2 -0
  90. package/src/db/LoadFilter.res +127 -0
  91. package/src/db/LoadFilter.res.mjs +177 -0
  92. package/src/db/Sql.res +103 -0
  93. package/src/db/Sql.res.mjs +111 -0
  94. package/src/db/Table.res +52 -165
  95. package/src/db/Table.res.mjs +147 -131
  96. package/src/sidecar/ChildOutput.res +19 -0
  97. package/src/sidecar/ChildOutput.res.mjs +28 -0
  98. package/src/sidecar/ResolverActions.res +27 -0
  99. package/src/sidecar/ResolverActions.res.mjs +17 -0
  100. package/src/sidecar/ServeProcess.res +162 -0
  101. package/src/sidecar/ServeProcess.res.mjs +223 -0
  102. package/src/sidecar/SidecarCall.res +13 -0
  103. package/src/sidecar/SidecarCall.res.mjs +2 -0
  104. package/src/sidecar/SidecarContract.res +77 -0
  105. package/src/sidecar/SidecarContract.res.mjs +85 -0
  106. package/src/sidecar/SidecarProcess.res +220 -0
  107. package/src/sidecar/SidecarProcess.res.mjs +266 -0
  108. package/src/sidecar/SidecarSender.res +590 -0
  109. package/src/sidecar/SidecarSender.res.mjs +666 -0
  110. package/src/sidecar/WebhookListener.res +265 -0
  111. package/src/sidecar/WebhookListener.res.mjs +288 -0
  112. package/src/sidecar/client.d.ts +107 -0
  113. package/src/sidecar/client.js +443 -0
  114. package/src/sidecar/contract.d.ts +67 -0
  115. package/src/sidecar/contract.js +62 -0
  116. package/src/sidecar/emit.d.ts +105 -0
  117. package/src/sidecar/emit.js +284 -0
  118. package/src/sidecar/env.d.ts +75 -0
  119. package/src/sidecar/env.js +120 -0
  120. package/src/sidecar/index.d.ts +8 -0
  121. package/src/sidecar/index.js +8 -0
  122. package/src/sidecar/log.d.ts +23 -0
  123. package/src/sidecar/log.js +50 -0
  124. package/src/sidecar/metrics.d.ts +16 -0
  125. package/src/sidecar/metrics.js +80 -0
  126. package/src/sidecar/resolvers.d.ts +104 -0
  127. package/src/sidecar/resolvers.js +111 -0
  128. package/src/sidecar/runtime.d.ts +168 -0
  129. package/src/sidecar/runtime.js +727 -0
  130. package/src/sidecar/sql.d.ts +60 -0
  131. package/src/sidecar/sql.js +263 -0
  132. package/src/tui/Tui.res +14 -4
  133. package/src/tui/Tui.res.mjs +18 -7
  134. package/svm.schema.json +96 -0
  135. package/src/bindings/Postgres.res +0 -131
  136. package/src/bindings/Postgres.res.mjs +0 -20
  137. package/src/db/IndexCatalog.res +0 -248
  138. package/src/db/IndexCatalog.res.mjs +0 -222
  139. package/src/db/IndexManager.res +0 -145
  140. package/src/db/IndexManager.res.mjs +0 -130
package/evm.schema.json CHANGED
@@ -56,6 +56,40 @@
56
56
  "null"
57
57
  ]
58
58
  },
59
+ "sidecar": {
60
+ "description": "A sidecar: your own long-running TypeScript process, run next to the indexer by `envio sidecar` (and started by `envio dev`). Handlers call its functions through `context.sidecar.<name>(input)` without waiting.",
61
+ "anyOf": [
62
+ {
63
+ "$ref": "#/$defs/SidecarConfig"
64
+ },
65
+ {
66
+ "type": "null"
67
+ }
68
+ ]
69
+ },
70
+ "resolvers": {
71
+ "description": "Extra root query fields in the GraphQL API, answered by your sidecar or a webhook. Each one is published as a Hasura action, so Hasura and envio serve call the handler with Hasura's action payload.",
72
+ "type": [
73
+ "object",
74
+ "null"
75
+ ],
76
+ "additionalProperties": {
77
+ "$ref": "#/$defs/ResolverConfig"
78
+ }
79
+ },
80
+ "types": {
81
+ "description": "Output object types for `resolvers:`: type name to a map of field name to GraphQL type string (for example `address: \"String!\"`).",
82
+ "type": [
83
+ "object",
84
+ "null"
85
+ ],
86
+ "additionalProperties": {
87
+ "type": "object",
88
+ "additionalProperties": {
89
+ "type": "string"
90
+ }
91
+ }
92
+ },
59
93
  "ecosystem": {
60
94
  "description": "Ecosystem of the project.",
61
95
  "anyOf": [
@@ -273,6 +307,68 @@
273
307
  }
274
308
  ]
275
309
  },
310
+ "SidecarConfig": {
311
+ "type": "object",
312
+ "properties": {
313
+ "contract": {
314
+ "description": "Relative path to the contract module: `export default defineSidecar({ functions, webhooks })`. Schemas only, since both the indexer and the sidecar load it. Types `context.sidecar`.",
315
+ "type": "string"
316
+ },
317
+ "entry": {
318
+ "description": "Relative path to the sidecar entry module, which default-exports the `createSidecar(contract, {...})` result. Keep it outside the handlers directory, which the indexer auto-loads.",
319
+ "type": "string"
320
+ }
321
+ },
322
+ "additionalProperties": false,
323
+ "required": [
324
+ "contract",
325
+ "entry"
326
+ ]
327
+ },
328
+ "ResolverConfig": {
329
+ "type": "object",
330
+ "properties": {
331
+ "description": {
332
+ "description": "Shown as the field's description in the GraphQL schema.",
333
+ "type": [
334
+ "string",
335
+ "null"
336
+ ]
337
+ },
338
+ "args": {
339
+ "description": "Argument name to GraphQL type string, for example `limit: \"Int!\"`. Scalars or lists of scalars.",
340
+ "type": [
341
+ "object",
342
+ "null"
343
+ ],
344
+ "additionalProperties": {
345
+ "type": "string"
346
+ }
347
+ },
348
+ "output": {
349
+ "description": "GraphQL type string of the result, for example `\"[TopTrader!]!\"`. Scalars or types declared under `types:`.",
350
+ "type": "string"
351
+ },
352
+ "handler": {
353
+ "description": "`sidecar` (answered by the `resolvers` of your `createSidecar`) or an http(s):// URL that accepts Hasura's action payload.",
354
+ "type": "string"
355
+ },
356
+ "timeoutMs": {
357
+ "description": "How long the GraphQL layer waits for the handler. (default: 30000)",
358
+ "type": [
359
+ "integer",
360
+ "null"
361
+ ],
362
+ "format": "uint64",
363
+ "minimum": 0
364
+ }
365
+ },
366
+ "additionalProperties": false,
367
+ "required": [
368
+ "output",
369
+ "handler"
370
+ ]
371
+ },
276
372
  "EcosystemTag": {
277
373
  "type": "string",
278
374
  "enum": [
package/fuel.schema.json CHANGED
@@ -56,6 +56,40 @@
56
56
  "null"
57
57
  ]
58
58
  },
59
+ "sidecar": {
60
+ "description": "A sidecar: your own long-running TypeScript process, run next to the indexer by `envio sidecar` (and started by `envio dev`). Handlers call its functions through `context.sidecar.<name>(input)` without waiting.",
61
+ "anyOf": [
62
+ {
63
+ "$ref": "#/$defs/SidecarConfig"
64
+ },
65
+ {
66
+ "type": "null"
67
+ }
68
+ ]
69
+ },
70
+ "resolvers": {
71
+ "description": "Extra root query fields in the GraphQL API, answered by your sidecar or a webhook. Each one is published as a Hasura action, so Hasura and envio serve call the handler with Hasura's action payload.",
72
+ "type": [
73
+ "object",
74
+ "null"
75
+ ],
76
+ "additionalProperties": {
77
+ "$ref": "#/$defs/ResolverConfig"
78
+ }
79
+ },
80
+ "types": {
81
+ "description": "Output object types for `resolvers:`: type name to a map of field name to GraphQL type string (for example `address: \"String!\"`).",
82
+ "type": [
83
+ "object",
84
+ "null"
85
+ ],
86
+ "additionalProperties": {
87
+ "type": "object",
88
+ "additionalProperties": {
89
+ "type": "string"
90
+ }
91
+ }
92
+ },
59
93
  "ecosystem": {
60
94
  "description": "Ecosystem of the project.",
61
95
  "$ref": "#/$defs/EcosystemTag"
@@ -231,6 +265,68 @@
231
265
  }
232
266
  ]
233
267
  },
268
+ "SidecarConfig": {
269
+ "type": "object",
270
+ "properties": {
271
+ "contract": {
272
+ "description": "Relative path to the contract module: `export default defineSidecar({ functions, webhooks })`. Schemas only, since both the indexer and the sidecar load it. Types `context.sidecar`.",
273
+ "type": "string"
274
+ },
275
+ "entry": {
276
+ "description": "Relative path to the sidecar entry module, which default-exports the `createSidecar(contract, {...})` result. Keep it outside the handlers directory, which the indexer auto-loads.",
277
+ "type": "string"
278
+ }
279
+ },
280
+ "additionalProperties": false,
281
+ "required": [
282
+ "contract",
283
+ "entry"
284
+ ]
285
+ },
286
+ "ResolverConfig": {
287
+ "type": "object",
288
+ "properties": {
289
+ "description": {
290
+ "description": "Shown as the field's description in the GraphQL schema.",
291
+ "type": [
292
+ "string",
293
+ "null"
294
+ ]
295
+ },
296
+ "args": {
297
+ "description": "Argument name to GraphQL type string, for example `limit: \"Int!\"`. Scalars or lists of scalars.",
298
+ "type": [
299
+ "object",
300
+ "null"
301
+ ],
302
+ "additionalProperties": {
303
+ "type": "string"
304
+ }
305
+ },
306
+ "output": {
307
+ "description": "GraphQL type string of the result, for example `\"[TopTrader!]!\"`. Scalars or types declared under `types:`.",
308
+ "type": "string"
309
+ },
310
+ "handler": {
311
+ "description": "`sidecar` (answered by the `resolvers` of your `createSidecar`) or an http(s):// URL that accepts Hasura's action payload.",
312
+ "type": "string"
313
+ },
314
+ "timeoutMs": {
315
+ "description": "How long the GraphQL layer waits for the handler. (default: 30000)",
316
+ "type": [
317
+ "integer",
318
+ "null"
319
+ ],
320
+ "format": "uint64",
321
+ "minimum": 0
322
+ }
323
+ },
324
+ "additionalProperties": false,
325
+ "required": [
326
+ "output",
327
+ "handler"
328
+ ]
329
+ },
234
330
  "EcosystemTag": {
235
331
  "type": "string",
236
332
  "enum": [
package/index.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import * as Sury from "rescript-schema";
2
2
  import type { default as BigDecimalT } from "bignumber.js";
3
+ import type {
4
+ SidecarCaller,
5
+ SidecarContract,
6
+ SidecarFunctionInput,
7
+ SidecarFunctionName,
8
+ SidecarWebhookBody,
9
+ SidecarWebhookName,
10
+ } from "./src/sidecar/contract.js";
3
11
  export { default as BigDecimal } from "bignumber.js";
4
12
 
5
13
  // Runtime value stubs used by the `S.*` namespace declarations further down
@@ -307,6 +315,65 @@ type GlobalConfig = Global extends { config: infer C extends IndexerConfigTypes
307
315
  ? C
308
316
  : {};
309
317
 
318
+ /** `context.sidecar` for the `sidecar.contract` module codegen binds. The
319
+ * contract is matched with `infer C extends SidecarContract`: testing an alias
320
+ * with `extends SidecarContract` and passing it on can hand `SidecarCaller`
321
+ * its intersection with `SidecarContract`, whose index signature lets any
322
+ * function name through. */
323
+ type GlobalSidecarCaller = Global extends { sidecar: infer C extends SidecarContract }
324
+ ? SidecarCaller<C>
325
+ : NotConfigured<"context.sidecar", "Add `sidecar: { contract, entry }`">;
326
+
327
+ /** The webhooks the indexer itself posts about `context.sidecar` calls. */
328
+ export type SidecarBuiltinWebhookName = "sidecar.ack" | "sidecar.dropped";
329
+
330
+ /** A name `indexer.onWebhook` accepts: the contract's webhooks, the built-ins,
331
+ * or any other name `emit.raw` may post. The contract is inferred where it is
332
+ * used, as for `GlobalSidecarCaller`, so its index signature never leaks in. */
333
+ export type WebhookName =
334
+ | (Global extends { sidecar: infer C extends SidecarContract } ? SidecarWebhookName<C> : never)
335
+ | SidecarBuiltinWebhookName
336
+ | (string & {});
337
+
338
+ /** The call a built-in webhook is about, with its input decoded. */
339
+ type SidecarCallRefOf<C extends SidecarContract> = {
340
+ [K in SidecarFunctionName<C>]: {
341
+ readonly callId: string;
342
+ readonly fn: K;
343
+ readonly input: SidecarFunctionInput<C, K>;
344
+ };
345
+ }[SidecarFunctionName<C>];
346
+
347
+ type WebhookBodyFrom<C extends SidecarContract, N extends string> = N extends "sidecar.ack"
348
+ ? SidecarCallRefOf<C>
349
+ : N extends "sidecar.dropped"
350
+ ? SidecarCallRefOf<C> & { readonly reason: string }
351
+ : N extends SidecarWebhookName<C>
352
+ ? SidecarWebhookBody<C, N>
353
+ : unknown;
354
+
355
+ type WebhookBodyOf<N extends string> = Global extends { sidecar: infer C extends SidecarContract }
356
+ ? WebhookBodyFrom<C, N>
357
+ : N extends SidecarBuiltinWebhookName
358
+ ? { readonly callId: string; readonly fn: string; readonly input: unknown; readonly reason?: string }
359
+ : unknown;
360
+
361
+ /** What an `indexer.onWebhook` handler is handed about the webhook. */
362
+ export type OnWebhookEvent<N extends string = string> = {
363
+ readonly name: N;
364
+ /** Decoded with the contract's `webhooks.<name>.body` schema. */
365
+ readonly body: WebhookBodyOf<N>;
366
+ /** The id the listener answered the sidecar with. */
367
+ readonly id: string;
368
+ readonly chainId: number;
369
+ /** When the listener accepted it, in milliseconds since the epoch. */
370
+ readonly receivedAt: number;
371
+ readonly idempotencyKey?: string;
372
+ /** The block the webhook's writes are recorded at: where its chain's batch
373
+ * left the chain. */
374
+ readonly block: { readonly number: number; readonly timestamp?: number };
375
+ };
376
+
310
377
  /** Error-message string returned by project-bound aliases when codegen has
311
378
  * not run yet. Resolves to `string` so handler signatures stay assignable.
312
379
  * Wording is package-manager-neutral — `envio init` lets users pick pnpm,
@@ -581,6 +648,10 @@ type BaseHandlerContext<Config extends IndexerConfigTypes = GlobalConfig, ChainI
581
648
  readonly id: ChainId;
582
649
  readonly isRealtime: boolean;
583
650
  };
651
+ /** Call the sidecar declared by `sidecar:` in config.yaml. A call returns its
652
+ * id synchronously, is skipped in preload, and is delivered after the batch
653
+ * commits. */
654
+ readonly sidecar: GlobalSidecarCaller;
584
655
  } & {
585
656
  readonly [K in keyof ConfigEntities<Config>]: EntityOperations<ConfigEntities<Config>[K]>;
586
657
  };
@@ -1861,6 +1932,20 @@ export type IndexerFromConfig<Config extends IndexerConfigTypes = GlobalConfig>
1861
1932
  readonly rollbackToBlock: number;
1862
1933
  }) => Promise<void>,
1863
1934
  ) => void;
1935
+ /**
1936
+ * Handle a webhook the sidecar posts with `emit.<name>(body)`, or a
1937
+ * built-in one about a `context.sidecar` call (`sidecar.ack`,
1938
+ * `sidecar.dropped`). It runs once, in the next batch of its chain after
1939
+ * that batch's events, with a normal handler context. A webhook undone by
1940
+ * a reorg is not replayed.
1941
+ */
1942
+ readonly onWebhook: <const N extends WebhookName>(
1943
+ options: { readonly name: N },
1944
+ handler: (args: {
1945
+ readonly webhook: OnWebhookEvent<N>;
1946
+ readonly context: BaseHandlerContext<Config, number>;
1947
+ }) => Promise<void>,
1948
+ ) => void;
1864
1949
  } & SingleEcosystemChains<Config>
1865
1950
  >;
1866
1951
 
@@ -2066,6 +2151,11 @@ type EntityChange<Config extends IndexerConfigTypes = GlobalConfig> = {
2066
2151
  readonly addresses?: {
2067
2152
  readonly sets?: readonly AddressRegistration[];
2068
2153
  };
2154
+ /** `context.sidecar` calls made in this block, in call order. `input` is the
2155
+ * JSON form the sidecar receives (bigint as a string). */
2156
+ readonly sidecar?: {
2157
+ readonly calls: readonly { readonly id: string; readonly fn: string; readonly input: unknown }[];
2158
+ };
2069
2159
  } & {
2070
2160
  readonly [K in keyof ConfigEntities<Config>]?: EntityChangeValue<
2071
2161
  ConfigEntities<Config>[K]
@@ -2209,6 +2299,32 @@ type SingleEcosystemTestChains<Config extends IndexerConfigTypes = GlobalConfig>
2209
2299
  ? SvmTestEcosystem<Config>
2210
2300
  : CodegenRequiredFallback;
2211
2301
 
2302
+ /** A `context.sidecar` call as the test indexer records it, with `input` as the
2303
+ * handler passed it (a bigint stays a bigint). */
2304
+ export type TestIndexerSidecarCall<C extends SidecarContract> = {
2305
+ [K in SidecarFunctionName<C>]: {
2306
+ readonly id: string;
2307
+ readonly fn: K;
2308
+ readonly chainId: number;
2309
+ readonly blockNumber: number;
2310
+ /** Absent for block handlers. */
2311
+ readonly blockTimestamp?: number;
2312
+ readonly input: SidecarFunctionInput<C, K>;
2313
+ };
2314
+ }[SidecarFunctionName<C>];
2315
+
2316
+ /** `indexer.sidecar` on a test indexer. The test indexer never talks to a
2317
+ * sidecar: it records the calls instead. */
2318
+ type TestIndexerSidecar = Global extends { sidecar: infer C extends SidecarContract }
2319
+ ? {
2320
+ /** Every call the handlers made, across `process` runs, in call order.
2321
+ * Calls made during preload are not recorded. */
2322
+ readonly calls: readonly TestIndexerSidecarCall<C>[];
2323
+ /** Forget the recorded calls. */
2324
+ readonly clear: () => void;
2325
+ }
2326
+ : NotConfigured<"indexer.sidecar", "Add `sidecar: { contract, entry }`">;
2327
+
2212
2328
  /**
2213
2329
  * Test indexer type resolved from config.
2214
2330
  * Allows running the indexer for specific block ranges and inspecting results.
@@ -2221,6 +2337,8 @@ export type TestIndexerFromConfig<Config extends IndexerConfigTypes = GlobalConf
2221
2337
  /** Changes happened during the processing. */
2222
2338
  readonly changes: readonly EntityChange<Config>[];
2223
2339
  }>;
2340
+ /** The `context.sidecar` calls the handlers made. */
2341
+ readonly sidecar: TestIndexerSidecar;
2224
2342
  } & SingleEcosystemTestChains<Config> & {
2225
2343
  /** Entity operations for direct manipulation outside of handlers. */
2226
2344
  readonly [K in keyof ConfigEntities<Config>]: TestIndexerEntityOperations<
@@ -2306,3 +2424,14 @@ export const indexer: Indexer;
2306
2424
 
2307
2425
  /** Construct a {@link TestIndexer} for use in unit tests. */
2308
2426
  export const createTestIndexer: () => TestIndexer;
2427
+
2428
+ // ============== Sidecars ==============
2429
+
2430
+ export {
2431
+ defineSidecar,
2432
+ type SidecarContract,
2433
+ type SidecarContractDef,
2434
+ type SidecarCaller,
2435
+ type SidecarFunctionInput,
2436
+ type SidecarWebhookBody,
2437
+ } from "./src/sidecar/contract.js";
package/index.js CHANGED
@@ -46,3 +46,5 @@ export const S = {
46
46
  assertOrThrow: Sury.assertOrThrow,
47
47
  parseOrThrow: Sury.parseOrThrow,
48
48
  };
49
+
50
+ export { defineSidecar } from "./src/sidecar/contract.js";
package/package.json CHANGED
@@ -1,11 +1,23 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.14.1",
3
+ "version": "3.15.0-sidecars.2",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
7
7
  "main": "./index.js",
8
8
  "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "default": "./index.js"
13
+ },
14
+ "./sidecar": {
15
+ "types": "./sidecar.d.ts",
16
+ "default": "./sidecar.js"
17
+ },
18
+ "./package.json": "./package.json",
19
+ "./*": "./*"
20
+ },
9
21
  "repository": {
10
22
  "type": "git",
11
23
  "url": "git+https://github.com/enviodev/hyperindex.git"
@@ -37,27 +49,29 @@
37
49
  "rescript.json",
38
50
  "index.d.ts",
39
51
  "index.js",
52
+ "sidecar.d.ts",
53
+ "sidecar.js",
40
54
  "src",
41
55
  "licenses"
42
56
  ],
43
57
  "dependencies": {
44
58
  "@elastic/ecs-pino-format": "1.4.0",
59
+ "@rescript/runtime": "12.2.0",
45
60
  "bignumber.js": "9.3.1",
61
+ "dotenv": "16.4.5",
46
62
  "eventsource": "4.1.0",
47
63
  "express": "4.19.2",
48
64
  "pino": "10.3.1",
49
65
  "pino-pretty": "13.1.3",
50
- "@rescript/runtime": "12.2.0",
66
+ "postgres": "3.4.8",
51
67
  "rescript-schema": "9.5.1",
52
- "viem": "2.54.0",
53
- "dotenv": "16.4.5",
54
- "postgres": "3.4.8"
68
+ "viem": "2.54.0"
55
69
  },
56
70
  "optionalDependencies": {
57
- "envio-linux-x64": "3.14.1",
58
- "envio-linux-x64-musl": "3.14.1",
59
- "envio-linux-arm64": "3.14.1",
60
- "envio-darwin-x64": "3.14.1",
61
- "envio-darwin-arm64": "3.14.1"
71
+ "envio-linux-x64": "3.15.0-sidecars.2",
72
+ "envio-linux-x64-musl": "3.15.0-sidecars.2",
73
+ "envio-linux-arm64": "3.15.0-sidecars.2",
74
+ "envio-darwin-x64": "3.15.0-sidecars.2",
75
+ "envio-darwin-arm64": "3.15.0-sidecars.2"
62
76
  }
63
77
  }
package/sidecar.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/sidecar/index.js";
package/sidecar.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/sidecar/index.js";
package/src/Batch.res CHANGED
@@ -14,6 +14,8 @@ type chainAfterBatch = {
14
14
  totalEventsProcessed: float,
15
15
  fetchState: FetchState.t,
16
16
  isProgressAtHeadWhenBatchCreated: bool,
17
+ // Taken off the chain's webhook queue.
18
+ webhooksCount?: int,
17
19
  }
18
20
 
19
21
  // A per-chain snapshot of the scanned block hashes still inside the reorg
@@ -38,6 +40,7 @@ type chainBeforeBatch = {
38
40
  sourceBlockNumber: int,
39
41
  totalEventsProcessed: float,
40
42
  chainConfig: Config.chain,
43
+ pendingWebhooks: array<Internal.pendingWebhook>,
41
44
  }
42
45
 
43
46
  type t = {
@@ -58,6 +61,9 @@ type t = {
58
61
  // event, however many items it made.
59
62
  checkpointEventsProcessed: array<int>,
60
63
  registeredAddresses: array<AddressRows.staged>,
64
+ // `context.sidecar` calls the handlers made, in call order. Handed to the
65
+ // sidecar only once the batch has committed.
66
+ sidecarCalls: array<SidecarCall.t>,
61
67
  }
62
68
 
63
69
  // What a chain contributed to the batch: items taken off its buffer, and the
@@ -70,11 +76,12 @@ let getProgressedChainsById = {
70
76
  ~progressBlockNumberAfterBatch,
71
77
  ~fetchStateAfterBatch,
72
78
  ~counts: chainBatchCounts,
79
+ ~webhooksCount,
73
80
  ) => {
74
81
  // The check is sufficient, since we guarantee to include a full block in a batch
75
82
  // Also, this might be true even if batchSize is 0,
76
83
  // eg when indexing at the head and chain doesn't have items in a block
77
- if chainBeforeBatch.progressBlockNumber < progressBlockNumberAfterBatch {
84
+ if chainBeforeBatch.progressBlockNumber < progressBlockNumberAfterBatch || webhooksCount > 0 {
78
85
  Some(
79
86
  (
80
87
  {
@@ -85,8 +92,13 @@ let getProgressedChainsById = {
85
92
  totalEventsProcessed: chainBeforeBatch.totalEventsProcessed +.
86
93
  counts.eventsProcessed->Int.toFloat,
87
94
  fetchState: fetchStateAfterBatch,
88
- isProgressAtHeadWhenBatchCreated: progressBlockNumberAfterBatch >=
89
- chainBeforeBatch.sourceBlockNumber - chainBeforeBatch.chainConfig.blockLag,
95
+ // A batch of webhooks alone can come before the source has
96
+ // answered the height, while it still reads 0: nothing is at a
97
+ // head that isn't known yet.
98
+ isProgressAtHeadWhenBatchCreated: chainBeforeBatch.sourceBlockNumber > 0 &&
99
+ progressBlockNumberAfterBatch >=
100
+ chainBeforeBatch.sourceBlockNumber - chainBeforeBatch.chainConfig.blockLag,
101
+ webhooksCount,
90
102
  }: chainAfterBatch
91
103
  ),
92
104
  )
@@ -99,6 +111,7 @@ let getProgressedChainsById = {
99
111
  ~chainsBeforeBatch: dict<chainBeforeBatch>,
100
112
  ~countsPerChain: dict<chainBatchCounts>,
101
113
  ~progressBlockNumberPerChain: dict<int>,
114
+ ~webhooksCountPerChain: dict<int>,
102
115
  ) => {
103
116
  let progressedChainsById = Dict.make()
104
117
 
@@ -117,6 +130,10 @@ let getProgressedChainsById = {
117
130
  | Some(progressBlockNumber) => progressBlockNumber
118
131
  | None => chainBeforeBatch.progressBlockNumber
119
132
  }
133
+ let webhooksCount =
134
+ webhooksCountPerChain
135
+ ->Utils.Dict.dangerouslyGetNonOption(fetchState.chainId->ChainId.toString)
136
+ ->Option.getOr(0)
120
137
 
121
138
  switch switch countsPerChain->Utils.Dict.dangerouslyGetNonOption(
122
139
  fetchState.chainId->ChainId.toString,
@@ -128,6 +145,7 @@ let getProgressedChainsById = {
128
145
  ~counts,
129
146
  ~fetchStateAfterBatch=fetchState->FetchState.updateInternal(~mutItems=leftItems),
130
147
  ~progressBlockNumberAfterBatch,
148
+ ~webhooksCount,
131
149
  )
132
150
  // Skip not affected chains
133
151
  | None =>
@@ -136,6 +154,7 @@ let getProgressedChainsById = {
136
154
  ~counts={size: 0, eventsProcessed: 0},
137
155
  ~fetchStateAfterBatch=chainBeforeBatch.fetchState,
138
156
  ~progressBlockNumberAfterBatch,
157
+ ~webhooksCount,
139
158
  )
140
159
  } {
141
160
  | Some(progressedChain) =>
@@ -230,6 +249,7 @@ let make = (
230
249
  let cursor = sequence->CheckpointSequence.cursor(~frontier)
231
250
  let mutCountsPerChain = Dict.make()
232
251
  let mutProgressBlockNumberPerChain = Dict.make()
252
+ let mutWebhooksCountPerChain = Dict.make()
233
253
 
234
254
  let items = []
235
255
  let checkpointIds = []
@@ -263,8 +283,7 @@ let make = (
263
283
  // The buffer is sorted, so a log's items are consecutive: the first of
264
284
  // them is the only one that counts as an event.
265
285
  let isNewEvent =
266
- idx === 0 ||
267
- !(fetchState.buffer->Array.getUnsafe(idx - 1)->FetchState.isSameLog(item))
286
+ idx === 0 || !(fetchState.buffer->Array.getUnsafe(idx - 1)->FetchState.isSameLog(item))
268
287
  if isNewEvent {
269
288
  chainEventsProcessed := chainEventsProcessed.contents + 1
270
289
  }
@@ -345,6 +364,56 @@ let make = (
345
364
  ~mutCheckpointEventsProcessed=checkpointEventsProcessed,
346
365
  )
347
366
 
367
+ let webhooksCount = chainBeforeBatch.pendingWebhooks->Array.length
368
+ if webhooksCount > 0 {
369
+ let lastIndex = checkpointIds->Array.length - 1
370
+
371
+ // On a checkpoint of their own block, so a rollback to below it undoes
372
+ // them along with it, and one to it or above keeps them.
373
+ if (
374
+ lastIndex >= 0 &&
375
+ checkpointChainIds->Array.getUnsafe(lastIndex) === fetchState.chainId &&
376
+ checkpointBlockNumbers->Array.getUnsafe(lastIndex) === progressBlockNumberAfterBatch
377
+ ) {
378
+ checkpointItemsCount->Array.setUnsafe(
379
+ lastIndex,
380
+ checkpointItemsCount->Array.getUnsafe(lastIndex) + webhooksCount,
381
+ )
382
+ } else {
383
+ checkpointIds
384
+ ->Array.push(cursor->CheckpointSequence.next(~chainId=fetchState.chainId))
385
+ ->ignore
386
+ checkpointChainIds->Array.push(fetchState.chainId)->ignore
387
+ checkpointBlockNumbers->Array.push(progressBlockNumberAfterBatch)->ignore
388
+ checkpointBlockHashes
389
+ ->Array.push(
390
+ switch chainBeforeBatch.scannedHashes.hashByBlockNumber->Utils.Dict.dangerouslyGetByIntNonOption(
391
+ progressBlockNumberAfterBatch,
392
+ ) {
393
+ | Some(hash) => Null.Value(hash)
394
+ | None => Null.Null
395
+ },
396
+ )
397
+ ->ignore
398
+ checkpointItemsCount->Array.push(webhooksCount)->ignore
399
+ checkpointEventsProcessed->Array.push(0)->ignore
400
+ }
401
+ let blockTimestamp = chainBeforeBatch.blockTimeAt(progressBlockNumberAfterBatch)
402
+ chainBeforeBatch.pendingWebhooks->Array.forEach(pendingWebhook =>
403
+ items
404
+ ->Array.push(
405
+ Internal.Webhook({
406
+ pendingWebhook,
407
+ chainId: fetchState.chainId,
408
+ blockNumber: progressBlockNumberAfterBatch,
409
+ blockTimestamp,
410
+ }),
411
+ )
412
+ ->ignore
413
+ )
414
+ mutWebhooksCountPerChain->ChainId.Dict.set(fetchState.chainId, webhooksCount)
415
+ }
416
+
348
417
  mutProgressBlockNumberPerChain->ChainId.Dict.set(
349
418
  fetchState.chainId,
350
419
  progressBlockNumberAfterBatch,
@@ -360,6 +429,7 @@ let make = (
360
429
  ~chainsBeforeBatch,
361
430
  ~countsPerChain=mutCountsPerChain,
362
431
  ~progressBlockNumberPerChain=mutProgressBlockNumberPerChain,
432
+ ~webhooksCountPerChain=mutWebhooksCountPerChain,
363
433
  ),
364
434
  history,
365
435
  checkpointIds,
@@ -369,6 +439,7 @@ let make = (
369
439
  checkpointItemsCount,
370
440
  checkpointEventsProcessed,
371
441
  registeredAddresses: [],
442
+ sidecarCalls: [],
372
443
  }
373
444
  }
374
445
 
@@ -390,7 +461,7 @@ let findFirstEventBlockNumber = (batch: t, ~chainId) => {
390
461
  let checkpointChainId = batch.checkpointChainIds->Array.getUnsafe(idx.contents)
391
462
  if (
392
463
  checkpointChainId === chainId &&
393
- batch.checkpointItemsCount->Array.getUnsafe(idx.contents) > 0
464
+ batch.checkpointEventsProcessed->Array.getUnsafe(idx.contents) > 0
394
465
  ) {
395
466
  result := Some(batch.checkpointBlockNumbers->Array.getUnsafe(idx.contents))
396
467
  } else {
@@ -414,7 +485,7 @@ let findLastEventItem = (batch: t, ~chainId) => {
414
485
  idx := idx.contents - 1
415
486
  }
416
487
  }
417
- | Internal.Block(_) => idx := idx.contents - 1
488
+ | Internal.Block(_) | Internal.Webhook(_) => idx := idx.contents - 1
418
489
  }
419
490
  }
420
491
  result.contents