Skip to content

Commit 6b7c2d7

Browse files
DZakhclaude
andauthored
Use project's installed envio binary in e2e tests (#1157)
* Use project-installed envio in isolated dependency test After #1116 moved the CLI into in-process bin.mjs, running envio via the e2e-tests workspace's bin.mjs while the handler imports `generated` → `envio` resolved to the project's freshly installed envio produced two distinct envio module instances in one process. HandlerRegister state lives per-instance, so `indexer.onEvent` wrote to the project's dict while `applyRegistrations` read the e2e-tests dict, causing every event to surface as "no event handler" and chain 1 to fail with "Nothing to fetch". Invoke envio through `<projectDir>/node_modules/envio/bin.mjs` so bin.mjs and the handlers resolve to the same physical envio, matching the real `npx envio dev` flow. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Remove temporary PR build workflow The NAPI migration (#1116) is merged to main, so the stopgap `build_and_verify_pr.yml` (which ran on `pull_request` so PR-modified build steps were exercised) is no longer needed. `build_and_verify.yml` already covers PRs via `pull_request_target`. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Share HandlerRegister state via globalThis When the CLI's bin.mjs resolves envio from one path (e.g. a globally installed or test-resolved copy) and the user's handlers resolve `import "envio"` from a different path (e.g. their project's node_modules), Node treats them as two separate ESM module instances. HandlerRegister's module-local state then lives per-instance: the handler's `indexer.onEvent` writes to one dict while `HandlerLoader.applyRegistrations` reads the other, so every event ends up "without an event handler" and chains with no other registrations fail with "Nothing to fetch". Stash `eventRegistrations`, `activeRegistration`, and `preRegistered` on `globalThis` so all envio instances in a single process share one registry. This is the same trick `react` and `@apollo/client` use to guard against the "two copies of the package" footgun. Revert the install.test.ts workaround that routed `envio dev` through the project-local bin.mjs — with the globalThis fix, the original `config.envioCommand` invocation works. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Version-gate HandlerRegister globalThis registry Stashing the registry on unversioned globalThis keys would silently blend state if two envio majors ended up in one process (monorepo with divergent pins, a transitive dep shipping its own copy, an in-flight upgrade). The record shapes evolve between majors, so silent sharing would corrupt handlers/config reads later with opaque errors. Collapse the three globalThis slots into a single \`__envioRegistry\` object tagged with the envio package version. On load, if a registry already exists we accept it only when versions match; otherwise we throw with a deduplication hint pointing at the real problem (\`node_modules/envio\` duplication) rather than a random decode failure. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Rewrite HandlerRegister globalThis init in ReScript Drop the %raw IIFE that boxed the globalThis lookup + version check in a JS string, and replace it with a typed ReScript match. `globalThis` is now bound via `@val external` with an open `{..}` type so bracket access (\`globalThis["__envioRegistry"]\`) stays syntactically light; the slot's real shape is encoded as a typed record (\`registryShape\`). The version check becomes a plain pattern match with a guard. Behavior unchanged; generated JS is equivalent. Payoff is that the string-embedded JS no longer shadows types, and future edits (e.g. adding a field to the registry) are caught by the compiler. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Tighten globalThis external binding to a typed record Replace \`{..}\` with a \`mutable __envioRegistry\` record so both the lookup and the assignment are statically typed, and drop the local \`Nullable.t<registryShape>\` annotation. Typo in the slot name now surfaces at compile time instead of silently creating a new globalThis property. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT * Align version-gate comment with the strict-equality check Comment said shapes "evolve between envio majors" but the guard is full-version equality, so a patch/pre-release mismatch also throws. Keep the stricter check (safer default) and reword the comment so a future reader doesn't assume major-only matching. https://claude.ai/code/session_011rsp7McDiNshdkTLCnP6eT --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a7ca442 commit 6b7c2d7

2 files changed

Lines changed: 55 additions & 224 deletions

File tree

.github/workflows/build_and_verify_pr.yml

Lines changed: 0 additions & 212 deletions
This file was deleted.

packages/envio/src/HandlerRegister.res

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,59 @@ let empty = {
1010
eventOptions: None,
1111
}
1212

13-
let eventRegistrations: dict<eventRegistration> = Dict.make()
13+
type registrations = {onBlockByChainId: dict<array<Internal.onBlockConfig>>}
14+
15+
type activeRegistration = {
16+
ecosystem: Ecosystem.t,
17+
multichain: Internal.multichain,
18+
registrations: registrations,
19+
mutable finished: bool,
20+
}
21+
22+
// Stashed on `globalThis` so a duplicate envio module instance — e.g. when the
23+
// CLI's `bin.mjs` resolves envio from one path but the user's handlers resolve
24+
// it from `node_modules/envio` — shares one registry. Without this, each copy
25+
// keeps its own dict and `applyRegistrations` reads empty state.
26+
//
27+
// Version-gated: the record shapes below can evolve between envio versions,
28+
// so the guard uses strict full-version equality. On mismatch we throw with
29+
// a deduplication hint instead of silently mixing shapes across builds.
30+
type registryShape = {
31+
version: string,
32+
eventRegistrations: dict<eventRegistration>,
33+
activeRegistration: ref<option<activeRegistration>>,
34+
preRegistered: array<activeRegistration => unit>,
35+
}
36+
37+
// Record type with `mutable` so assignment typechecks; ReScript keeps the
38+
// field name verbatim in the generated JS so the globalThis slot is
39+
// `__envioRegistry`.
40+
type globalThis = {mutable __envioRegistry: Nullable.t<registryShape>}
41+
@val external globalThis: globalThis = "globalThis"
42+
43+
%%private(
44+
let registry: registryShape = {
45+
let version = Utils.EnvioPackage.value.version
46+
switch globalThis.__envioRegistry->Nullable.toOption {
47+
| Some(existing) if existing.version === version => existing
48+
| Some(existing) =>
49+
JsError.throwWithMessage(
50+
`Multiple incompatible envio versions loaded in the same process: ${existing.version} and ${version}. Deduplicate the 'envio' dependency in your project.`,
51+
)
52+
| None =>
53+
let fresh = {
54+
version,
55+
eventRegistrations: Dict.make(),
56+
activeRegistration: ref(None),
57+
preRegistered: [],
58+
}
59+
globalThis.__envioRegistry = Nullable.make(fresh)
60+
fresh
61+
}
62+
}
63+
)
64+
65+
let eventRegistrations = registry.eventRegistrations
1466

1567
let getKey = (~contractName, ~eventName) => contractName ++ "." ++ eventName
1668

@@ -25,16 +77,7 @@ let set = (~contractName, ~eventName, registration) => {
2577
eventRegistrations->Dict.set(getKey(~contractName, ~eventName), registration)
2678
}
2779

28-
type registrations = {onBlockByChainId: dict<array<Internal.onBlockConfig>>}
29-
30-
type activeRegistration = {
31-
ecosystem: Ecosystem.t,
32-
multichain: Internal.multichain,
33-
registrations: registrations,
34-
mutable finished: bool,
35-
}
36-
37-
let activeRegistration = ref(None)
80+
let activeRegistration = registry.activeRegistration
3881

3982
// Might happen for tests when the handler file
4083
// is imported by a non-envio process (eg mocha)
@@ -43,7 +86,7 @@ let activeRegistration = ref(None)
4386
// Theoretically we could keep preRegistration without an explicit start
4487
// but I want it to be this way, so for the actual indexer run
4588
// an error is thrown with the exact stack trace where the handler was registered.
46-
let preRegistered = []
89+
let preRegistered = registry.preRegistered
4790

4891
let withRegistration = (fn: activeRegistration => unit) => {
4992
switch activeRegistration.contents {

0 commit comments

Comments
 (0)