mithril-lynx 0.0.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/element.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Ambient declaration for the ESM element.js (the file itself is not
2
+ // type-checked; this describes its runtime export shape for TS consumers).
3
+
4
+ export interface SelectorParams {
5
+ onlyCurrentComponent?: boolean;
6
+ }
7
+
8
+ export interface AnimationTimingOptions {
9
+ name?: string;
10
+ duration?: number | string;
11
+ delay?: number | string;
12
+ iterationCount?: number | string;
13
+ fillMode?: string;
14
+ timingFunction?: string;
15
+ direction?: string;
16
+ }
17
+
18
+ export type Keyframe = Record<string, string | number>;
19
+
20
+ export interface MainThreadElement {
21
+ setStyleProperty(name: string, value: string | number): void;
22
+ setStyleProperties(styles: Record<string, string | number>): void;
23
+ setAttribute(name: string, value: unknown): void;
24
+ querySelector(selector: string, params?: SelectorParams): MainThreadElement | null;
25
+ querySelectorAll(selector: string, params?: SelectorParams): MainThreadElement[];
26
+ animate(keyframes: Keyframe[], options?: AnimationTimingOptions): void;
27
+ playAnimation(name: string): void;
28
+ pauseAnimation(name: string): void;
29
+ cancelAnimation(name: string): void;
30
+ invoke(method: string, params?: Record<string, unknown>): Promise<{ code: number; data: unknown }>;
31
+ }
32
+
33
+ /** Wraps a node (anything with a `_handle`, i.e. a real LynxNodeWrapper) with imperative PAPI methods. */
34
+ export function wrapElement(node: { _handle: unknown }): MainThreadElement;
package/element.js ADDED
@@ -0,0 +1,79 @@
1
+ // element.js
2
+ //
3
+ // Ergonomic imperative escape hatch for a main-thread node — whatever
4
+ // Mithril's oncreate(vnode)/onupdate(vnode) hooks hand you as `vnode.dom`
5
+ // (works for any real LynxNodeWrapper: main-thread-owned mode, or
6
+ // data-channel mode's main-thread half). Not part of render.js's own DOM
7
+ // contract (see ../CONTRACT.md) — these are Element PAPI capabilities apps
8
+ // reach for directly (focusing a native input, animating, calling a native
9
+ // custom element's method), so they live in their own small module instead
10
+ // of the shim.
11
+ //
12
+ // Project plan, Phase 5. Background-thread refs are the selector-query
13
+ // equivalent — see background.js's createRef().
14
+
15
+ const ANIMATION_OPERATION = { START: 0, PLAY: 1, PAUSE: 2, CANCEL: 3 };
16
+
17
+ function wrapRef(handle) {
18
+ return handle == null ? null : wrapElement({ _handle: handle });
19
+ }
20
+
21
+ /**
22
+ * Wraps a node (anything with a `_handle`, i.e. a real LynxNodeWrapper OR a
23
+ * raw ElementRef from querySelector) with imperative PAPI methods render.js
24
+ * itself never needs. setAttribute mirrors the real LynxNodeWrapper's own
25
+ * class/id/data-prefixed/generic-attribute special-casing (see
26
+ * ../CONTRACT.md) rather than delegating to node.setAttribute() directly —
27
+ * a raw querySelector result has no such method, only a `_handle`.
28
+ */
29
+ export function wrapElement(node) {
30
+ const handle = node._handle;
31
+
32
+ return {
33
+ setStyleProperty(name, value) {
34
+ __SetInlineStyles(handle, { [name]: value });
35
+ },
36
+
37
+ setStyleProperties(styles) {
38
+ __SetInlineStyles(handle, styles);
39
+ },
40
+
41
+ setAttribute(name, value) {
42
+ if (name === "class") __SetClasses(handle, value == null ? undefined : String(value));
43
+ else if (name === "id") __SetID(handle, value == null ? null : String(value));
44
+ else if (name.slice(0, 5) === "data-") __AddDataset(handle, name.slice(5), value);
45
+ else __SetAttribute(handle, name, value == null ? null : value);
46
+ },
47
+
48
+ querySelector(selector, params) {
49
+ return wrapRef(__QuerySelector(handle, selector, params || {}));
50
+ },
51
+
52
+ querySelectorAll(selector, params) {
53
+ return __QuerySelectorAll(handle, selector, params || {}).map((ref) => wrapRef(ref));
54
+ },
55
+
56
+ animate(keyframes, options) {
57
+ __ElementAnimate(handle, [ANIMATION_OPERATION.START, (options && options.name) || "", keyframes, options]);
58
+ },
59
+
60
+ playAnimation(name) {
61
+ __ElementAnimate(handle, [ANIMATION_OPERATION.PLAY, name]);
62
+ },
63
+
64
+ pauseAnimation(name) {
65
+ __ElementAnimate(handle, [ANIMATION_OPERATION.PAUSE, name]);
66
+ },
67
+
68
+ cancelAnimation(name) {
69
+ __ElementAnimate(handle, [ANIMATION_OPERATION.CANCEL, name]);
70
+ },
71
+
72
+ /** Always resolves with { code, data } — check `code` yourself; PAPI's success/failure convention isn't assumed here. */
73
+ invoke(method, params) {
74
+ return new Promise((resolve) => {
75
+ __InvokeUIMethod(handle, method, params || {}, (res) => resolve(res));
76
+ });
77
+ },
78
+ };
79
+ }
package/gesture.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ // Ambient declaration for the ESM gesture.js (the file itself is not
2
+ // type-checked; this describes its runtime export shape for TS consumers).
3
+
4
+ export const GestureType: {
5
+ readonly COMPOSED: -1;
6
+ readonly PAN: 0;
7
+ readonly FLING: 1;
8
+ readonly DEFAULT: 2;
9
+ readonly TAP: 3;
10
+ readonly LONGPRESS: 4;
11
+ readonly ROTATION: 5;
12
+ readonly PINCH: 6;
13
+ readonly NATIVE: 7;
14
+ };
15
+
16
+ export type GestureTypeName = keyof typeof GestureType;
17
+
18
+ export interface Gesture {
19
+ id: number;
20
+ remove(): void;
21
+ setState(state: number): void;
22
+ }
23
+
24
+ export interface GestureController {
25
+ __SetGestureState(state: number): void;
26
+ __ConsumeGesture(options: Record<string, boolean>): void;
27
+ }
28
+
29
+ export interface CreateGestureOptions {
30
+ type: number | GestureTypeName;
31
+ /** Each callback is invoked as `(event, controller) => {}`; `controller` can usually be ignored. */
32
+ callbacks?: Record<string, (event: unknown, controller: GestureController) => unknown>;
33
+ waitFor?: Gesture[];
34
+ simultaneousWith?: Gesture[];
35
+ continueWith?: Gesture[];
36
+ config?: Record<string, unknown>;
37
+ }
38
+
39
+ /** Registers a gesture detector on a node (anything with a `_handle`). See the project plan, Phase 7. */
40
+ export function createGesture(node: { _handle: unknown }, options: CreateGestureOptions): Gesture;
package/gesture.js ADDED
@@ -0,0 +1,117 @@
1
+ // gesture.js
2
+ //
3
+ // Gesture composition API (project plan, Phase 7), main-thread only. Built
4
+ // on __SetGestureDetector — a plain, same-thread PAPI call (no cross-thread
5
+ // serialization boundary), so this needs nothing beyond a real node handle
6
+ // PLUS the small worklet-registration step below. A callback that needs to
7
+ // notify background-owned state can call main-thread.js's
8
+ // runOnBackground() itself, inside the callback body — an explicit, opt-in
9
+ // cross-thread hop only where the app actually needs one.
10
+ //
11
+ // RESOLVED (2026-09-09), via the SAME technique used to fix list.js's Tier
12
+ // 2 (read the real, shipped @lynx-js/react source more deeply — see
13
+ // DEVICE_VERIFICATION.md), plus one config-level bug found the hard way.
14
+ // THREE issues stacked here, found and fixed in this order:
15
+ //
16
+ // 1. The config/relationMap shape below matches @lynx-js/react's own
17
+ // runtime/lib/snapshot/gesture/processGesture.js verbatim (real, shipped
18
+ // code — __SetGestureDetector for both create and update, has-react-
19
+ // gesture/flatten set first). But that file's own callback slot type is
20
+ // loose (`callback: unknown`) — reading one file deeper,
21
+ // runtime/lib/worklet-runtime/workletRuntime.js, revealed native invokes
22
+ // gesture callbacks through a global `runWorklet(ctx, params)`, whose
23
+ // `validateWorklet(ctx)` requires `typeof ctx === 'object'`. A plain JS
24
+ // function fails that and is dropped with NO error — exactly this
25
+ // project's first real-device symptom (zero console output on a swipe).
26
+ // Fixed by `./src/worklet-runtime.js`'s `wrapWorkletCallback()`.
27
+ // 2. Even with (1) fixed, still zero output — because `enableNewGesture`
28
+ // (a Lynx SDK compiler/runtime option, @lynx-js/type-config's
29
+ // config.d.ts, `@defaultValue false`) was never turned on for this app.
30
+ // Without it, __SetGestureDetector registrations are accepted but the
31
+ // runtime keeps using "the legacy touch-only gesture path" and never
32
+ // acts on them at all — see the consuming app's lynx.config.ts.
33
+ // 3. With both fixed, native finally called into JS — and immediately threw
34
+ // `TypeError: not a object`. Bisected step-by-step on-device: native
35
+ // calls a gesture callback with TWO arguments, `(event, controller)`,
36
+ // where `controller` is a native host object (`{__SetGestureState,
37
+ // __ConsumeGesture}`). That object throws when it crosses
38
+ // `Function.prototype.apply()`'s argument-list marshalling, but is fine
39
+ // passed positionally — matching @lynx-js/react's own runWorkletImpl,
40
+ // which never uses apply()/call() either (`worklet(...params_)`, a
41
+ // plain spread call). Fixed in `./src/worklet-runtime.js`.
42
+ //
43
+ // Confirmed end-to-end on the real device: a full pan (start → update →
44
+ // end) updates UI state with zero errors. See `./src/worklet-runtime.js`
45
+ // for exactly what's reimplemented vs. scoped out of the real worklet
46
+ // runtime, and `callbacks`' new second-argument note below.
47
+
48
+ import { wrapWorkletCallback } from "./src/worklet-runtime.js";
49
+
50
+ export const GestureType = {
51
+ COMPOSED: -1,
52
+ PAN: 0,
53
+ FLING: 1,
54
+ DEFAULT: 2,
55
+ TAP: 3,
56
+ LONGPRESS: 4,
57
+ ROTATION: 5,
58
+ PINCH: 6,
59
+ NATIVE: 7,
60
+ };
61
+
62
+ let nextGestureId = 1;
63
+
64
+ /**
65
+ * Registers a gesture detector on `node` (anything with a `_handle`, i.e. a
66
+ * real LynxNodeWrapper). `type` is a GestureType value or its string key
67
+ * ("pan", "tap", ...). `callbacks` keys are event names (e.g. "onStart",
68
+ * "onUpdate", "onEnd") mapped to plain functions, called directly, same
69
+ * thread. Each is invoked as `(event, controller) => {}` — `controller`
70
+ * (`{__SetGestureState, __ConsumeGesture}`) is a native gesture-arena
71
+ * handle; most callbacks can ignore it entirely.
72
+ * `waitFor`/`simultaneousWith`/`continueWith` are arrays of OTHER
73
+ * createGesture() return values, for gesture-arena composition.
74
+ */
75
+ export function createGesture(node, options) {
76
+ const {
77
+ type,
78
+ callbacks = {},
79
+ waitFor = [],
80
+ simultaneousWith = [],
81
+ continueWith = [],
82
+ config,
83
+ } = options;
84
+ const handle = node._handle;
85
+ const gestureType = typeof type === "string" ? GestureType[type.toUpperCase()] : type;
86
+ const id = nextGestureId++;
87
+
88
+ // Marker attributes native needs to recognize a gesture-enabled element —
89
+ // kept verbatim from processGesture.js's own names/values, since it's
90
+ // unclear whether native checks these exact strings independent of
91
+ // framework, or whether they're purely a ReactLynx-side bookkeeping detail.
92
+ __SetAttribute(handle, "has-react-gesture", true);
93
+ __SetAttribute(handle, "flatten", false);
94
+
95
+ const detectorConfig = {
96
+ callbacks: Object.keys(callbacks).map((name) => ({ name, callback: wrapWorkletCallback(callbacks[name]) })),
97
+ };
98
+ if (config != null) detectorConfig.config = config;
99
+
100
+ const relationMap = {
101
+ waitFor: waitFor.map((g) => g.id),
102
+ simultaneous: simultaneousWith.map((g) => g.id),
103
+ continueWith: continueWith.map((g) => g.id),
104
+ };
105
+
106
+ __SetGestureDetector(handle, id, gestureType, detectorConfig, relationMap);
107
+
108
+ return {
109
+ id,
110
+ remove() {
111
+ if (typeof __RemoveGestureDetector === "function") __RemoveGestureDetector(handle, id);
112
+ },
113
+ setState(state) {
114
+ __SetGestureState(handle, id, state);
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,26 @@
1
+ // Shared event-name constants for the main-thread <-> background-thread
2
+ // channel, ported from lynx-examples/examples/vanilla/src/common/constant.ts.
3
+ // Native engine lifecycle events keep their exact native names; custom
4
+ // app-level events are namespaced to avoid colliding with other libraries
5
+ // dispatching on the same shared event bus.
6
+
7
+ export const renderPageEventName = "__RenderPage";
8
+ export const updatePageEventName = "__UpdatePage";
9
+ export const destroyLifetimeEventName = "__DestroyLifetime";
10
+
11
+ export const updateDataFromMainThreadEventName = "MithrilLynx:UpdateDataFromMainThread";
12
+ export const updateDataFromBackgroundEventName = "MithrilLynx:UpdateDataFromBackground";
13
+ export const dispatchEventToBackgroundEventName = "MithrilLynx:DispatchEventToBackground";
14
+
15
+ // Renderer mode (Phase 4): background thread ships op-log patches to main
16
+ // thread; main thread forwards real PAPI events back to background by vid.
17
+ export const rendererPatchEventName = "MithrilLynx:RendererPatch";
18
+ export const rendererEventEventName = "MithrilLynx:RendererEvent";
19
+
20
+ // Cross-thread function registry (Phase 6, worklet substitute): call/return
21
+ // correlation for genuinely cross-thread calls. "ToMainThread"/"ToBackground"
22
+ // name which side the CALL travels to — each has its own result event.
23
+ export const callMainThreadEventName = "MithrilLynx:CallMainThread";
24
+ export const callMainThreadResultEventName = "MithrilLynx:CallMainThreadResult";
25
+ export const callBackgroundEventName = "MithrilLynx:CallBackground";
26
+ export const callBackgroundResultEventName = "MithrilLynx:CallBackgroundResult";
@@ -0,0 +1,388 @@
1
+ // internal/virtual-node.js
2
+ //
3
+ // Background-thread-only "virtual" mirror of LynxNodeWrapper/LynxStyleProxy
4
+ // (src/lynx-mithril-shim.js). Implements the EXACT SAME duck-typed DOM
5
+ // surface Mithril's render.js needs (per ../CONTRACT.md) — createElement,
6
+ // appendChild, style, addEventListener, etc — but backed by a plain-JS
7
+ // shadow tree instead of real Element PAPI calls, since the background
8
+ // thread has no PAPI access at all.
9
+ //
10
+ // Every node gets a unique `vid` (vid 0 is reserved for the page root, by
11
+ // convention shared with renderer/main-thread.js — no handshake needed).
12
+ // Every WRITE emits a serializable op addressed by vid; ops describe the
13
+ // SAME method calls a real LynxNodeWrapper would receive (createElement,
14
+ // appendChild, setProp, setAttribute, setStyleProps, ...), so
15
+ // renderer/main-thread.js's applyPatch() can replay them by calling those
16
+ // exact methods on real LynxNodeWrapper instances — no PAPI-mapping logic
17
+ // is duplicated here; the real wrapper's own (already-tested) setters do
18
+ // that mapping on replay.
19
+ //
20
+ // Because the render algorithm (factory() in the shim) is UNMODIFIED and
21
+ // only ever talks to `dom.*` methods generically, this file is a complete,
22
+ // independent implementation of the wrapper contract — it does not import
23
+ // or extend the real LynxNodeWrapper.
24
+
25
+ "use strict"
26
+
27
+ var DIRECT_PROPS = ["value", "checked", "selectedIndex", "className", "id", "type"]
28
+
29
+ function camelize(str) {
30
+ return str.replace(/-([a-z])/g, function (m, c) { return c.toUpperCase() })
31
+ }
32
+
33
+ // Mirrors the real LynxStyleProxy's registry exactly: render.js sometimes
34
+ // writes a style property via plain camelCase assignment
35
+ // (`element.style.fontSize = "20px"`) with no prior setProperty() call, so
36
+ // no accessor exists yet to trigger a flush on write. Flushing every
37
+ // registered proxy at the end of a render/redraw pass (mirroring the real
38
+ // shim's flushTree(), see createVirtualDocument's returned flushStyleProxies)
39
+ // is the safety net that catches those. The registry is scoped to one
40
+ // createVirtualDocument() call (passed in as `register`), not module-level —
41
+ // a real app only ever creates one document, but this keeps independent
42
+ // virtual trees (as in tests) from leaking proxies into each other.
43
+ function VirtualStyleProxy(wrapper, register) {
44
+ this._wrapper = wrapper
45
+ this._props = {}
46
+ register(this)
47
+ }
48
+
49
+ VirtualStyleProxy.prototype._flush = function () {
50
+ var styles = {}
51
+ var keys = Object.keys(this)
52
+ for (var i = 0; i < keys.length; i++) {
53
+ var key = keys[i]
54
+ if (key === "_wrapper" || key === "_props") continue
55
+ var value = this[key]
56
+ if (value == null) continue
57
+ styles[camelize(key)] = String(value)
58
+ }
59
+ this._wrapper._emit({ op: "setStyleProps", vid: this._wrapper._vid, styles: styles })
60
+ }
61
+
62
+ VirtualStyleProxy.prototype.setProperty = function (name, value) {
63
+ var key = camelize(name)
64
+ if (value == null || value === "") {
65
+ this.removeProperty(name)
66
+ return
67
+ }
68
+ var self = this
69
+ if (!Object.prototype.hasOwnProperty.call(this, key)) {
70
+ Object.defineProperty(this, key, {
71
+ configurable: true,
72
+ enumerable: true,
73
+ get: function () { return self._props[key] },
74
+ set: function (v) {
75
+ if (v == null || v === "") delete self._props[key]
76
+ else self._props[key] = String(v)
77
+ self._flush()
78
+ }
79
+ })
80
+ }
81
+ this._props[key] = String(value)
82
+ this._flush()
83
+ }
84
+
85
+ VirtualStyleProxy.prototype.removeProperty = function (name) {
86
+ var key = camelize(name)
87
+ delete this._props[key]
88
+ if (Object.prototype.hasOwnProperty.call(this, key)) delete this[key]
89
+ this._flush()
90
+ }
91
+
92
+ Object.defineProperty(VirtualStyleProxy.prototype, "cssText", {
93
+ get: function () {
94
+ var out = []
95
+ for (var key in this._props) out.push(key + ":" + this._props[key])
96
+ return out.join(";")
97
+ },
98
+ set: function (value) {
99
+ var self = this
100
+ Object.keys(this).forEach(function (key) {
101
+ if (key !== "_wrapper" && key !== "_props") delete self[key]
102
+ })
103
+ this._props = {}
104
+ if (value) {
105
+ // Only used for the shim's first-render `dom.style = ""` clear path
106
+ // in practice — a non-empty string assignment isn't exercised by
107
+ // render.js itself (see CONTRACT.md §f), so no parsing is needed here.
108
+ this._flush()
109
+ }
110
+ }
111
+ })
112
+
113
+ function VirtualNodeWrapper(vid, emit, registerStyleProxy) {
114
+ this._vid = vid
115
+ this._emit = emit
116
+ this._style = new VirtualStyleProxy(this, registerStyleProxy)
117
+ this._directProps = {}
118
+ this._listeners = Object.create(null)
119
+ this._isRawText = false
120
+ this._text = null
121
+ this._document = null
122
+ this._tag = null
123
+ this._parent = null
124
+ this._children = []
125
+ this.vnodes = null
126
+ }
127
+
128
+ Object.defineProperty(VirtualNodeWrapper.prototype, "nodeType", {
129
+ get: function () { return this._isRawText ? 3 : 1 }
130
+ })
131
+
132
+ Object.defineProperty(VirtualNodeWrapper.prototype, "ownerDocument", {
133
+ get: function () { return this._document }
134
+ })
135
+
136
+ Object.defineProperty(VirtualNodeWrapper.prototype, "namespaceURI", {
137
+ get: function () { return undefined }
138
+ })
139
+
140
+ Object.defineProperty(VirtualNodeWrapper.prototype, "parentNode", {
141
+ get: function () { return this._parent }
142
+ })
143
+
144
+ Object.defineProperty(VirtualNodeWrapper.prototype, "firstChild", {
145
+ get: function () { return this._children.length > 0 ? this._children[0] : null }
146
+ })
147
+
148
+ Object.defineProperty(VirtualNodeWrapper.prototype, "nextSibling", {
149
+ get: function () {
150
+ if (this._parent == null) return null
151
+ var siblings = this._parent._children
152
+ var index = siblings.indexOf(this)
153
+ return index >= 0 && index + 1 < siblings.length ? siblings[index + 1] : null
154
+ }
155
+ })
156
+
157
+ Object.defineProperty(VirtualNodeWrapper.prototype, "textContent", {
158
+ get: function () {
159
+ if (this._isRawText) return this._text
160
+ var out = ""
161
+ for (var i = 0; i < this._children.length; i++) out += this._children[i].textContent
162
+ return out
163
+ },
164
+ set: function (value) {
165
+ if (this._isRawText) {
166
+ this.nodeValue = value
167
+ return
168
+ }
169
+ // Mirrors the real wrapper: only ever called with "" (first-render
170
+ // clear). A genuinely non-empty assignment isn't exercised by
171
+ // render.js (see CONTRACT.md) so it's intentionally not implemented.
172
+ if (this._children.length > 0) {
173
+ var removed = this._children.slice()
174
+ for (var i = 0; i < removed.length; i++) this.removeChild(removed[i])
175
+ }
176
+ }
177
+ })
178
+
179
+ Object.defineProperty(VirtualNodeWrapper.prototype, "nodeValue", {
180
+ get: function () { return this._isRawText ? this._text : null },
181
+ set: function (value) {
182
+ if (this._isRawText) {
183
+ this._text = String(value)
184
+ this._emit({ op: "setText", vid: this._vid, value: this._text })
185
+ }
186
+ }
187
+ })
188
+
189
+ Object.defineProperty(VirtualNodeWrapper.prototype, "innerHTML", {
190
+ get: function () { return this.textContent },
191
+ set: function () {
192
+ throw new Error("m.trust / innerHTML is not supported by the Lynx shim (renderer mode).")
193
+ }
194
+ })
195
+
196
+ Object.defineProperty(VirtualNodeWrapper.prototype, "style", {
197
+ get: function () { return this._style },
198
+ set: function (value) {
199
+ if (value == null) this._style.cssText = ""
200
+ else if (typeof value === "string") this._style.cssText = value
201
+ }
202
+ })
203
+
204
+ DIRECT_PROPS.forEach(function (p) {
205
+ Object.defineProperty(VirtualNodeWrapper.prototype, p, {
206
+ configurable: true,
207
+ enumerable: true,
208
+ get: function () { return this._directProps[p] },
209
+ set: function (v) {
210
+ this._directProps[p] = v
211
+ this._emit({ op: "setProp", vid: this._vid, key: p, value: v })
212
+ }
213
+ })
214
+ })
215
+
216
+ VirtualNodeWrapper.prototype.setAttribute = function (key, value) {
217
+ this._emit({ op: "setAttribute", vid: this._vid, key: key, value: value == null ? null : String(value) })
218
+ }
219
+
220
+ VirtualNodeWrapper.prototype.removeAttribute = function (key) {
221
+ this._emit({ op: "removeAttribute", vid: this._vid, key: key })
222
+ }
223
+
224
+ VirtualNodeWrapper.prototype.setAttributeNS = function (ns, key, value) {
225
+ this.setAttribute(key, value)
226
+ }
227
+
228
+ VirtualNodeWrapper.prototype.appendChild = function (child) {
229
+ if (child == null) return child
230
+ if (child.nodeType === 11) return child // inert fragment
231
+ if (child._parent != null) {
232
+ var oldSiblings = child._parent._children
233
+ var oldIndex = oldSiblings.indexOf(child)
234
+ if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
235
+ }
236
+ this._children.push(child)
237
+ child._parent = this
238
+ this._emit({ op: "appendChild", parentVid: this._vid, childVid: child._vid })
239
+ return child
240
+ }
241
+
242
+ VirtualNodeWrapper.prototype.insertBefore = function (child, ref) {
243
+ if (child == null) return child
244
+ if (child.nodeType === 11) return child
245
+ if (ref == null) return this.appendChild(child)
246
+ if (child._parent != null) {
247
+ var oldSiblings = child._parent._children
248
+ var oldIndex = oldSiblings.indexOf(child)
249
+ if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
250
+ }
251
+ var refIndex = this._children.indexOf(ref)
252
+ this._children.splice(refIndex >= 0 ? refIndex : this._children.length, 0, child)
253
+ child._parent = this
254
+ this._emit({ op: "insertBefore", parentVid: this._vid, childVid: child._vid, refVid: ref._vid })
255
+ return child
256
+ }
257
+
258
+ VirtualNodeWrapper.prototype.removeChild = function (child) {
259
+ if (child == null) return child
260
+ if (child.nodeType === 11) return child
261
+ var index = this._children.indexOf(child)
262
+ if (index >= 0) this._children.splice(index, 1)
263
+ child._parent = null
264
+ this._emit({ op: "removeChild", parentVid: this._vid, childVid: child._vid })
265
+ return child
266
+ }
267
+
268
+ VirtualNodeWrapper.prototype.contains = function (other) {
269
+ if (other == null) return false
270
+ var node = other
271
+ while (node != null) {
272
+ if (node === this) return true
273
+ node = node._parent
274
+ }
275
+ return false
276
+ }
277
+
278
+ VirtualNodeWrapper.prototype.focus = function () {}
279
+
280
+ // Events: unlike the real wrapper (which wraps + registers a real PAPI
281
+ // listener per type), this only needs to know WHETHER a type transitions
282
+ // between "has a handler" and "has none" — that's what the main thread
283
+ // needs to know to attach/detach its forwarding listener. The actual
284
+ // listener function/object is resolved by dispatchEvent() at CALL time, not
285
+ // registration time, so Mithril swapping vnode.events' handlers across
286
+ // redraws (a very common pattern) never needs a new op.
287
+ VirtualNodeWrapper.prototype.addEventListener = function (type, listener) {
288
+ var hadListener = this._listeners[type] != null
289
+ this._listeners[type] = listener
290
+ if (!hadListener) this._emit({ op: "addEvent", vid: this._vid, type: type })
291
+ }
292
+
293
+ VirtualNodeWrapper.prototype.removeEventListener = function (type, listener) {
294
+ if (this._listeners[type] !== listener) return
295
+ delete this._listeners[type]
296
+ this._emit({ op: "removeEvent", vid: this._vid, type: type })
297
+ }
298
+
299
+ // Invoked when renderer/background.js receives a forwarded real-PAPI event.
300
+ VirtualNodeWrapper.prototype.dispatchEvent = function (ev) {
301
+ var listener = this._listeners[ev.type]
302
+ if (listener == null) return
303
+ if (typeof listener === "function") listener.call(ev.currentTarget, ev)
304
+ else if (typeof listener.handleEvent === "function") listener.handleEvent(ev)
305
+ }
306
+
307
+ function createRawTextNode(vid, emit, registerStyleProxy, value) {
308
+ var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
309
+ wrapper._isRawText = true
310
+ wrapper._text = String(value)
311
+ emit({ op: "createText", vid: vid, value: wrapper._text })
312
+ return wrapper
313
+ }
314
+
315
+ function createElementWrapper(vid, emit, registerStyleProxy, tag) {
316
+ var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
317
+ wrapper._tag = tag
318
+ emit({ op: "createElement", vid: vid, tag: tag })
319
+ return wrapper
320
+ }
321
+
322
+ // Inert fragment — same "never attached, children go straight to the real
323
+ // parent" contract as the real shim's createFragment().
324
+ function createFragment(document) {
325
+ return {
326
+ nodeType: 11,
327
+ ownerDocument: document,
328
+ appendChild: function () {},
329
+ insertBefore: function () {},
330
+ removeChild: function () {}
331
+ }
332
+ }
333
+
334
+ // createVirtualDocument(emit, onCreateNode) — emit(op) is called
335
+ // synchronously for every mutation; the caller (renderer/background.js) is
336
+ // responsible for batching and flushing the accumulated ops across the
337
+ // thread boundary. onCreateNode(wrapper) is called for every node created
338
+ // (root included) so the caller can index wrappers by vid. Each call gets
339
+ // its own independent style-proxy registry and vid counter — see
340
+ // VirtualStyleProxy's comment for why this isn't module-level.
341
+ function createVirtualDocument(emit, onCreateNode) {
342
+ var nextVid = 1 // vid 0 is reserved for the page root.
343
+ var notify = onCreateNode || function () {}
344
+ var styleProxies = []
345
+ var registerStyleProxy = function (proxy) { styleProxies.push(proxy) }
346
+
347
+ var document = {
348
+ createElement: function (tag) {
349
+ var wrapper = createElementWrapper(nextVid++, emit, registerStyleProxy, tag)
350
+ wrapper._document = document
351
+ notify(wrapper)
352
+ return wrapper
353
+ },
354
+ createElementNS: function (ns, tag) {
355
+ return document.createElement(tag)
356
+ },
357
+ createTextNode: function (value) {
358
+ var wrapper = createRawTextNode(nextVid++, emit, registerStyleProxy, value)
359
+ wrapper._document = document
360
+ notify(wrapper)
361
+ return wrapper
362
+ },
363
+ createDocumentFragment: function () {
364
+ return createFragment(document)
365
+ }
366
+ }
367
+ Object.defineProperty(document, "activeElement", {
368
+ get: function () { return null }
369
+ })
370
+
371
+ function createRootWrapper() {
372
+ var root = new VirtualNodeWrapper(0, emit, registerStyleProxy)
373
+ root._document = document
374
+ root._tag = "page"
375
+ notify(root)
376
+ return root
377
+ }
378
+
379
+ function flushStyleProxies() {
380
+ for (var i = 0; i < styleProxies.length; i++) {
381
+ try { styleProxies[i]._flush() } catch (e) { /* ignore */ }
382
+ }
383
+ }
384
+
385
+ return { document: document, createRootWrapper: createRootWrapper, flushStyleProxies: flushStyleProxies }
386
+ }
387
+
388
+ export { createVirtualDocument, VirtualNodeWrapper, VirtualStyleProxy }