mithril-lynx 0.0.8 → 2.0.0
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/.omo/plans/m-request-fetch-lynx.md +306 -0
- package/.omo/plans/m-route-en-memoria.md +397 -0
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
- package/FETCH_INVESTIGATION.md +307 -0
- package/README.md +32 -284
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +4 -27
- package/plugin.js +142 -359
- package/rstest.config.ts +27 -0
- package/src/apply-patch.js +179 -0
- package/src/backends/virtual-backend.js +80 -0
- package/src/background.d.ts +11 -0
- package/src/background.js +79 -0
- package/src/channel.js +41 -0
- package/src/commit.js +67 -0
- package/src/dev-reload-client.js +245 -0
- package/src/dev-transport-noop.js +10 -0
- package/src/fake-dom.js +374 -0
- package/src/main-thread.d.ts +1 -0
- package/src/main-thread.js +68 -0
- package/src/mount-redraw.js +67 -0
- package/src/patch-protocol.js +40 -0
- package/src/reload/version.js +28 -0
- package/src/request.d.ts +37 -0
- package/src/request.js +181 -0
- package/src/route.d.ts +33 -0
- package/src/route.js +207 -0
- package/test/end-to-end.test.ts +86 -0
- package/test/reload-version.test.ts +17 -0
- package/test/request.test.ts +182 -0
- package/test/route-hot-reload.test.ts +40 -0
- package/test/route.test.ts +152 -0
- package/test/setup.ts +25 -0
- package/test/structural-reload.test.ts +95 -0
- package/CONTRACT.md +0 -151
- package/LICENSE +0 -21
- package/background.d.ts +0 -54
- package/background.js +0 -169
- package/element.d.ts +0 -34
- package/element.js +0 -83
- package/gesture.d.ts +0 -40
- package/gesture.js +0 -117
- package/internal/constants.js +0 -26
- package/internal/virtual-node.js +0 -388
- package/list.d.ts +0 -31
- package/list.js +0 -185
- package/main-thread.d.ts +0 -43
- package/main-thread.js +0 -165
- package/navigation.d.ts +0 -35
- package/navigation.js +0 -76
- package/renderer/background.d.ts +0 -21
- package/renderer/background.js +0 -84
- package/renderer/main-thread.d.ts +0 -12
- package/renderer/main-thread.js +0 -175
- package/src/lynx-mithril-shim.d.ts +0 -16
- package/src/lynx-mithril-shim.js +0 -1505
- package/src/worklet-runtime.js +0 -82
- package/testing.d.ts +0 -10
- package/testing.js +0 -91
package/CONTRACT.md
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# Mithril 2.3.8 `render/render.js` — Extracted Contract
|
|
2
|
-
|
|
3
|
-
Source: `node_modules/mithril/render/render.js` (910 lines), mithril **2.3.8**.
|
|
4
|
-
All line numbers cite that file unless prefixed with `hyperscript.js:` (which cites `node_modules/mithril/render/hyperscript.js`).
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## a. Factory signature & how the render function is obtained
|
|
9
|
-
|
|
10
|
-
- **Line 8**: `module.exports = function() {` — the module exports a **zero-argument factory function**.
|
|
11
|
-
- The factory closes over module-level mutable state:
|
|
12
|
-
- `currentRedraw` (line 14) — the active `redraw` callback, captured by `EventDict` for auto-redraw.
|
|
13
|
-
- `currentRender` (line 15) — a per-render generation marker object (used by `delayedRemoval`).
|
|
14
|
-
- `currentDOM` (line 880) — the DOM node currently being rendered to (reentrancy lock).
|
|
15
|
-
- The **render function is the closure returned by the factory**: lines 882–909, `return function(dom, vnodes, redraw) { ... }`.
|
|
16
|
-
- Module dependencies (lines 3–6): `./vnode`, `./delayedRemoval`, `./domFor`, `./cachedAttrsIsStaticMap`.
|
|
17
|
-
- The factory pattern means each call to `require("mithril/render/render")()` produces an **independent renderer instance** with its own `currentRedraw`/`currentDOM`/`currentRender` state.
|
|
18
|
-
|
|
19
|
-
## b. Render function signature: `render(dom, vnodes, redraw)`
|
|
20
|
-
|
|
21
|
-
Defined at **line 882**: `return function(dom, vnodes, redraw) {`
|
|
22
|
-
|
|
23
|
-
| Param | Contract |
|
|
24
|
-
|---|---|
|
|
25
|
-
| `dom` | The DOM element to render into. **Line 883**: throws `TypeError("DOM element being rendered to does not exist.")` if falsy. **Lines 884–886**: throws `TypeError("Node is currently being rendered to and thus is locked.")` if `currentDOM != null && dom.contains(currentDOM)` (reentrancy guard). |
|
|
26
|
-
| `vnodes` | A vnode or array of vnodes. **Line 899**: normalized via `Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])`. |
|
|
27
|
-
| `redraw` | Optional. **Line 894**: `currentRedraw = typeof redraw === "function" ? redraw : undefined`. Consumed by `EventDict.handleEvent` (lines 811–817) to auto-redraw after events. |
|
|
28
|
-
|
|
29
|
-
Body sequence:
|
|
30
|
-
1. **Line 898**: first render into a node clears it — `if (dom.vnodes == null) dom.textContent = ""`.
|
|
31
|
-
2. **Line 900**: `updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)` — diffs old (`dom.vnodes`) vs new (`vnodes`); the XHTML namespace is normalized to `undefined` (which enables the property-key path in `hasPropertyKey`).
|
|
32
|
-
3. **Line 901**: `dom.vnodes = vnodes` — **prior vnodes are stored on the DOM node itself**.
|
|
33
|
-
4. **Line 903**: focus restoration — if `document.activeElement` changed and the old active element still has `.focus`, it is refocused.
|
|
34
|
-
5. **Line 904**: post-render hooks (`oncreate`/`onupdate`) flushed in order.
|
|
35
|
-
6. **Lines 905–908**: `finally` restores `currentRedraw`/`currentDOM` to their previous values.
|
|
36
|
-
|
|
37
|
-
## c. DOM surface accessed on the `dom` parameter
|
|
38
|
-
|
|
39
|
-
All access goes through the `dom` node passed to `render()` (or its descendants). `getDocument(dom)` (lines 17–19) returns `dom.ownerDocument`.
|
|
40
|
-
|
|
41
|
-
| API | Lines | Usage |
|
|
42
|
-
|---|---|---|
|
|
43
|
-
| `ownerDocument` | 18 | `getDocument()`; source of all document-level factories |
|
|
44
|
-
| `createTextNode` | 76 | `createText` — `vnode.dom = getDocument(parent).createTextNode(vnode.children)` |
|
|
45
|
-
| `createElement` / `createElementNS` | 120–122 | `createElement` for HTML, `createElementNS(ns, tag)` for svg/math; `{is: is}` third arg for custom elements |
|
|
46
|
-
| `createDocumentFragment` | 96, 104, 551 | `createHTML` (96), `createFragment` (104), `moveDOM` for multi-node moves (551) |
|
|
47
|
-
| `insertBefore` / `appendChild` | 558–561 | `insertDOM`: `insertBefore(dom, nextSibling)` if `nextSibling != null`, else `appendChild(dom)` |
|
|
48
|
-
| `removeChild` | 610–617 | `removeDOM`: single `removeChild(vnode.dom)` or per-node via `domFor` for fragments |
|
|
49
|
-
| `nodeValue` | 422 | `updateText` — `old.dom.nodeValue = vnode.children` |
|
|
50
|
-
| `value` | 654–658, 666, 699, 703 | Read for same-value coercion skips (input/textarea/select/option); written via generic `vnode.dom[key] = value` (666); select late-attrs (699, 703) |
|
|
51
|
-
| `checked` | 730 | Only as a key name in `isFormAttribute`; written via generic property path (666) |
|
|
52
|
-
| `selectedIndex` | 685, 699, 702, 707 | `removeAttr` guard (685), `setLateSelectAttrs` (699, 702, 707) |
|
|
53
|
-
| `className` | 672, 681, 693 | `setAttr` maps `className` → `"class"` attribute (672); `removeAttr` excludes it from property-null path (681) and maps to `"class"` (693) |
|
|
54
|
-
| `setAttribute` | 665, 669–670, 672 | input `type` (665), boolean attrs (669–670), generic attrs (672) |
|
|
55
|
-
| `removeAttribute` | 670, 693 | boolean-false (670), generic removal (693) |
|
|
56
|
-
| `setAttributeNS` | 645 | `xlink:`-prefixed keys → `setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value)` |
|
|
57
|
-
| `style` | 646, 678, 747–787 | `updateStyle` dual-mode (see §f) |
|
|
58
|
-
| `innerHTML` | 89, 92, 571 | `createHTML` (89 svg-wrapped, 92 plain), contenteditable sync (571) |
|
|
59
|
-
| `textContent` | 898 | First-render clear |
|
|
60
|
-
| `firstChild` | 90, 94, 98, 109 | `createHTML` unwrap (90, 94, 98), `createFragment` dom anchor (109) |
|
|
61
|
-
| `parentNode` | 730 | `isFormAttribute` — `option` whose parent is the active element |
|
|
62
|
-
| `contains` | 884 | Reentrancy lock check |
|
|
63
|
-
| `namespaceURI` | 891 | Namespace detection for the diff call |
|
|
64
|
-
| `focus` | 903 | Focus restoration |
|
|
65
|
-
| `nextSibling` | domFor.js:12 | Fragment iteration in `domFor` |
|
|
66
|
-
|
|
67
|
-
**Not used (verified by grep across the whole package):**
|
|
68
|
-
- `getAttribute` — render.js only *writes* attributes (`setAttribute`/`removeAttribute`/`setAttributeNS`); it never reads them.
|
|
69
|
-
- `nodeType` — appears nowhere in mithril. Do not rely on it in a reimplementation.
|
|
70
|
-
|
|
71
|
-
## d. Prior-vnode storage & diffing of repeated `render()` calls
|
|
72
|
-
|
|
73
|
-
**Storage**: old vnodes live on the DOM node as `dom.vnodes` — read at line 898 (first-render check) and 900 (diff input), written at line 901.
|
|
74
|
-
|
|
75
|
-
**`updateNodes(parent, old, vnodes, hooks, nextSibling, ns)`** (lines 270–395):
|
|
76
|
-
|
|
77
|
-
1. **Trivial cases** (271–273): `old === vnodes` or both null → no-op; `old` empty → create all; `vnodes` empty → remove all.
|
|
78
|
-
2. **Keyed detection** (275–276): lists are keyed iff `old[0].key != null` / `vnodes[0].key != null` (first non-null node, 278–279).
|
|
79
|
-
3. **Keyed/unkeyed mismatch** (280–282): remove all old + create all new.
|
|
80
|
-
4. **Unkeyed diff** (283–299): walk the common length index-by-index; `o === v` or both null → skip; null old → create; null new → remove; else `updateNode`. Tails handled by `removeNodes` (298) / `createNodes` (299).
|
|
81
|
-
5. **Keyed diff** (300–392), with the documented optimizations (comment block 184–268):
|
|
82
|
-
- **Bottom-up tail match** (305–312): while tail keys equal, update in place — identical tails are guaranteed part of the LIS, so no moves (tail optimization, comment 244–245).
|
|
83
|
-
- **Top-down head match** (314–320): same for the head.
|
|
84
|
-
- **Swaps & reversals** (322–336): two-node cross-swap fast path.
|
|
85
|
-
- **Bottom-up again** (338–345): re-check tails after head/tail consumption.
|
|
86
|
-
- **Leftovers** (346–347): remove remaining old or create remaining new.
|
|
87
|
-
- **LIS-based middle diff** (348–391): builds `oldIndices` (350–351), maps new keys → old indices via `getKeyMap` (352–365; impl 478–488), nulls matched old entries, removes unmatched old (367), creates all if nothing matched (368), then either moves non-LIS nodes (`makeLisIndices`, 370–383; impl 494–534, lifted from ivi) or a simple create loop when order was preserved (384–390).
|
|
88
|
-
6. **`getNextSibling`** (536–541): next sibling is found by scanning the *old* list forward from `i+1` for a node with a `dom` — this is what makes top-down DOM insertion correct.
|
|
89
|
-
7. **`moveDOM`** (544–556): moves single nodes directly; multi-node fragments are moved via a `createDocumentFragment` + `domFor` loop.
|
|
90
|
-
|
|
91
|
-
**`updateNode`** (396–419): same `tag` + same `is` → in-place update (state/events carried over at 399–400; `shouldNotUpdate` short-circuit at 401, impl 852–878); otherwise `removeNode` + `createNode`. Per-tag updates: `updateText` (420–425), `updateHTML` (426–435), `updateFragment` (436–450), `updateElement` (451–461), `updateComponent` (462–477).
|
|
92
|
-
|
|
93
|
-
## e. How `m()` (hyperscript) creates events & attrs
|
|
94
|
-
|
|
95
|
-
**Hyperscript side** (`render/hyperscript.js`):
|
|
96
|
-
- Selector parsing: `compileSelector` (hyperscript.js:21–42) — `#id`, `.class`, `[attr]`, `[attr=value]`; `class` → `className` (hyperscript.js:38); form-attribute keys (`value`/`checked`/`selectedIndex`/`selected`) mark the attrs object as non-static (hyperscript.js:17–19, 34).
|
|
97
|
-
- `class` attr → `className` (hyperscript.js:54–57); `input[type]` reordered first (hyperscript.js:69–74, workaround for #2622); `vnode.is = attrs.is` (hyperscript.js:77).
|
|
98
|
-
|
|
99
|
-
**Event side** (render.js):
|
|
100
|
-
- **Dispatch rule** (line 644): any attr key starting with `on` (`key[0] === "o" && key[1] === "n"`) is routed to `updateEvent` (826–842), never to the DOM attribute path.
|
|
101
|
-
- **`EventDict`** (800–823): a per-element event listener object, prototype `Object.create(null)` (804). Constructor captures `this._ = currentRedraw` (802). `handleEvent(ev)` (805–823):
|
|
102
|
-
- Looks up `this["on" + ev.type]` (806).
|
|
103
|
-
- Function handler → called with `ev.currentTarget` as `this` (808); object handler → `handler.handleEvent(ev)` (809).
|
|
104
|
-
- Auto-redraw: if `this._ != null` and `ev.redraw !== false`, calls the captured redraw (811–812); also after the handler's returned promise resolves (813–817).
|
|
105
|
-
- `return false` → `ev.preventDefault()` + `ev.stopPropagation()` (819–822).
|
|
106
|
-
- **`updateEvent`** (826–842): `addEventListener(key.slice(2), vnode.events, false)` — the **EventDict object itself is the listener** (831, 839); handlers stored as `vnode.events[key]`; removal via `removeEventListener` (834). `vnode.events` is carried across updates (line 400).
|
|
107
|
-
|
|
108
|
-
**Attribute side** (render.js):
|
|
109
|
-
- **`setAttr`** (642–674) precedence: skip `key`/null-value/lifecycle (643) → `on*` events (644) → `xlink:` (645) → `style` (646) → **`hasPropertyKey`** (647–666) → attribute fallback (667–673).
|
|
110
|
-
- **`hasPropertyKey`** (735–744): property assignment only when `ns === undefined` AND (custom element: tag contains `-` or `vnode.is`, OR key not in the browser-bug blacklist `href`/`list`/`form`/`width`/`height`) AND `key in vnode.dom`.
|
|
111
|
-
- Property path (666): `vnode.dom[key] = value`, with `value` coercion guards (648–663: input/textarea/select/option same-value skip; file-input read-only warning at 661) and `input[type]` forced through `setAttribute` (665).
|
|
112
|
-
- Attribute path (667–673): boolean → `setAttribute(key, "")` / `removeAttribute(key)`; else `setAttribute(key === "className" ? "class" : key, value)`.
|
|
113
|
-
- **`removeAttr`** (675–695): property-null path excludes `className`, `title`, `value` (option/select edge), `input[type]`; else `removeAttribute` with `className` → `"class"` mapping.
|
|
114
|
-
- **`updateAttrs`** (709–728): removals first (713–722), then sets (723–727); warns on reused attrs objects (714–716).
|
|
115
|
-
- **`isFormAttribute`** (729–731): `value`/`checked`/`selectedIndex`/`selected` (with active-element/option-parent conditions) — these bypass the `old === value` skip so form state always syncs.
|
|
116
|
-
|
|
117
|
-
## f. `updateStyle()` dual-mode (lines 747–787)
|
|
118
|
-
|
|
119
|
-
`updateStyle(element, old, style)`:
|
|
120
|
-
|
|
121
|
-
| Case | Lines | Behavior |
|
|
122
|
-
|---|---|---|
|
|
123
|
-
| `old === style` | 748–749 | No-op |
|
|
124
|
-
| `style == null` | 750–752 | `element.style = ""` (clear) |
|
|
125
|
-
| `typeof style !== "object"` | 753–755 | `element.style = style` (string passthrough) |
|
|
126
|
-
| `old` missing/string, `style` object | 756–766 | Clear, then for each key: **`key.includes("-")` → `element.style.setProperty(key, String(value))`** (763); **else `element.style[key] = String(value)`** (764) |
|
|
127
|
-
| Both objects | 767–786 | Remove stale keys first (772–777: `removeProperty` for dash-case, `= ""` for camelCase), then set changed keys (779–785: same dual-mode) |
|
|
128
|
-
|
|
129
|
-
Key contract points:
|
|
130
|
-
- **Dash-case keys** (`-` in the name) → `setProperty` / `removeProperty`; **camelCase keys** → direct `style[k]` assignment.
|
|
131
|
-
- All values coerced with `String()` (763–764, 781).
|
|
132
|
-
- Removal happens before setting (770–771) to avoid dash-case/camelCase aliasing bugs.
|
|
133
|
-
|
|
134
|
-
## g. Version & packaging
|
|
135
|
-
|
|
136
|
-
- **Version**: `2.3.8` (package.json `version` field).
|
|
137
|
-
- **No `main` / `module` field** in package.json (verified — only `unpkg`/`jsdelivr`/`repository`/`license`/`scripts`/`devDependencies`). The package is consumed by **file-path require**: `require("mithril/render/render")` resolves directly to `node_modules/mithril/render/render.js`.
|
|
138
|
-
- Entry points: `index.js` (browser bundle), `render.js` (top-level re-export of `render/render.js`), `hyperscript.js` (re-export of `render/hyperscript.js` + `trust`/`fragment`).
|
|
139
|
-
- 2.3.8 is the latest mithril release on npm (as of this scaffold's dependency pin).
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## Reimplementation checklist (what a Lynx port must honor)
|
|
144
|
-
|
|
145
|
-
1. Factory `module.exports = function()` returning `function(dom, vnodes, redraw)` with per-instance `currentRedraw`/`currentRender`/`currentDOM` state.
|
|
146
|
-
2. Store prior vnodes on the target node (`dom.vnodes`); first render clears via `textContent = ""`.
|
|
147
|
-
3. Diff pipeline: trivial cases → keyed detection → unkeyed walk → keyed (tail/head/swaps/LIS) → leftover create/remove.
|
|
148
|
-
4. DOM surface: `createTextNode`, `createElement(NS)`, `createDocumentFragment`, `insertBefore`/`appendChild`, `removeChild`, `nodeValue`, `value`, `checked`, `selectedIndex`, `className`, `setAttribute`/`removeAttribute`/`setAttributeNS`, `style`, `innerHTML`, `textContent`, `firstChild`, `parentNode`, `ownerDocument`, `namespaceURI`, `contains`, `focus`. **No `getAttribute`, no `nodeType`.**
|
|
149
|
-
5. Events: `on*` keys → single `EventDict` object per element registered via `addEventListener`; `handleEvent` dispatches by `ev.type`, binds `this` to `ev.currentTarget`, auto-redraws, honors `return false`.
|
|
150
|
-
6. Attrs: `hasPropertyKey` gate (property vs attribute), `className`→`class` mapping, boolean attrs, `xlink:` namespace, `value`/`checked`/`selectedIndex` form-attribute exceptions.
|
|
151
|
-
7. Styles: dual-mode `updateStyle` — `setProperty` for `-` keys, `style[k] = v` for camelCase, `String()` coercion, remove-before-set.
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 carlos-sweb
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/background.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Ambient declaration for the ESM background.js (the file itself is not
|
|
2
|
-
// type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
-
|
|
4
|
-
export type StoreData = Record<string, unknown>;
|
|
5
|
-
|
|
6
|
-
export interface SetDataOptions {
|
|
7
|
-
/** Set false to update the store without pushing a sync to the main thread. Defaults to true. */
|
|
8
|
-
shouldSyncToMainThread?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Returns the background thread's mutable data store. */
|
|
12
|
-
export function getData<T = StoreData>(): T;
|
|
13
|
-
|
|
14
|
-
/** Merges `patch` into the store, then (by default) pushes the changed keys to the main thread. */
|
|
15
|
-
export function setData(patch: StoreData, options?: SetDataOptions): void;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Registers the single handler invoked for every dispatchToBackground() call
|
|
19
|
-
* made from the main thread, as (handlerName, data).
|
|
20
|
-
*/
|
|
21
|
-
export function setBackgroundEventHandler(
|
|
22
|
-
handleEvent: (handlerName: string, data: unknown) => unknown,
|
|
23
|
-
): void;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Wires the background thread's core-context listeners for the
|
|
27
|
-
* main-thread <-> background-thread data channel. Call once, at
|
|
28
|
-
* background.ts's top level. See the project plan, Phase 3.
|
|
29
|
-
*/
|
|
30
|
-
export function setupBackground(): void;
|
|
31
|
-
|
|
32
|
-
export interface BackgroundRef {
|
|
33
|
-
/** Resolves with the native method's success data, rejects with its failure data. */
|
|
34
|
-
invoke(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A background-thread ref: imperative calls to a native element identified
|
|
39
|
-
* by a CSS selector, via lynx.createSelectorQuery(). See the project plan,
|
|
40
|
-
* Phase 5.
|
|
41
|
-
*/
|
|
42
|
-
export function createRef(selector: string): BackgroundRef;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Registers a handler main-thread.js's runOnBackground(key, ...args) can
|
|
46
|
-
* call by name. See the project plan, Phase 6.
|
|
47
|
-
*/
|
|
48
|
-
export function registerHandler(key: string, fn: (...args: unknown[]) => unknown): void;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Calls a handler main-thread.js registered via registerHandler(key, fn).
|
|
52
|
-
* Args and the resolved value must be JSON-serializable.
|
|
53
|
-
*/
|
|
54
|
-
export function runOnMainThread<T = unknown>(key: string, ...args: unknown[]): Promise<T>;
|
package/background.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// background.js
|
|
2
|
-
//
|
|
3
|
-
// Cross-thread "data-channel mode" adapter for the background thread (see
|
|
4
|
-
// the project plan, Phase 3). Ports
|
|
5
|
-
// lynx-examples/examples/vanilla/src/common/background/{setup,data,event}.ts
|
|
6
|
-
// into a single module — this side is framework-agnostic (Mithril never
|
|
7
|
-
// renders on the background thread in data-channel mode), so nothing here
|
|
8
|
-
// depends on the shim.
|
|
9
|
-
//
|
|
10
|
-
// A single mutable data store is diffed against its last-synced snapshot on
|
|
11
|
-
// every setData() call and only the changed keys are pushed to the main
|
|
12
|
-
// thread. Data received FROM the main thread is never echoed back (the
|
|
13
|
-
// reference implementation does, on every update after the first — a
|
|
14
|
-
// pure round-trip echo that's wasteful, and directly visible as a redundant
|
|
15
|
-
// extra Mithril redraw in this port, so it isn't reproduced here).
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
callBackgroundEventName,
|
|
19
|
-
callBackgroundResultEventName,
|
|
20
|
-
callMainThreadEventName,
|
|
21
|
-
callMainThreadResultEventName,
|
|
22
|
-
destroyLifetimeEventName,
|
|
23
|
-
dispatchEventToBackgroundEventName,
|
|
24
|
-
updateDataFromBackgroundEventName,
|
|
25
|
-
updateDataFromMainThreadEventName,
|
|
26
|
-
} from "./internal/constants.js";
|
|
27
|
-
|
|
28
|
-
const data = {};
|
|
29
|
-
let lastSyncedData = { ...data };
|
|
30
|
-
let handleBackgroundEvent;
|
|
31
|
-
|
|
32
|
-
export function getData() {
|
|
33
|
-
return data;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function setData(patch, options = {}) {
|
|
37
|
-
const { shouldSyncToMainThread = true } = options;
|
|
38
|
-
Object.assign(data, patch);
|
|
39
|
-
if (!shouldSyncToMainThread) {
|
|
40
|
-
lastSyncedData = { ...data };
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
sendToMainThread();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function sendToMainThread() {
|
|
47
|
-
const patch = {};
|
|
48
|
-
for (const [key, value] of Object.entries(data)) {
|
|
49
|
-
if (value !== lastSyncedData[key]) patch[key] = value;
|
|
50
|
-
}
|
|
51
|
-
if (Object.keys(patch).length === 0) return;
|
|
52
|
-
lastSyncedData = { ...data };
|
|
53
|
-
lynx.getCoreContext().dispatchEvent({
|
|
54
|
-
type: updateDataFromBackgroundEventName,
|
|
55
|
-
data: patch,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// handleEvent(handlerName, data) — called for every dispatchToBackground()
|
|
60
|
-
// call made from the main thread. Only one handler at a time; Phase 6 of the
|
|
61
|
-
// project plan generalizes this into a full string-keyed registry with
|
|
62
|
-
// call/return correlation.
|
|
63
|
-
export function setBackgroundEventHandler(handleEvent) {
|
|
64
|
-
handleBackgroundEvent = handleEvent;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function setupBackground() {
|
|
68
|
-
const coreContext = lynx.getCoreContext();
|
|
69
|
-
|
|
70
|
-
const onUpdateDataFromMainThread = (event) => {
|
|
71
|
-
const incoming = event.data;
|
|
72
|
-
if (!incoming || typeof incoming !== "object" || Array.isArray(incoming)) return;
|
|
73
|
-
// Never echo data back to the thread it just came from — only
|
|
74
|
-
// background-initiated setData() calls (elsewhere in app code)
|
|
75
|
-
// should sync to the main thread.
|
|
76
|
-
setData(incoming, { shouldSyncToMainThread: false });
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const onDispatchToBackground = (event) => {
|
|
80
|
-
const payload = event.data;
|
|
81
|
-
if (!payload || typeof payload.handlerName !== "string") return;
|
|
82
|
-
handleBackgroundEvent?.(payload.handlerName, payload.data);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const cleanup = () => {
|
|
86
|
-
coreContext.removeEventListener(updateDataFromMainThreadEventName, onUpdateDataFromMainThread);
|
|
87
|
-
coreContext.removeEventListener(dispatchEventToBackgroundEventName, onDispatchToBackground);
|
|
88
|
-
coreContext.removeEventListener(destroyLifetimeEventName, cleanup);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
coreContext.addEventListener(updateDataFromMainThreadEventName, onUpdateDataFromMainThread);
|
|
92
|
-
coreContext.addEventListener(dispatchEventToBackgroundEventName, onDispatchToBackground);
|
|
93
|
-
coreContext.addEventListener(destroyLifetimeEventName, cleanup);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Refs (project plan, Phase 5): the background thread has no direct native
|
|
97
|
-
// handle, so imperative calls go through Lynx's existing selector-query
|
|
98
|
-
// bridge (the same primitive ReactLynx's own background-thread refs
|
|
99
|
-
// ultimately bottom out on) rather than a new protocol.
|
|
100
|
-
export function createRef(selector) {
|
|
101
|
-
return {
|
|
102
|
-
invoke(method, params) {
|
|
103
|
-
return new Promise((resolve, reject) => {
|
|
104
|
-
lynx.createSelectorQuery()
|
|
105
|
-
.select(selector)
|
|
106
|
-
.invoke({
|
|
107
|
-
method,
|
|
108
|
-
params: params || {},
|
|
109
|
-
success: (data) => resolve(data),
|
|
110
|
-
fail: (data) => reject(data),
|
|
111
|
-
})
|
|
112
|
-
.exec();
|
|
113
|
-
});
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Cross-thread function registry (project plan, Phase 6 — worklet
|
|
119
|
-
// substitute). See main-thread.js's registerHandler()/runOnBackground() for
|
|
120
|
-
// the full rationale; this is the mirror image for the reverse direction.
|
|
121
|
-
// Lazy setup on first use, same reasoning as main-thread.js.
|
|
122
|
-
const backgroundHandlers = new Map();
|
|
123
|
-
const pendingMainThreadCalls = new Map();
|
|
124
|
-
let nextCallId = 1;
|
|
125
|
-
let crossThreadCallsReady = false;
|
|
126
|
-
|
|
127
|
-
function ensureCrossThreadCalls() {
|
|
128
|
-
if (crossThreadCallsReady) return;
|
|
129
|
-
crossThreadCallsReady = true;
|
|
130
|
-
const coreContext = lynx.getCoreContext();
|
|
131
|
-
|
|
132
|
-
coreContext.addEventListener(callBackgroundEventName, (event) => {
|
|
133
|
-
const { callId, key, args } = event.data;
|
|
134
|
-
const fn = backgroundHandlers.get(key);
|
|
135
|
-
let result;
|
|
136
|
-
let error;
|
|
137
|
-
try {
|
|
138
|
-
result = fn ? fn(...args) : undefined;
|
|
139
|
-
} catch (e) {
|
|
140
|
-
error = e instanceof Error ? e.message : String(e);
|
|
141
|
-
}
|
|
142
|
-
coreContext.dispatchEvent({ type: callBackgroundResultEventName, data: { callId, result, error } });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
coreContext.addEventListener(callMainThreadResultEventName, (event) => {
|
|
146
|
-
const { callId, result, error } = event.data;
|
|
147
|
-
const pending = pendingMainThreadCalls.get(callId);
|
|
148
|
-
if (pending == null) return;
|
|
149
|
-
pendingMainThreadCalls.delete(callId);
|
|
150
|
-
if (error != null) pending.reject(new Error(error));
|
|
151
|
-
else pending.resolve(result);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Registers a handler main-thread.js's runOnBackground(key, ...) can call by name. */
|
|
156
|
-
export function registerHandler(key, fn) {
|
|
157
|
-
ensureCrossThreadCalls();
|
|
158
|
-
backgroundHandlers.set(key, fn);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Calls a handler main-thread.js registered via registerHandler(key, fn), by name. Args must be JSON-serializable. */
|
|
162
|
-
export function runOnMainThread(key, ...args) {
|
|
163
|
-
ensureCrossThreadCalls();
|
|
164
|
-
return new Promise((resolve, reject) => {
|
|
165
|
-
const callId = nextCallId++;
|
|
166
|
-
pendingMainThreadCalls.set(callId, { resolve, reject });
|
|
167
|
-
lynx.getCoreContext().dispatchEvent({ type: callMainThreadEventName, data: { callId, key, args } });
|
|
168
|
-
});
|
|
169
|
-
}
|
package/element.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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
|
-
// "" not undefined when clearing: real hardware's FiberSetClasses
|
|
43
|
-
// rejects a non-string argument ("FiberSetClasses param 1 should be
|
|
44
|
-
// String") — see lynx-mithril-shim.js's own matching fix for the
|
|
45
|
-
// same call.
|
|
46
|
-
if (name === "class") __SetClasses(handle, value == null ? "" : String(value));
|
|
47
|
-
else if (name === "id") __SetID(handle, value == null ? null : String(value));
|
|
48
|
-
else if (name.slice(0, 5) === "data-") __AddDataset(handle, name.slice(5), value);
|
|
49
|
-
else __SetAttribute(handle, name, value == null ? null : value);
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
querySelector(selector, params) {
|
|
53
|
-
return wrapRef(__QuerySelector(handle, selector, params || {}));
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
querySelectorAll(selector, params) {
|
|
57
|
-
return __QuerySelectorAll(handle, selector, params || {}).map((ref) => wrapRef(ref));
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
animate(keyframes, options) {
|
|
61
|
-
__ElementAnimate(handle, [ANIMATION_OPERATION.START, (options && options.name) || "", keyframes, options]);
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
playAnimation(name) {
|
|
65
|
-
__ElementAnimate(handle, [ANIMATION_OPERATION.PLAY, name]);
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
pauseAnimation(name) {
|
|
69
|
-
__ElementAnimate(handle, [ANIMATION_OPERATION.PAUSE, name]);
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
cancelAnimation(name) {
|
|
73
|
-
__ElementAnimate(handle, [ANIMATION_OPERATION.CANCEL, name]);
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
/** Always resolves with { code, data } — check `code` yourself; PAPI's success/failure convention isn't assumed here. */
|
|
77
|
-
invoke(method, params) {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
__InvokeUIMethod(handle, method, params || {}, (res) => resolve(res));
|
|
80
|
-
});
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
package/gesture.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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;
|