@sigx/lynx-runtime 0.4.0 → 0.4.1

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 (41) hide show
  1. package/dist/animated/animated-value.d.ts +2 -2
  2. package/dist/animated/animated-value.js +15 -0
  3. package/dist/animated/shared-value.d.ts +1 -1
  4. package/dist/animated/shared-value.js +94 -0
  5. package/dist/animated/use-animated-style.d.ts +3 -3
  6. package/dist/animated/use-animated-style.js +53 -0
  7. package/dist/animated-bridge.js +71 -0
  8. package/dist/bg-bridge.js +63 -0
  9. package/dist/event-registry.js +75 -0
  10. package/dist/flush.d.ts +1 -1
  11. package/dist/flush.js +8 -0
  12. package/dist/hmr.js +119 -39
  13. package/dist/index.d.ts +24 -22
  14. package/dist/index.js +37 -849
  15. package/dist/jsx.d.ts +29 -3
  16. package/dist/jsx.js +19 -0
  17. package/dist/main-thread-ref.js +134 -0
  18. package/dist/model-processor.js +76 -0
  19. package/dist/mt-hmr-bridge.js +125 -53
  20. package/dist/native/gesture-detector.d.ts +1 -1
  21. package/dist/native/gesture-detector.js +340 -0
  22. package/dist/native/index.d.ts +2 -2
  23. package/dist/native/index.js +1 -0
  24. package/dist/nodeOps.d.ts +1 -1
  25. package/dist/nodeOps.js +319 -0
  26. package/dist/op-queue.js +213 -0
  27. package/dist/render.d.ts +1 -1
  28. package/dist/render.js +125 -0
  29. package/dist/run-on-background.d.ts +1 -1
  30. package/dist/run-on-background.js +201 -0
  31. package/dist/shadow-element.js +91 -0
  32. package/dist/threading.d.ts +1 -1
  33. package/dist/threading.js +124 -0
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.js +10 -0
  36. package/dist/use-element-layout.d.ts +72 -0
  37. package/dist/use-element-layout.js +40 -0
  38. package/package.json +10 -8
  39. package/dist/hmr.js.map +0 -1
  40. package/dist/index.js.map +0 -1
  41. package/dist/mt-hmr-bridge.js.map +0 -1
@@ -12,5 +12,5 @@
12
12
  *
13
13
  * The old names continue to work via these re-exports for one minor cycle.
14
14
  */
15
- export { SharedValue as AnimatedValue, useSharedValue as useAnimatedValue, } from './shared-value';
16
- export type { SharedValueState as AnimatedValueState } from './shared-value';
15
+ export { SharedValue as AnimatedValue, useSharedValue as useAnimatedValue, } from './shared-value.js';
16
+ export type { SharedValueState as AnimatedValueState } from './shared-value.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @deprecated since Phase 2.8 — renamed to `SharedValue` to reflect that the
3
+ * primitive is a general MT-writeable, BG-observable cross-thread value, not
4
+ * an animation-specific construct. Animation is one customer; scroll, sensors,
5
+ * and gestures are equally first-class consumers.
6
+ *
7
+ * Import from `@sigx/lynx` directly:
8
+ *
9
+ * - `useAnimatedValue` → `useSharedValue`
10
+ * - `AnimatedValue` → `SharedValue`
11
+ * - `AnimatedValueState` → `SharedValueState`
12
+ *
13
+ * The old names continue to work via these re-exports for one minor cycle.
14
+ */
15
+ export { SharedValue as AnimatedValue, useSharedValue as useAnimatedValue, } from './shared-value.js';
@@ -1,5 +1,5 @@
1
1
  import type { PrimitiveSignal } from '@sigx/reactivity';
2
- import { MainThreadRef } from '../main-thread-ref';
2
+ import { MainThreadRef } from '../main-thread-ref.js';
3
3
  /**
4
4
  * Internal envelope shape stored under `MainThreadRef.current`. Wrapping the
5
5
  * value in `{ value: T }` (rather than a bare `T`) lets MT worklets mutate
@@ -0,0 +1,94 @@
1
+ import { onUnmounted } from '@sigx/runtime-core';
2
+ import { MainThreadRef } from '../main-thread-ref.js';
3
+ import { pushOp, scheduleFlush } from '../op-queue.js';
4
+ import { registerBgSink, unregisterBgSink } from '../animated-bridge.js';
5
+ import { OP } from '@sigx/lynx-runtime-internal';
6
+ /**
7
+ * MT-writeable, BG-readable cross-thread value with sigx reactive tracking.
8
+ *
9
+ * **Not animation-specific.** Animation is one customer (see `@sigx/motion`),
10
+ * gestures is another (`<Draggable translateX={sv} />`), scroll is another
11
+ * (`useScrollViewOffset`). Anywhere fast-or-frequent state lives natively on
12
+ * MT and you want BG to observe it reactively, this is the primitive.
13
+ *
14
+ * **MT side** — inside `'main thread'` worklets, mutate via `sv.current.value
15
+ * = newValue`. The MainThreadRef machinery makes the mutation stick across
16
+ * worklet invocations on the same wvid; the bridge picks up the new value on
17
+ * the next `__FlushElementTree` boundary.
18
+ *
19
+ * **BG side** — read via `sv.value` to get the latest published snapshot.
20
+ * Sigx tracking applies, so an `effect(() => console.log(sv.value))` re-runs
21
+ * whenever an MT publish ingests a new value. Writes from BG are read-only;
22
+ * dev-mode warns to point users back at the MT path.
23
+ *
24
+ * The class extends `MainThreadRef<{value:T}>` so that BG-side serialization
25
+ * (`nodeOps.ts:sanitizeCaptured`) recognizes it as a worklet ref and ships
26
+ * `{_wvid, _initValue}` over the wire to the MT bundle.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const tx = useSharedValue(0);
31
+ *
32
+ * <Draggable translateX={tx} />
33
+ * <text>x = {tx.value}</text> // BG-reactive, updates per drag frame
34
+ * ```
35
+ */
36
+ export class SharedValue extends MainThreadRef {
37
+ /** @internal — populated by `useSharedValue`. Reads here back the BG signal. */
38
+ _bgSignal;
39
+ constructor(initial) {
40
+ super({ value: initial });
41
+ }
42
+ /** @internal */
43
+ _bind(sig) {
44
+ this._bgSignal = sig;
45
+ }
46
+ /**
47
+ * BG-side reactive read. Returns the latest snapshot published by the MT
48
+ * bridge (or the initial value before any publish has occurred).
49
+ */
50
+ get value() {
51
+ return this._bgSignal.value;
52
+ }
53
+ set value(_v) {
54
+ // Reach for process via globalThis so the package doesn't pull in
55
+ // @types/node just for a dev-mode warning. NODE_ENV defaults to a
56
+ // non-production string, so the warning fires unless the bundler has
57
+ // explicitly substituted it to 'production'.
58
+ const env = globalThis
59
+ .process?.env?.['NODE_ENV'];
60
+ if (env !== 'production') {
61
+ console.warn('[sigx] SharedValue.value is read-only on the BG thread. ' +
62
+ 'Mutate via the MT worklet (sv.current.value = v).');
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Allocate a `SharedValue<T>` whose MT-side mutations are observable on the
68
+ * BG thread via sigx reactivity. Bind it to a component prop or read its
69
+ * `.value` from a BG `effect`.
70
+ *
71
+ * Lifecycle:
72
+ * - On allocate: pushes `INIT_MT_REF` (creates the MT-side refMap holder)
73
+ * and `REGISTER_AV_BRIDGE` (tells MT to track this wvid in its diff/
74
+ * publish pass). Allocates a BG-side signal mirror keyed by wvid.
75
+ * - On the owning component's unmount: pushes `UNREGISTER_AV_BRIDGE` and
76
+ * `RELEASE_MT_REF`, drops the BG signal.
77
+ */
78
+ export function useSharedValue(initial) {
79
+ const sv = new SharedValue(initial);
80
+ // BG: register the signal mirror under the wvid allocated in super().
81
+ sv._bind(registerBgSink(sv._wvid, initial));
82
+ // MT: create the refMap entry (envelope shape) and begin tracking this
83
+ // wvid in the diff/publish pass.
84
+ pushOp(OP.INIT_MT_REF, sv._wvid, sv._initValue);
85
+ pushOp(OP.REGISTER_AV_BRIDGE, sv._wvid, initial);
86
+ scheduleFlush();
87
+ onUnmounted(() => {
88
+ pushOp(OP.UNREGISTER_AV_BRIDGE, sv._wvid);
89
+ pushOp(OP.RELEASE_MT_REF, sv._wvid);
90
+ scheduleFlush();
91
+ unregisterBgSink(sv._wvid);
92
+ });
93
+ return sv;
94
+ }
@@ -1,7 +1,7 @@
1
- import type { MainThreadRef } from '../main-thread-ref';
2
- import type { MainThread } from '../jsx';
1
+ import type { MainThreadRef } from '../main-thread-ref.js';
2
+ import type { MainThread } from '../jsx.js';
3
3
  import type { BuiltinMapperName, MapperParams } from '@sigx/lynx-runtime-internal';
4
- import type { SharedValue } from './shared-value';
4
+ import type { SharedValue } from './shared-value.js';
5
5
  export type { BuiltinMapperName, MapperParams };
6
6
  /** Reset hook for tests. */
7
7
  export declare function resetAnimatedStyleBindingIds(): void;
@@ -0,0 +1,53 @@
1
+ import { onUnmounted } from '@sigx/runtime-core';
2
+ import { pushOp, scheduleFlush } from '../op-queue.js';
3
+ import { OP } from '@sigx/lynx-runtime-internal';
4
+ let nextBindingId = 1;
5
+ /** Reset hook for tests. */
6
+ export function resetAnimatedStyleBindingIds() {
7
+ nextBindingId = 1;
8
+ }
9
+ /**
10
+ * Returns the next binding id without incrementing — internal helper used by
11
+ * the `useAnimatedStyle` overloads to share allocation. Not exported.
12
+ */
13
+ function allocBindingId() {
14
+ return nextBindingId++;
15
+ }
16
+ /**
17
+ * Bind a `MainThreadRef`'s element style to a `SharedValue` via a named
18
+ * mapper. The MT-side runtime applies the mapper's output via
19
+ * `setStyleProperties` on every flush boundary where the SharedValue's value changed.
20
+ *
21
+ * The mapper runs on the **Main Thread** with no thread crossing per frame —
22
+ * the only inputs are the SharedValue's value (already on MT) and the `params` shipped
23
+ * once in the registration op. Because mappers are looked up by *name*, the
24
+ * SWC worklet transform doesn't have to capture a function reference (which
25
+ * it can't), so this fits the existing worklet pipeline cleanly.
26
+ *
27
+ * Multiple bindings on the same element compose into a single
28
+ * `setStyleProperties` call per flush. `transform` outputs concatenate in
29
+ * registration order (e.g. `translateX(50px) translateY(20px)`); other style
30
+ * keys merge by last-write-wins. When ANY binding on an element is dirty,
31
+ * ALL of that element's bindings re-run so partial outputs don't drop the
32
+ * unchanged-axis contribution.
33
+ *
34
+ * @example Ghost follower at 0.5×
35
+ * ```tsx
36
+ * const tx = useSharedValue(0);
37
+ * const ghostRef = useMainThreadRef<MainThread.Element | null>(null);
38
+ *
39
+ * useAnimatedStyle(ghostRef, tx, 'translateX', { factor: 0.5 });
40
+ *
41
+ * <Draggable translateX={tx} />
42
+ * <view main-thread:ref={ghostRef} style={...} />
43
+ * ```
44
+ */
45
+ export function useAnimatedStyle(elRef, sv, mapperName, params) {
46
+ const bindingId = allocBindingId();
47
+ pushOp(OP.REGISTER_AV_STYLE_BINDING, bindingId, elRef._wvid, sv._wvid, mapperName, params ?? null);
48
+ scheduleFlush();
49
+ onUnmounted(() => {
50
+ pushOp(OP.UNREGISTER_AV_STYLE_BINDING, bindingId);
51
+ scheduleFlush();
52
+ });
53
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * BG-side SharedValue bridge — receives MT-thread publishes.
3
+ *
4
+ * Maintains a `wvid → signal<T>` registry. The MT-side bridge in
5
+ * `@sigx/lynx-runtime-main/animated-bridge-mt.ts` dispatches batched
6
+ * `Lynx.Sigx.AvPublish` events with `[wvid, value]` tuples; `bg-bridge.ts`
7
+ * routes them through `ingestAvPublishes()` here, which writes each new
8
+ * value into the corresponding signal. Sigx reactivity tracking then re-runs
9
+ * any `effect` that read `sv.value` on BG.
10
+ *
11
+ * Producer: the `useSharedValue` hook in `@sigx/lynx` allocates the
12
+ * BG-side signal via `registerBgSink(wvid, initial)` and tears it down via
13
+ * `unregisterBgSink(wvid)` on component unmount.
14
+ *
15
+ * Naming note: the wire-format ops (`REGISTER_AV_BRIDGE`, `UNREGISTER_AV_BRIDGE`,
16
+ * `Lynx.Sigx.AvPublish`) keep their original `Av` prefix as infrastructure
17
+ * constants. The user-facing primitive renamed from `AnimatedValue` to
18
+ * `SharedValue` in Phase 2.8.
19
+ */
20
+ import { signal } from '@sigx/reactivity';
21
+ const bgRegistry = new Map();
22
+ /**
23
+ * Allocate a BG-side signal mirror for the given wvid. Returns the signal so
24
+ * the caller can read it via `.value` — sigx tracking applies as usual.
25
+ *
26
+ * Idempotent on the wvid: if a signal is already registered, returns the
27
+ * existing one without resetting its value (avoids losing in-flight publishes
28
+ * during HMR-triggered re-registration).
29
+ */
30
+ export function registerBgSink(wvid, initial) {
31
+ const existing = bgRegistry.get(wvid);
32
+ if (existing)
33
+ return existing;
34
+ // sigx's `signal` has Primitive vs object overloads; SharedValues hold
35
+ // primitives in practice (numbers, strings) and the bridge only ever
36
+ // writes through `.value`, so a single-overload selection via cast keeps
37
+ // the unconstrained T usable here without forcing SharedValue consumers
38
+ // to type-narrow at every call site.
39
+ const s = signal(initial);
40
+ bgRegistry.set(wvid, s);
41
+ return s;
42
+ }
43
+ /**
44
+ * Drop the BG-side signal for this wvid. Called on component unmount.
45
+ * Subsequent `ingestPublish` entries for this wvid become no-ops.
46
+ */
47
+ export function unregisterBgSink(wvid) {
48
+ bgRegistry.delete(wvid);
49
+ }
50
+ /**
51
+ * Ingest a batch of `[wvid, value]` tuples from the MT bridge. Writes each
52
+ * into the corresponding signal so any `effect` that read it re-runs on the
53
+ * sigx scheduler's next tick. Tuples for unregistered wvids (race with
54
+ * unmount) are silently dropped.
55
+ */
56
+ export function ingestAvPublishes(updates) {
57
+ for (const [wvid, value] of updates) {
58
+ const s = bgRegistry.get(wvid);
59
+ if (!s)
60
+ continue;
61
+ s.value = value;
62
+ }
63
+ }
64
+ /** Reset hook — for testing. Drops every registered sink. */
65
+ export function resetBgAvBridge() {
66
+ bgRegistry.clear();
67
+ }
68
+ /** Test hook — number of currently registered sinks. */
69
+ export function bgAvSinkCount() {
70
+ return bgRegistry.size;
71
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * BG-side listener for the MT→BG `Lynx.Sigx.PublishEvent` channel.
3
+ *
4
+ * The MT-side hybrid worklet (`lynx-runtime-main/src/hybrid-worklet.ts`) calls
5
+ * `lynx.getJSContext().dispatchEvent({ type: 'Lynx.Sigx.PublishEvent', data })`
6
+ * to fire the BG handler whose sign is captured in the hybrid ctx. We listen
7
+ * for that event here and route through the existing event-registry's
8
+ * `publishEvent` — the same dispatcher Lynx native calls when a normal
9
+ * `bindtap` fires on BG. The user's BG handler runs in the same call-stack
10
+ * shape it always has, so signal updates / `count.value++` etc. work without
11
+ * any awareness that the trigger came from MT.
12
+ *
13
+ * Side-effect import from `index.ts` so the listener is wired before any
14
+ * user code runs.
15
+ *
16
+ * CROSS-THREAD ASYMMETRY (per @lynx-js/react/runtime/lib/worklet/call/runOnBackground.js):
17
+ * - MT → BG dispatch: MT calls `lynx.getJSContext().dispatchEvent(...)`,
18
+ * BG listens via `lynx.getCoreContext().addEventListener(...)`.
19
+ * - BG → MT dispatch: BG calls `lynx.getCoreContext().dispatchEvent(...)`,
20
+ * MT listens via `lynx.getJSContext().addEventListener(...)`.
21
+ * Each side calls a DIFFERENT method to reach the other thread — they're not
22
+ * symmetric. Listening on `lynx.getJSContext()` from BG just listens on BG's
23
+ * own context (no cross-thread events arrive).
24
+ *
25
+ * `lynx` is closure-injected by RuntimeWrapperWebpackPlugin (declared in
26
+ * shims.d.ts). It is NOT available as `globalThis.lynx` — use the free
27
+ * identifier directly.
28
+ */
29
+ import { publishEvent } from './event-registry.js';
30
+ import { ingestAvPublishes } from './animated-bridge.js';
31
+ const lynxObj = typeof lynx !== 'undefined'
32
+ ? lynx
33
+ : undefined;
34
+ const ctx = lynxObj?.getCoreContext?.();
35
+ if (ctx?.addEventListener) {
36
+ ctx.addEventListener('Lynx.Sigx.PublishEvent', (e) => {
37
+ let payload;
38
+ try {
39
+ payload = JSON.parse(e.data);
40
+ }
41
+ catch {
42
+ return; // malformed bridge message — drop
43
+ }
44
+ if (typeof payload.sign === 'string') {
45
+ publishEvent(payload.sign, payload.event);
46
+ }
47
+ });
48
+ // SharedValue bridge: each event payload is an array of
49
+ // `[wvid, value]` tuples coalesced from one MT flush window. See
50
+ // `animated-bridge.ts` and `@sigx/lynx-runtime-main/animated-bridge-mt.ts`.
51
+ ctx.addEventListener('Lynx.Sigx.AvPublish', (e) => {
52
+ let updates;
53
+ try {
54
+ updates = JSON.parse(e.data);
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ if (Array.isArray(updates)) {
60
+ ingestAvPublishes(updates);
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Sign-based event handler registry for the Background Thread.
3
+ *
4
+ * When patchProp registers an event handler, it gets a unique sign string.
5
+ * The Main Thread stores this sign with __AddEvent(). When Native fires an
6
+ * event it calls publishEvent(sign, data) on the BG Thread, which looks up
7
+ * the handler and executes it directly — no cross-thread round-trip.
8
+ *
9
+ * sigx uses signal closures, so event handlers are plain functions captured by the component's
10
+ * render closure.
11
+ */
12
+ // The consumer bundle can materialise multiple event-registry module instances
13
+ // (for example via separate entry-background/runtime graphs). Store the state
14
+ // on globalThis so register() and publishEvent() still see the same handlers.
15
+ const REGISTRY_STATE_KEY = '__SIGX_LYNX_EVENT_REGISTRY__';
16
+ function getRegistryState() {
17
+ const g = globalThis;
18
+ let state = g[REGISTRY_STATE_KEY];
19
+ if (!state) {
20
+ state = {
21
+ signCounter: 0,
22
+ handlers: new Map(),
23
+ };
24
+ Object.defineProperty(g, REGISTRY_STATE_KEY, {
25
+ value: state,
26
+ configurable: true,
27
+ enumerable: false,
28
+ writable: true,
29
+ });
30
+ }
31
+ return state;
32
+ }
33
+ /**
34
+ * Register a handler and return a unique sign string.
35
+ * The sign is passed to __AddEvent so that the MTS can route events back.
36
+ */
37
+ export function register(handler) {
38
+ const state = getRegistryState();
39
+ const sign = `sigx:${state.signCounter++}`;
40
+ state.handlers.set(sign, handler);
41
+ return sign;
42
+ }
43
+ /**
44
+ * Update the handler for an existing sign without changing the sign.
45
+ * Used on re-renders: keeps the same sign registered on the Main Thread
46
+ * while pointing it to the freshest handler closure.
47
+ */
48
+ export function updateHandler(sign, handler) {
49
+ getRegistryState().handlers.set(sign, handler);
50
+ }
51
+ /**
52
+ * Get the current handler for a sign.
53
+ */
54
+ export function getHandler(sign) {
55
+ return getRegistryState().handlers.get(sign);
56
+ }
57
+ /**
58
+ * Unregister a handler by its sign.
59
+ */
60
+ export function unregister(sign) {
61
+ getRegistryState().handlers.delete(sign);
62
+ }
63
+ /**
64
+ * Called by Lynx Native when an event fires on the BG Thread.
65
+ * Looks up the handler by sign and invokes it with the event data.
66
+ */
67
+ export function publishEvent(sign, data) {
68
+ getRegistryState().handlers.get(sign)?.(data);
69
+ }
70
+ /** Reset all state — for testing only. */
71
+ export function resetRegistry() {
72
+ const state = getRegistryState();
73
+ state.signCounter = 0;
74
+ state.handlers.clear();
75
+ }
package/dist/flush.d.ts CHANGED
@@ -5,4 +5,4 @@
5
5
  * directly. Instead, ops are batched in a queue and flushed to the Main Thread
6
6
  * via sigxPatchUpdate. This file exists for backwards-compatible imports.
7
7
  */
8
- export { scheduleFlush, flushNow, resetOpQueue as resetFlushState } from './op-queue';
8
+ export { scheduleFlush, flushNow, resetOpQueue as resetFlushState } from './op-queue.js';
package/dist/flush.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Flush scheduler — re-exports from op-queue.ts.
3
+ *
4
+ * In the new architecture, the BG thread no longer calls __FlushElementTree
5
+ * directly. Instead, ops are batched in a queue and flushed to the Main Thread
6
+ * via sigxPatchUpdate. This file exists for backwards-compatible imports.
7
+ */
8
+ export { scheduleFlush, flushNow, resetOpQueue as resetFlushState } from './op-queue.js';
package/dist/hmr.js CHANGED
@@ -1,42 +1,122 @@
1
- //#region src/hmr.ts
2
- var e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), n = null, r = null, i = !1;
3
- function a(t, n) {
4
- i || (i = !0, n && (r = n), t({ onDefine(t, n, i) {
5
- let a = s();
6
- if (!a) return;
7
- n.__hmrId = a;
8
- let o = e.get(a);
9
- o && o.size > 0 && o.forEach((e) => {
10
- let n = r ? r(e.ctx) : null;
11
- try {
12
- let t = i(e.ctx);
13
- e.ctx.renderFn = t, e.ctx.update();
14
- } catch (e) {
15
- let n = e?.message ?? String(e), r = e?.stack ?? "<no stack>";
16
- console.error(`[sigx-hmr] Failed to update ${t || "component"}: ${n}\n${r}`);
17
- } finally {
18
- r && r(n);
19
- }
20
- });
21
- let c = i;
22
- n.__setup = (t) => {
23
- let n = c(t), r = { ctx: t }, i = e.get(a);
24
- return i || (i = /* @__PURE__ */ new Set(), e.set(a, i)), i.add(r), t.onUnmounted(() => {
25
- let t = e.get(a);
26
- t && t.delete(r);
27
- }), n;
28
- };
29
- } }));
1
+ /**
2
+ * HMR runtime for sigx-lynx (rspack/rsbuild).
3
+ *
4
+ * The `@sigx/lynx` meta package inlines runtime-core into a single bundle,
5
+ * so `@sigx/runtime-core/internals` would resolve to a *different* copy of
6
+ * the plugin registry than the one used by `component()`. To work around
7
+ * this the HMR loader injects a call to `initHMR(registerComponentPlugin)`
8
+ * where `registerComponentPlugin` is imported from `@sigx/lynx` (the same
9
+ * bundle the app uses), ensuring a single shared plugin array.
10
+ *
11
+ * Flow:
12
+ * 1. Loader prepends:
13
+ * import { __registerComponentPlugin } from '@sigx/lynx';
14
+ * import { initHMR, registerHMRModule } from '@sigx/lynx-runtime/hmr';
15
+ * initHMR(__registerComponentPlugin);
16
+ * registerHMRModule('<moduleId>');
17
+ * 2. On first call, `initHMR` installs the onDefine plugin.
18
+ * 3. On HMR update the module re-executes, `registerHMRModule` resets
19
+ * the per-module index, `component()` fires `onDefine`, and existing
20
+ * instances are patched in-place (property-ops only — no crash).
21
+ */
22
+ // Track instances by component ID (moduleId:index)
23
+ const instancesByComponentId = new Map();
24
+ // Track component definition order within each module
25
+ const moduleComponentIndex = new Map();
26
+ // Current module being registered
27
+ let currentModuleId = null;
28
+ // The renderer's currentInstance setter — captured from the app-side bundle
29
+ // so push/pop targets the SAME instance stack the renderer reads from. If
30
+ // missing (older app version that doesn't inject it), HMR patches skip the
31
+ // push/pop and rely on the caller having no context-dependent hooks.
32
+ let setCurrentInstance = null;
33
+ let installed = false;
34
+ /**
35
+ * Initialise the HMR plugin using the *app-side* registerComponentPlugin.
36
+ * Called once by the loader-injected preamble. Idempotent.
37
+ *
38
+ * `setCurrentInstanceFn` is the renderer's instance-stack push/pop helper
39
+ * (re-exported from `@sigx/lynx` as `__setCurrentInstanceForHMR`). Without
40
+ * it, re-running a screen's setup during HMR throws on hooks that depend on
41
+ * provide/inject (`useNav`, etc.) because the renderer's currentInstance is
42
+ * `null` when called outside the normal mount path.
43
+ */
44
+ export function initHMR(registerComponentPlugin, setCurrentInstanceFn) {
45
+ if (installed)
46
+ return;
47
+ installed = true;
48
+ if (setCurrentInstanceFn) {
49
+ setCurrentInstance = setCurrentInstanceFn;
50
+ }
51
+ registerComponentPlugin({
52
+ onDefine(name, factory, setup) {
53
+ const componentId = getNextComponentId();
54
+ if (!componentId)
55
+ return;
56
+ factory.__hmrId = componentId;
57
+ const existingInstances = instancesByComponentId.get(componentId);
58
+ if (existingInstances && existingInstances.size > 0) {
59
+ // HMR update: patch all existing instances with the new setup.
60
+ // The renderer pushes the active instance onto a stack before
61
+ // calling setup so that hooks like `useNav()` can resolve
62
+ // provide/inject up the parent chain. We're calling setup
63
+ // *outside* the renderer's mount path here, so we mirror the
64
+ // push/pop ourselves — otherwise context-dependent hooks
65
+ // throw with messages like "no <NavigationRoot> is mounted".
66
+ existingInstances.forEach(instance => {
67
+ const prev = setCurrentInstance ? setCurrentInstance(instance.ctx) : null;
68
+ try {
69
+ const newRenderFn = setup(instance.ctx);
70
+ instance.ctx.renderFn = newRenderFn;
71
+ instance.ctx.update();
72
+ }
73
+ catch (e) {
74
+ const msg = e?.message ?? String(e);
75
+ const stack = e?.stack ?? '<no stack>';
76
+ console.error(`[sigx-hmr] Failed to update ${name || 'component'}: ${msg}\n${stack}`);
77
+ }
78
+ finally {
79
+ if (setCurrentInstance)
80
+ setCurrentInstance(prev);
81
+ }
82
+ });
83
+ }
84
+ // Wrap setup to track future instances
85
+ const originalSetup = setup;
86
+ factory.__setup = (ctx) => {
87
+ const renderFn = originalSetup(ctx);
88
+ const instance = { ctx };
89
+ let instances = instancesByComponentId.get(componentId);
90
+ if (!instances) {
91
+ instances = new Set();
92
+ instancesByComponentId.set(componentId, instances);
93
+ }
94
+ instances.add(instance);
95
+ ctx.onUnmounted(() => {
96
+ const instances = instancesByComponentId.get(componentId);
97
+ if (instances)
98
+ instances.delete(instance);
99
+ });
100
+ return renderFn;
101
+ };
102
+ }
103
+ });
30
104
  }
31
- function o(e) {
32
- n = e, t.set(e, 0);
105
+ /**
106
+ * Register the current module for HMR tracking.
107
+ * Called at the top of each transformed module by the HMR loader.
108
+ */
109
+ export function registerHMRModule(moduleId) {
110
+ currentModuleId = moduleId;
111
+ moduleComponentIndex.set(moduleId, 0);
33
112
  }
34
- function s() {
35
- if (!n) return null;
36
- let e = t.get(n) || 0;
37
- return t.set(n, e + 1), `${n}:${e}`;
113
+ /**
114
+ * Get the next component ID for the current module.
115
+ */
116
+ function getNextComponentId() {
117
+ if (!currentModuleId)
118
+ return null;
119
+ const index = moduleComponentIndex.get(currentModuleId) || 0;
120
+ moduleComponentIndex.set(currentModuleId, index + 1);
121
+ return `${currentModuleId}:${index}`;
38
122
  }
39
- //#endregion
40
- export { a as initHMR, o as registerHMRModule };
41
-
42
- //# sourceMappingURL=hmr.js.map
package/dist/index.d.ts CHANGED
@@ -1,24 +1,26 @@
1
- import './jsx';
2
- import './types';
3
- import './model-processor';
4
- import './bg-bridge';
5
- import './run-on-background';
6
- export { render, lynxMount } from './render';
7
- export { nodeOps } from './nodeOps';
8
- export type { LynxNode, LynxElement } from './nodeOps';
9
- export { ShadowElement, createPageRoot, resetShadowState } from './shadow-element';
10
- export { pushOp, takeOps, scheduleFlush, flushNow, resetOpQueue } from './op-queue';
1
+ import './jsx.js';
2
+ import './types.js';
3
+ import './model-processor.js';
4
+ import './bg-bridge.js';
5
+ import './run-on-background.js';
6
+ export { render, lynxMount } from './render.js';
7
+ export { nodeOps } from './nodeOps.js';
8
+ export type { LynxNode, LynxElement } from './nodeOps.js';
9
+ export { ShadowElement, createPageRoot, resetShadowState } from './shadow-element.js';
10
+ export { pushOp, takeOps, scheduleFlush, flushNow, resetOpQueue } from './op-queue.js';
11
11
  export { OP } from '@sigx/lynx-runtime-internal';
12
12
  export type { OpCode, MapperParams, RangeParams, BuiltinMapperName, AnimatedStyleMapper, } from '@sigx/lynx-runtime-internal';
13
- export { register, updateHandler, unregister, getHandler, publishEvent, resetRegistry, } from './event-registry';
14
- export { MainThreadRef, useMainThreadRef, resetWvidCounter, } from './main-thread-ref';
15
- export { registerBgSink, unregisterBgSink, ingestAvPublishes, resetBgAvBridge, bgAvSinkCount, } from './animated-bridge';
16
- export { useSharedValue, SharedValue, } from './animated/shared-value';
17
- export type { SharedValueState } from './animated/shared-value';
18
- export { useAnimatedStyle, resetAnimatedStyleBindingIds, } from './animated/use-animated-style';
19
- export { useAnimatedValue, AnimatedValue, } from './animated/animated-value';
20
- export type { AnimatedValueState } from './animated/animated-value';
21
- export { runOnMainThread, runOnBackground, resetThreading, transformToWorklet, resetRunOnBackgroundState, } from './threading';
22
- export { Gesture, GestureType, useGestureDetector, resetGestureIdCounter, } from './native/index';
23
- export type { GestureTypeValue, GestureWorklet, GestureCallback, BaseGesture, ComposedGesture, AnyGesture, } from './native/index';
24
- export type { LynxEventHandler, LynxCommonAttributes, ViewAttributes, TextAttributes, ImageAttributes, ScrollViewAttributes, ListAttributes, ListItemAttributes, InputAttributes, TextAreaAttributes, PageAttributes, SvgAttributes, FilterImageAttributes, MainThread, } from './jsx';
13
+ export { register, updateHandler, unregister, getHandler, publishEvent, resetRegistry, } from './event-registry.js';
14
+ export { MainThreadRef, useMainThreadRef, resetWvidCounter, } from './main-thread-ref.js';
15
+ export { useElementLayout } from './use-element-layout.js';
16
+ export type { ElementLayout, LayoutChangeEvent, UseElementLayoutResult, } from './use-element-layout.js';
17
+ export { registerBgSink, unregisterBgSink, ingestAvPublishes, resetBgAvBridge, bgAvSinkCount, } from './animated-bridge.js';
18
+ export { useSharedValue, SharedValue, } from './animated/shared-value.js';
19
+ export type { SharedValueState } from './animated/shared-value.js';
20
+ export { useAnimatedStyle, resetAnimatedStyleBindingIds, } from './animated/use-animated-style.js';
21
+ export { useAnimatedValue, AnimatedValue, } from './animated/animated-value.js';
22
+ export type { AnimatedValueState } from './animated/animated-value.js';
23
+ export { runOnMainThread, runOnBackground, resetThreading, transformToWorklet, resetRunOnBackgroundState, } from './threading.js';
24
+ export { Gesture, GestureType, useGestureDetector, resetGestureIdCounter, } from './native/index.js';
25
+ export type { GestureTypeValue, GestureWorklet, GestureCallback, BaseGesture, ComposedGesture, AnyGesture, } from './native/index.js';
26
+ export type { LynxEventHandler, LynxCommonAttributes, ViewAttributes, TextAttributes, ImageAttributes, ScrollViewAttributes, ListAttributes, ListItemAttributes, InputAttributes, TextAreaAttributes, PageAttributes, SvgAttributes, FilterImageAttributes, MainThread, } from './jsx.js';