@sigx/lynx-runtime 0.2.6 → 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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Op queue — accumulates renderer ops on the Background Thread and flushes
3
+ * them to the Main Thread via the Lynx host bridge.
4
+ *
5
+ * The bridge identifiers `lynx` and `lynxCoreInject` are NOT on globalThis.
6
+ * They are injected as closure parameters by RuntimeWrapperWebpackPlugin
7
+ * (peer dep `@lynx-js/runtime-wrapper-webpack-plugin`), which wraps the BG
8
+ * bundle in `__init_card_bundle__(lynxCoreInject, lynx, ...)`. Once wrapped,
9
+ * any module in the bundle can reference them as bare identifiers.
10
+ *
11
+ */
12
+ export { OP } from '@sigx/lynx-runtime-internal';
13
+ // ---------------------------------------------------------------------------
14
+ // Ambient declarations for the closure-injected host bridge identifiers
15
+ // ---------------------------------------------------------------------------
16
+ // `lynx` and `lynxCoreInject` are declared in src/shims.d.ts as the
17
+ // single source of truth for closure-injected identifiers from
18
+ // runtime-wrapper-webpack-plugin. Both are typed as `any` since their
19
+ // shape varies by host — call sites guard with typeof checks.
20
+ // ---------------------------------------------------------------------------
21
+ // Op buffer
22
+ // ---------------------------------------------------------------------------
23
+ let buffer = [];
24
+ /**
25
+ * Push one op (opcode + arguments) into the buffer as a flat sequence.
26
+ * Example: pushOp(OP.CREATE, id, type) → buffer gets [0, id, type].
27
+ */
28
+ export function pushOp(...args) {
29
+ for (const arg of args) {
30
+ buffer.push(arg);
31
+ }
32
+ }
33
+ /** Take all buffered ops and reset the buffer. */
34
+ export function takeOps() {
35
+ const b = buffer;
36
+ buffer = [];
37
+ return b;
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Scheduling
41
+ // ---------------------------------------------------------------------------
42
+ let scheduled = false;
43
+ /**
44
+ * Schedule a flush of the ops buffer at the end of the current microtask.
45
+ * Multiple calls within one tick are coalesced into one cross-thread call.
46
+ */
47
+ export function scheduleFlush() {
48
+ if (scheduled)
49
+ return;
50
+ scheduled = true;
51
+ Promise.resolve().then(doFlush);
52
+ }
53
+ /**
54
+ * Immediately flush all buffered ops — used on initial mount so the first
55
+ * frame is committed synchronously.
56
+ */
57
+ export function flushNow() {
58
+ scheduled = false;
59
+ const ops = takeOps();
60
+ if (ops.length === 0)
61
+ return;
62
+ sendOps(ops);
63
+ }
64
+ /** Reset module state — for testing only. */
65
+ export function resetOpQueue() {
66
+ buffer = [];
67
+ scheduled = false;
68
+ pendingAckResolve = null;
69
+ pendingAckPromise = null;
70
+ }
71
+ function doFlush() {
72
+ scheduled = false;
73
+ const ops = takeOps();
74
+ if (ops.length === 0)
75
+ return;
76
+ sendOps(ops);
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Main-thread ack tracking
80
+ //
81
+ // callLepusMethod is asynchronous: by the time the BG flush cycle finishes,
82
+ // the MT has not yet applied the ops. Track a promise that resolves when the
83
+ // MT acks via the callback so callers can `await waitForFlush()` if they need
84
+ // to coordinate with the next-tick UI state.
85
+ // ---------------------------------------------------------------------------
86
+ let pendingAckResolve = null;
87
+ let pendingAckPromise = null;
88
+ /**
89
+ * Resolves once the most recent ops batch has been applied on the main
90
+ * thread. If no ops are in flight, resolves immediately.
91
+ */
92
+ export function waitForFlush() {
93
+ return pendingAckPromise ?? Promise.resolve();
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Transport (BG → MT)
97
+ // ---------------------------------------------------------------------------
98
+ function sendOps(ops) {
99
+ const data = JSON.stringify(ops);
100
+ // Create the ack promise BEFORE sending so any waitForFlush() chained
101
+ // immediately after this call observes the in-flight batch.
102
+ pendingAckPromise = new Promise((resolve) => {
103
+ pendingAckResolve = resolve;
104
+ });
105
+ // Primary path: closure-injected `lynx` from RuntimeWrapperWebpackPlugin.
106
+ if (typeof lynx !== 'undefined') {
107
+ const app = lynx?.getNativeApp?.();
108
+ if (app && typeof app.callLepusMethod === 'function') {
109
+ app.callLepusMethod('sigxPatchUpdate', { data }, () => {
110
+ pendingAckResolve?.();
111
+ pendingAckResolve = null;
112
+ pendingAckPromise = null;
113
+ });
114
+ return;
115
+ }
116
+ }
117
+ // Same-thread fallback for unit tests where BG and MT share globalThis.
118
+ const g = globalThis;
119
+ if (typeof g['sigxPatchUpdate'] === 'function') {
120
+ g['sigxPatchUpdate']({ data });
121
+ pendingAckResolve?.();
122
+ pendingAckResolve = null;
123
+ pendingAckPromise = null;
124
+ return;
125
+ }
126
+ // No bridge available — drop and resolve so callers don't hang. This path
127
+ // indicates the bundle wasn't wrapped by RuntimeWrapperWebpackPlugin.
128
+ console.log('[sigx-bg] sendOps: no `lynx` global injected — bundle is missing RuntimeWrapperWebpackPlugin');
129
+ pendingAckResolve?.();
130
+ pendingAckResolve = null;
131
+ pendingAckPromise = null;
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Hot reload signal (BG → MT)
135
+ //
136
+ // Sent before a webpack HMR update replaces the BG module, so the MT resets
137
+ // its element registry and page root before the new ops batch arrives.
138
+ // ---------------------------------------------------------------------------
139
+ /**
140
+ * Tell the Main Thread to reset its element tree in preparation for a hot
141
+ * reload. The MT handler (`sigxHotReload`) calls `resetMainThreadState()`,
142
+ * re-creates the page root, and flushes — so the next `sigxPatchUpdate`
143
+ * batch builds on a clean tree.
144
+ *
145
+ * This is fire-and-forget: callLepusMethod messages are ordered, so
146
+ * sigxHotReload will be processed before any subsequent sigxPatchUpdate.
147
+ */
148
+ export function sendHotReloadSignal() {
149
+ // Primary path: closure-injected `lynx` from RuntimeWrapperWebpackPlugin.
150
+ if (typeof lynx !== 'undefined') {
151
+ const app = lynx?.getNativeApp?.();
152
+ if (app && typeof app.callLepusMethod === 'function') {
153
+ app.callLepusMethod('sigxHotReload', {}, () => { });
154
+ return;
155
+ }
156
+ }
157
+ // Same-thread fallback for testing where BG and MT share globalThis.
158
+ const g = globalThis;
159
+ if (typeof g['sigxHotReload'] === 'function') {
160
+ g['sigxHotReload']();
161
+ }
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Event dispatch (MT → BG)
165
+ //
166
+ // The Lynx host calls `lynxCoreInject.tt.publishEvent(sign, data)` (and
167
+ // `publicComponentEvent(cid, sign, data)`) when an event fires on the
168
+ // Main Thread. We install our dispatcher there once at module load.
169
+ // ---------------------------------------------------------------------------
170
+ /**
171
+ * Look up a sign in __SIGX_LYNX_EVENT_REGISTRY__ and invoke the registered
172
+ * handler. The registry is populated by lynx-runtime's patchProp branch
173
+ * whenever the renderer sees a `bindtap` / `onTap` / etc. prop.
174
+ */
175
+ function dispatchEvent(sign, evt) {
176
+ try {
177
+ const registry = globalThis.__SIGX_LYNX_EVENT_REGISTRY__;
178
+ if (!registry?.handlers || sign == null)
179
+ return;
180
+ const handlers = registry.handlers;
181
+ const fn = handlers instanceof Map
182
+ ? handlers.get(String(sign))
183
+ : handlers[String(sign)];
184
+ if (typeof fn === 'function')
185
+ fn(evt);
186
+ }
187
+ catch (e) {
188
+ console.log('[sigx-bg] event dispatch threw:', String(e));
189
+ }
190
+ }
191
+ /**
192
+ * Install our event dispatcher on `lynxCoreInject.tt` — the official place
193
+ * the Lynx host calls when it forwards Main Thread events to the BG.
194
+ *
195
+ * Idempotent. Called from render.ts on module load and from lynxMount() as
196
+ * a defensive re-install in case the host swaps the tt namespace between
197
+ * card loads.
198
+ */
199
+ export function installEventPublisher() {
200
+ // Primary install path — the canonical Lynx integration point.
201
+ if (typeof lynxCoreInject !== 'undefined' && lynxCoreInject?.tt) {
202
+ lynxCoreInject.tt.publishEvent = dispatchEvent;
203
+ lynxCoreInject.tt.publicComponentEvent = (_cid, sign, data) => dispatchEvent(sign, data);
204
+ }
205
+ // Fallback for older Lynx SDKs that look at globalThis.publishEvent.
206
+ const g = globalThis;
207
+ if (typeof g['publishEvent'] !== 'function') {
208
+ g['publishEvent'] = dispatchEvent;
209
+ }
210
+ if (typeof g['publicComponentEvent'] !== 'function') {
211
+ g['publicComponentEvent'] = ((_cid, sign, data) => dispatchEvent(sign, data));
212
+ }
213
+ }
package/dist/render.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Lynx renderer entry -- creates the renderer, defines lynxMount, and
3
+ * registers it as the default mount via setDefaultMount.
4
+ *
5
+ * Ported pattern from packages/runtime-terminal/src/index.ts
6
+ */
7
+ import { createRenderer, setDefaultMount } from '@sigx/runtime-core/internals';
8
+ import { nodeOps } from './nodeOps.js';
9
+ import { flushNow } from './flush.js';
10
+ import { installEventPublisher } from './op-queue.js';
11
+ import { createPageRoot } from './shadow-element.js';
12
+ // Install host-required event stubs (publishEvent / publicComponentEvent)
13
+ // before sigx mounts anything so the first MT → BG dispatch doesn't crash.
14
+ installEventPublisher();
15
+ // ---------------------------------------------------------------------------
16
+ // Renderer
17
+ // ---------------------------------------------------------------------------
18
+ const renderer = createRenderer(nodeOps);
19
+ export const { render } = renderer;
20
+ // ---------------------------------------------------------------------------
21
+ // lynxMount -- MountFn for Lynx environments
22
+ // ---------------------------------------------------------------------------
23
+ // ---------------------------------------------------------------------------
24
+ // HMR mount state — tracked so hot reloads can tear down and re-mount
25
+ // ---------------------------------------------------------------------------
26
+ let _hmrMounted = false;
27
+ let _hmrRoot = null;
28
+ let _hmrAppContext = undefined;
29
+ /**
30
+ * Mount function for Lynx environments.
31
+ *
32
+ * The page root is a ShadowElement with id=1 — the Main Thread creates the
33
+ * real page element in renderPage() before the BG thread runs. All subsequent
34
+ * ops reference this root by id so the MT can resolve it.
35
+ *
36
+ * On subsequent calls (hot reload), the previous tree is torn down and the
37
+ * Main Thread is signalled to reset before the new tree is mounted.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * import '@sigx/lynx-runtime'; // side-effect: registers lynxMount
42
+ * import { defineApp } from '@sigx/sigx';
43
+ *
44
+ * defineApp(<App />).mount();
45
+ * ```
46
+ */
47
+ export const lynxMount = (element, _container, appContext) => {
48
+ // Re-install in case the per-card app instance only became available
49
+ // after this module's top-level executed (timing varies by Lynx host).
50
+ installEventPublisher();
51
+ // Hot-reload fallback: if lynxMount is called again (e.g., main.tsx
52
+ // re-executed because a non-component module change bubbled up), the
53
+ // Lynx native engine cannot handle structural tree mutations. Fall
54
+ // back to a full card reload. With component-level HMR active, this
55
+ // path is only reached for non-component changes.
56
+ // See docs/hmr-investigation.md for details.
57
+ if (_hmrMounted && _hmrRoot) {
58
+ console.log('[sigx-hmr] Non-component change — triggering full card reload');
59
+ triggerLiveReload();
60
+ return () => { };
61
+ }
62
+ _hmrMounted = true;
63
+ const root = createPageRoot();
64
+ _hmrRoot = root;
65
+ _hmrAppContext = appContext;
66
+ render(element, root, appContext);
67
+ flushNow();
68
+ return () => {
69
+ render(null, root, appContext);
70
+ flushNow();
71
+ _hmrMounted = false;
72
+ _hmrRoot = null;
73
+ };
74
+ };
75
+ // ---------------------------------------------------------------------------
76
+ // Register as the default mount -- activated only when this module is imported
77
+ // ---------------------------------------------------------------------------
78
+ setDefaultMount(lynxMount);
79
+ // ---------------------------------------------------------------------------
80
+ // Live-reload fallback
81
+ //
82
+ // When a webpack HMR update cannot be applied (module shape changed too
83
+ // drastically), fall back to reloading the entire card bundle — the Lynx
84
+ // equivalent of a browser full-page refresh.
85
+ // ---------------------------------------------------------------------------
86
+ /**
87
+ * Trigger a full card reload via the Lynx host. Tries several host APIs
88
+ * in order of preference:
89
+ * 1. `lynxCoreInject.tt.reloadCard()` — standard Lynx host reload
90
+ * 2. `lynx.getNativeApp().callLepusMethod('sigxReloadCard', ...)` — custom
91
+ * reload signal the host can implement
92
+ * If none are available, logs a warning — the developer must manually reload.
93
+ */
94
+ function triggerLiveReload() {
95
+ try {
96
+ // Option 1: lynxCoreInject.tt.reloadCard (available in some Lynx hosts)
97
+ if (typeof lynxCoreInject !== 'undefined' && lynxCoreInject?.tt) {
98
+ const reloadCard = lynxCoreInject.tt['reloadCard'];
99
+ if (typeof reloadCard === 'function') {
100
+ reloadCard();
101
+ return;
102
+ }
103
+ }
104
+ // Option 2: signal via callLepusMethod so the MT can trigger a reload
105
+ if (typeof lynx !== 'undefined') {
106
+ const app = lynx?.getNativeApp?.();
107
+ if (app && typeof app.callLepusMethod === 'function') {
108
+ app.callLepusMethod('sigxReloadCard', {}, () => { });
109
+ return;
110
+ }
111
+ }
112
+ console.log('[sigx-hmr] No reload API available. Please manually reload the card.');
113
+ }
114
+ catch (e) {
115
+ console.log('[sigx-hmr] triggerLiveReload error:', String(e));
116
+ }
117
+ }
118
+ if (typeof module !== 'undefined' && module?.hot) {
119
+ module.hot.accept((err) => {
120
+ if (err) {
121
+ console.log('[sigx-hmr] Hot update failed, falling back to live reload:', String(err));
122
+ triggerLiveReload();
123
+ }
124
+ });
125
+ }
@@ -32,6 +32,6 @@ interface WorkletCtx {
32
32
  }
33
33
  export declare function transformToWorklet(fn: (...args: unknown[]) => unknown): JsFnHandle;
34
34
  export declare function registerWorkletCtx(ctx: WorkletCtx): void;
35
- export declare function runOnBackground<R, Fn extends (...args: never[]) => R>(_fn: Fn): (...args: Parameters<Fn>) => Promise<R>;
35
+ export declare function runOnBackground<R, Fn extends (...args: never[]) => R>(fn: Fn): (...args: Parameters<Fn>) => Promise<R>;
36
36
  export declare function resetRunOnBackgroundState(): void;
37
37
  export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * runOnBackground — BG-side wiring for the MT→BG cross-thread call channel.
3
+ *
4
+ * Two responsibilities:
5
+ * 1. `transformToWorklet(fn)` — wraps a BG function as a `JsFnHandle`
6
+ * `{ _jsFnId, _fn }` so the SWC transform can serialise it into the
7
+ * `_jsFn` slot of a worklet ctx. The BG worklet-loader emits inline
8
+ * `transformToWorklet(...)` calls when the user writes `runOnBackground(fn)`
9
+ * inside a `'main thread'` body.
10
+ * 2. `Lynx.Sigx.RunOnBackground` listener — when the MT-side dispatcher
11
+ * fires, finds the matching JsFnHandle by `(execId, fnId)` from the
12
+ * registered worklet ctxs, runs `_fn(...params)`, dispatches
13
+ * `Lynx.Sigx.FunctionCallRet` back with `{resolveId, returnValue}`.
14
+ *
15
+ * Mirrors @lynx-js/react/runtime/lib/worklet/call/runOnBackground +
16
+ * vue-lynx's run-on-background.ts (same protocol shape, sigx-namespaced
17
+ * event types).
18
+ */
19
+ const RUN_ON_BACKGROUND = 'Lynx.Sigx.RunOnBackground';
20
+ const FUNCTION_CALL_RET = 'Lynx.Sigx.FunctionCallRet';
21
+ // ---------------------------------------------------------------------------
22
+ // transformToWorklet — mint a JsFnHandle for cross-thread dispatch
23
+ //
24
+ // The SWC JS pass emits inline `transformToWorklet(fn)` calls in the BG bundle
25
+ // when it sees `runOnBackground(fn)` inside a `'main thread'` body. The handle
26
+ // flows into the worklet ctx's `_jsFn` slot; MT extracts it and dispatches via
27
+ // `runOnBackground(handle)(...args)`.
28
+ // ---------------------------------------------------------------------------
29
+ let lastJsFnId = 0;
30
+ export function transformToWorklet(fn) {
31
+ const id = ++lastJsFnId;
32
+ if (typeof fn !== 'function') {
33
+ return {
34
+ _jsFnId: id,
35
+ _error: `Argument of runOnBackground should be a function, got [${typeof fn}]`,
36
+ };
37
+ }
38
+ // Stamp toJSON so JSON.stringify of the worklet ctx replaces the function
39
+ // body with a placeholder string — MT only needs `_jsFnId`/`_execId`.
40
+ fn.toJSON ??= () => '[BackgroundFunction]';
41
+ return { _jsFnId: id, _fn: fn };
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // IndexMap — auto-incrementing Map (worklet exec-id allocator)
45
+ // ---------------------------------------------------------------------------
46
+ class IndexMap {
47
+ lastIndex = 0;
48
+ map = new Map();
49
+ add(value) {
50
+ const id = ++this.lastIndex;
51
+ this.map.set(id, value);
52
+ return id;
53
+ }
54
+ get(index) {
55
+ return this.map.get(index);
56
+ }
57
+ remove(index) {
58
+ this.map.delete(index);
59
+ }
60
+ }
61
+ class WorkletExecIdMap extends IndexMap {
62
+ add(worklet) {
63
+ const execId = super.add(worklet);
64
+ worklet._execId = execId;
65
+ return execId;
66
+ }
67
+ findJsFnHandle(execId, fnId) {
68
+ const worklet = this.get(execId);
69
+ if (!worklet)
70
+ return undefined;
71
+ const visited = new Set();
72
+ const search = (value) => {
73
+ if (value === null || typeof value !== 'object')
74
+ return undefined;
75
+ const obj = value;
76
+ if (visited.has(obj))
77
+ return undefined;
78
+ visited.add(obj);
79
+ if ('_jsFnId' in obj && obj['_jsFnId'] === fnId) {
80
+ return obj;
81
+ }
82
+ for (const key in obj) {
83
+ const result = search(obj[key]);
84
+ if (result)
85
+ return result;
86
+ }
87
+ return undefined;
88
+ };
89
+ return search(worklet);
90
+ }
91
+ }
92
+ // ---------------------------------------------------------------------------
93
+ // Module state — lazy-init so SSR / tests don't pay for the listener wiring
94
+ // ---------------------------------------------------------------------------
95
+ let execIdMap;
96
+ function getCoreContext() {
97
+ if (typeof lynx === 'undefined')
98
+ return undefined;
99
+ const obj = lynx;
100
+ return typeof obj.getCoreContext === 'function' ? obj.getCoreContext() : undefined;
101
+ }
102
+ function init() {
103
+ execIdMap = new WorkletExecIdMap();
104
+ const ctx = getCoreContext();
105
+ ctx?.addEventListener?.(RUN_ON_BACKGROUND, runJSFunction);
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // registerWorkletCtx — stamp _execId on outgoing worklet ctxs
109
+ //
110
+ // Called from nodeOps.patchProp (SET_WORKLET_EVENT path) and from
111
+ // runOnMainThread before shipping a ctx across threads. Must run BEFORE
112
+ // JSON.stringify so the ctx carries `_execId` to MT.
113
+ // ---------------------------------------------------------------------------
114
+ export function registerWorkletCtx(ctx) {
115
+ if (!execIdMap)
116
+ init();
117
+ execIdMap.add(ctx);
118
+ }
119
+ function runJSFunction(event) {
120
+ let data;
121
+ try {
122
+ data = JSON.parse(event.data);
123
+ }
124
+ catch {
125
+ return; // malformed bridge message — drop
126
+ }
127
+ const handle = execIdMap?.findJsFnHandle(data.obj._execId, data.obj._jsFnId);
128
+ if (!handle?._fn) {
129
+ // Fn is gone — likely the owning worklet ctx was unregistered. Resolve
130
+ // with undefined so the MT promise doesn't hang.
131
+ dispatchReturn(data.resolveId, undefined);
132
+ return;
133
+ }
134
+ let returnValue;
135
+ try {
136
+ returnValue = handle._fn(...data.params);
137
+ }
138
+ catch (e) {
139
+ dispatchReturn(data.resolveId, undefined);
140
+ throw e;
141
+ }
142
+ // Promise return values are not transferable across the JSON bridge — caller
143
+ // must await on the BG fn body itself if they need async results.
144
+ dispatchReturn(data.resolveId, returnValue);
145
+ }
146
+ function dispatchReturn(resolveId, returnValue) {
147
+ const ctx = getCoreContext();
148
+ ctx?.dispatchEvent?.({
149
+ type: FUNCTION_CALL_RET,
150
+ data: JSON.stringify({ resolveId, returnValue }),
151
+ });
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // User-facing entry point.
155
+ //
156
+ // SWC's BG-target worklet pass replaces every `runOnBackground(fn)` call
157
+ // inside a `'main thread'` body with a `transformToWorklet(fn)` placeholder
158
+ // at build time, so on the Background Thread bundle this function is normally
159
+ // only reached from outside a worklet (a misuse) and throws.
160
+ //
161
+ // On the Main Thread bundle the LEPUS pass *also* emits a bare
162
+ // `runOnBackground(handle)` call inside the registered worklet body. The
163
+ // `handle` there is a `JsFnHandle` (an object with `_jsFnId` / `_execId`
164
+ // stamped by `transformToWorklet` on the BG side and ferried across the
165
+ // SET_WORKLET_EVENT bridge). The call resolves to the module-level import
166
+ // of `runOnBackground` (this function), NOT to the MT-side dispatcher
167
+ // that `@sigx/lynx-runtime-main` installs on `globalThis.runOnBackground`
168
+ // during bootstrap. Without a hand-off, MT worklets would always hit the
169
+ // throw.
170
+ //
171
+ // Bridge: when the argument is handle-shaped AND `globalThis.runOnBackground`
172
+ // exists and is a different function (the MT-side dispatcher installed by
173
+ // `entry-main.ts`), delegate to it. Raw functions (real misuse — calling
174
+ // `runOnBackground(someFn)` outside a worklet body) still throw, so the
175
+ // API contract surfaces a useful error instead of silently resolving to
176
+ // `undefined`. On BG the global is never installed, so the throw remains
177
+ // the fallback path. Upstream `@lynx-js/react` achieves the same split
178
+ // via a build-time `__JS__` define; the runtime check here keeps us off
179
+ // that machinery while producing identical end-state behaviour.
180
+ // ---------------------------------------------------------------------------
181
+ function isJsFnHandle(value) {
182
+ return typeof value === 'object' && value !== null && '_jsFnId' in value;
183
+ }
184
+ export function runOnBackground(fn) {
185
+ if (isJsFnHandle(fn)) {
186
+ const g = globalThis;
187
+ if (typeof g.runOnBackground === 'function' && g.runOnBackground !== runOnBackground) {
188
+ return g.runOnBackground(fn);
189
+ }
190
+ }
191
+ throw new Error('runOnBackground() can only be used inside \'main thread\' functions. '
192
+ + 'The SWC worklet transform should replace this call at build time — '
193
+ + 'verify @sigx/lynx-plugin\'s worklet-loader is wired into your bundler.');
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Reset — for testing only
197
+ // ---------------------------------------------------------------------------
198
+ export function resetRunOnBackgroundState() {
199
+ execIdMap = undefined;
200
+ lastJsFnId = 0;
201
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ShadowElement: a lightweight doubly-linked tree node that lives entirely in
3
+ * the Background Thread. It lets the renderer call parentNode() / nextSibling()
4
+ * synchronously, while the real Lynx elements exist only on the Main Thread.
5
+ *
6
+ * id=1 is reserved for the page root (created via __CreatePage on Main Thread).
7
+ * Regular elements start from id=2.
8
+ */
9
+ export class ShadowElement {
10
+ static nextId = 2; // 1 is reserved for the page root
11
+ id;
12
+ type;
13
+ parent = null;
14
+ firstChild = null;
15
+ lastChild = null;
16
+ prev = null;
17
+ next = null;
18
+ // Cached style object (last value passed to patchProp 'style').
19
+ // Used by vShow to merge display:none without losing the original styles.
20
+ _style = {};
21
+ // Set to true by vShow when the element should be hidden.
22
+ _vShowHidden = false;
23
+ // Class management for Transition support.
24
+ _baseClass = '';
25
+ _transitionClasses = new Set();
26
+ constructor(type, forceId) {
27
+ this.id = forceId !== undefined ? forceId : ShadowElement.nextId++;
28
+ this.type = type;
29
+ }
30
+ insertBefore(child, anchor) {
31
+ // Detach from current parent first
32
+ if (child.parent) {
33
+ child.parent.removeChild(child);
34
+ }
35
+ child.parent = this;
36
+ if (anchor) {
37
+ // Insert before anchor
38
+ const prev = anchor.prev;
39
+ child.next = anchor;
40
+ child.prev = prev;
41
+ anchor.prev = child;
42
+ if (prev) {
43
+ prev.next = child;
44
+ }
45
+ else {
46
+ this.firstChild = child;
47
+ }
48
+ }
49
+ else {
50
+ // Append at end
51
+ if (this.lastChild) {
52
+ this.lastChild.next = child;
53
+ child.prev = this.lastChild;
54
+ }
55
+ else {
56
+ this.firstChild = child;
57
+ child.prev = null;
58
+ }
59
+ this.lastChild = child;
60
+ child.next = null;
61
+ }
62
+ }
63
+ removeChild(child) {
64
+ const prev = child.prev;
65
+ const next = child.next;
66
+ if (prev) {
67
+ prev.next = next;
68
+ }
69
+ else {
70
+ this.firstChild = next;
71
+ }
72
+ if (next) {
73
+ next.prev = prev;
74
+ }
75
+ else {
76
+ this.lastChild = prev;
77
+ }
78
+ child.parent = null;
79
+ child.prev = null;
80
+ child.next = null;
81
+ }
82
+ }
83
+ export const PAGE_ROOT_ID = 1;
84
+ /** Create the page root shadow element with the reserved id=1. */
85
+ export function createPageRoot() {
86
+ return new ShadowElement('page', PAGE_ROOT_ID);
87
+ }
88
+ /** Reset the ID counter — for testing only. */
89
+ export function resetShadowState() {
90
+ ShadowElement.nextId = 2;
91
+ }