@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.
- package/dist/animated/animated-value.js +15 -0
- package/dist/animated/shared-value.js +94 -0
- package/dist/animated/use-animated-style.js +53 -0
- package/dist/animated-bridge.js +71 -0
- package/dist/bg-bridge.js +63 -0
- package/dist/event-registry.js +75 -0
- package/dist/flush.js +8 -0
- package/dist/hmr.d.ts +9 -1
- package/dist/hmr.js +119 -35
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -849
- package/dist/jsx.d.ts +46 -6
- package/dist/jsx.js +19 -0
- package/dist/main-thread-ref.js +134 -0
- package/dist/model-processor.js +76 -0
- package/dist/mt-hmr-bridge.js +125 -53
- package/dist/native/gesture-detector.js +340 -0
- package/dist/native/index.js +1 -0
- package/dist/nodeOps.js +319 -0
- package/dist/op-queue.js +213 -0
- package/dist/render.js +125 -0
- package/dist/run-on-background.d.ts +1 -1
- package/dist/run-on-background.js +201 -0
- package/dist/shadow-element.js +91 -0
- package/dist/threading.js +124 -0
- package/dist/types.js +10 -0
- package/dist/use-element-layout.d.ts +72 -0
- package/dist/use-element-layout.js +40 -0
- package/package.json +10 -8
- package/dist/hmr.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mt-hmr-bridge.js.map +0 -1
package/dist/op-queue.js
ADDED
|
@@ -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>(
|
|
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
|
+
}
|