Skip to content

Verify @graphrefly/pure-ts runs on Hermes (React Native) — spike #4

@clfhhc

Description

@clfhhc

Context

memo:Re (cognitive-buddy) is shaping up to be the first concrete consumer of @graphrefly/pure-ts targeting React Native / Hermes runtime. While pure-ts is designed as universal (no node:* imports, no DOM globals), it has never been verified to actually run on Hermes — Hermes has historical quirks around ES features (the LangGraph.js RN integration required AsyncLocalStorage + crypto polyfills, per memo:Re's architecture doc).

This issue tracks a spike to verify basic primitives work, and to document any required polyfills as part of @graphrefly/pure-ts's supported-runtime claim.

Spike scope

API note: the original draft used state() / derived() / .set() — that was illustrative pseudo-code. pure-ts 0.45.0's one public primitive is node(); state / derived exist only as Graph methods, not standalone exports. The faithful translation below is what is actually run. Canonical implementation: scripts/hermes-smoke/spike-core.mjs.

import { node, DATA } from "@graphrefly/pure-ts";

// Manual source  = node([], undefined, { initial })
// Derived        = node(deps, fn) emitting via actions.emit; fn's
//                  `data[i]` is dep i's wave batch, ctx.prevData[i]
//                  its last value when it didn't emit this wave.
// subscribe(sink) receives message batches: [tier, value].
const mkState = (initial) => node([], undefined, { initial });
const mkDerived = (deps, compute) =>
  node(deps, (data, actions, ctx) => {
    const args = deps.map((_, i) => {
      const b = data[i];
      return b?.length ? b[b.length - 1] : ctx.prevData[i];
    });
    actions.emit(compute(...args));
  });
const onData = (n, cb) =>
  n.subscribe((msgs) => {
    for (const m of msgs) if (m[0] === DATA) cb(m[1]);
  });

// Basic propagation
const a = mkState(1);
const b = mkState(2);
const sum = mkDerived([a, b], (x, y) => x + y);
onData(sum, (v) => console.log("[spike] sum =", v));  // expect: 3
a.emit(10);                                            // expect: 12
b.emit(20);                                            // expect: 30

// Diamond fan-in (cascade dedupe)
const a2 = mkState(1);
const b2 = mkDerived([a2], (x) => x * 2);
const c2 = mkDerived([a2, b2], (x, y) => x + y);
onData(c2, (v) => console.log("[spike] c2 =", v));     // expect: 3
a2.emit(5);                                            // expect: 15 (ONCE, not twice)

Acceptance criteria

  • Dev build (expo run:ios / expo run:android) — both blocks log expected values
  • Release build (Hermes bytecode + aggressive optimization) — same. Release sometimes breaks where dev passes.
  • Document any required polyfills (crypto.randomUUID, AsyncLocalStorage, globalThis, structuredClone, etc.) and where they're applied
  • Record tested versions: @graphrefly/pure-ts vX.Y.Z, Expo SDK version, RN version, Hermes commit/version

Outcome

If verified, add "React Native (Hermes)" to the supported-runtimes documentation in @graphrefly/pure-ts. If polyfills are required, file follow-up issues for native fixes vs documented host-side requirements.

Context / driver

Architecture design conversation on memo:Re using reactiveFactStore (DS-14.7) as its memory substrate. Per the design lock, pure-ts is the substrate peer for non-Node universal contexts; RN is the headline first target.

Verification now lives in graphrefly-ts as an ongoing indicator (two tiers — see the result comment below): per-commit engine smoke pnpm test:hermes (scripts/hermes-smoke/, in CI) + periodic on-device fixture apps/rn-hermes-fixture. Not a one-off in the memo:Re repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions