mithril-lynx 2.6.0 → 3.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/CHANGELOG.md +50 -0
- package/FETCH_INVESTIGATION.md +3 -2
- package/INPUT.md +115 -0
- package/LICENSE.txt +21 -0
- package/PLAN_REVIEW.md +134 -0
- package/README.md +47 -7
- package/REQUEST.md +5 -2
- package/ROUTE.md +111 -16
- package/ROUTE_CONTRACT_ANALYSIS.md +224 -0
- package/UPDATE_ANIMATION_GAP.md +133 -0
- package/informe-contrato-mithril-lynx.md +206 -0
- package/package.json +1 -9
- package/plugin.js +33 -0
- package/prompts-analisis-contrato-mithril-lynx.md +141 -0
- package/src/apply-patch.js +408 -68
- package/src/backends/virtual-backend.js +120 -22
- package/src/background.d.ts +40 -1
- package/src/background.js +116 -7
- package/src/channel.js +32 -7
- package/src/commit.js +9 -0
- package/src/dev-reload-client.js +46 -0
- package/src/dev-transport-noop.js +3 -0
- package/src/fake-dom.js +559 -61
- package/src/list-attributes.js +69 -0
- package/src/list-runtime.js +425 -0
- package/src/main-thread.js +33 -3
- package/src/mount-redraw.d.ts +10 -0
- package/src/mount-redraw.js +69 -3
- package/src/patch-protocol.js +52 -13
- package/src/reload/version.js +10 -1
- package/src/request.d.ts +2 -0
- package/src/request.js +59 -3
- package/src/route.d.ts +52 -5
- package/src/route.js +236 -59
- package/src/testing.d.ts +14 -1
- package/src/testing.js +79 -11
- package/test/event-render-coalescing.test.ts +117 -0
- package/test/gesture.test.ts +75 -0
- package/test/input.test.ts +388 -0
- package/test/list.test.ts +422 -100
- package/test/patch-protocol.test.ts +41 -0
- package/test/remove-event.test.ts +26 -0
- package/test/remove-subtree-cleanup.test.ts +75 -0
- package/test/route-hot-reload.test.ts +4 -2
- package/test/route.test.ts +206 -6
- package/test/setup.ts +10 -0
- package/test/style-clear.test.ts +97 -0
- package/.omo/plans/m-request-fetch-lynx.md +0 -306
- package/.omo/plans/m-route-en-memoria.md +0 -397
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +0 -548
- package/.omo/run-continuation/ses_f48265d07ffenKG0GAz9ZRFvY6.json +0 -10
- package/src/list-cell.d.ts +0 -20
- package/src/list-cell.js +0 -73
- package/src/list-support.d.ts +0 -13
- package/src/list-support.js +0 -171
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
Everything since 2.6.2.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- **Native `<list>`:** `list` and `list-item` are ordinary Mithril elements. The `Op.CreateList`/`Op.SetListItems` protocol and the `mithril-lynx/list-cell` / `mithril-lynx/list-support` entry points are gone. Render `m("list", …, items.map((it) => m("list-item", { key: it.id, "item-key": it.id }, …)))` instead. See the README.
|
|
10
|
+
- **`route.set()` is asynchronous,** as in Mithril's `m.route`. The history changes right away, but the new screen resolves on the next microtask, and `route.get()` returns the previous path until then. Several `set()` calls in the same tick resolve only the last one.
|
|
11
|
+
- **`route(defaultRoute, routes)` requires `defaultRoute`** and throws a `TypeError` without it. Route templates with badly separated params (`/:a:b`) throw a `SyntaxError`, as upstream does.
|
|
12
|
+
- **Native events on `<input>`/`<textarea>`:** an `input` event that doesn't change the text is no longer forwarded. That covers native's echo of `setValue`, and the `input ""` a `<textarea>` fires on creation. `input` now means "the text changed", as on the web.
|
|
13
|
+
- **The patch protocol is now `0x4d4e`:** the main-thread and background bundles must be rebuilt together.
|
|
14
|
+
|
|
15
|
+
### New
|
|
16
|
+
|
|
17
|
+
- **`<input>` / `<textarea>`:**
|
|
18
|
+
- `value` works like the web's controlled input: sent with the native `setValue` only when it differs from the field, never echoing typed text, and guarded against racing keystrokes.
|
|
19
|
+
- `autofocus` focuses the field once, on creation.
|
|
20
|
+
- See [`INPUT.md`](./INPUT.md).
|
|
21
|
+
- **Native UI methods on any element:** `vnode.dom.invoke(method, params)` returns a promise, and `vnode.dom.focus()` / `vnode.dom.blur()` call the native methods. They run on the main thread right after the patch's flush, so they're safe from `oncreate`, with no `id`, `setTimeout` or selector query (new ops `Op.InvokeUIMethod`, `Op.SetInputValue`).
|
|
22
|
+
- **Native `<list>`:**
|
|
23
|
+
- every `<list>` / `<list-item>` attribute is passed with its real type (`src/list-attributes.js`);
|
|
24
|
+
- items are attached only when native asks for them (`src/list-runtime.js`, a port of @lynx-js/react's element-template list);
|
|
25
|
+
- verified on device up to ~2,000 items.
|
|
26
|
+
- **`route`:**
|
|
27
|
+
- `options.state`;
|
|
28
|
+
- a changed `:key` param recreates the page;
|
|
29
|
+
- `route.canGoBack()`;
|
|
30
|
+
- `route.listenBackButton()` for an opt-in Android back button, with a Kotlin host recipe in [`ROUTE.md`](./ROUTE.md).
|
|
31
|
+
- **`mithril-lynx/testing`:** a stand-in for `__InvokeUIMethod`, plus `uiMethodCalls` and `setUIMethodResponder()`.
|
|
32
|
+
|
|
33
|
+
### Fixes
|
|
34
|
+
|
|
35
|
+
- **`route.Link`:** runs its lifecycle hooks once, honors `e.preventDefault()` and `{ handleEvent }` listeners, and doesn't copy `key`/hooks onto the rendered element.
|
|
36
|
+
- **`route` redirects:** a redirect from inside a render (`route.set()` in `oninit`) no longer throws "Node is currently being rendered to".
|
|
37
|
+
- **`route.get()`:** returns the decoded path.
|
|
38
|
+
- **Removed subtrees:** release every descendant's bookkeeping on both threads, where before only the root's was released.
|
|
39
|
+
- **Gesture worklets:** unregistered when their detector or element goes away.
|
|
40
|
+
- **Clearing a whole style** (`style` removed, or replaced by a string) expands into per-property removals.
|
|
41
|
+
- **List removals:** a removed on-screen list item is detached only after native processed its removal (a device-verified crash fix).
|
|
42
|
+
|
|
43
|
+
### Performance
|
|
44
|
+
|
|
45
|
+
- The automatic redraw after a high-frequency event (`gesturemove`, `touchmove`, `scroll`) is coalesced to one per frame. Every handler still runs.
|
|
46
|
+
|
|
47
|
+
### Known gaps
|
|
48
|
+
|
|
49
|
+
- `<list update-animation="default">` is not safe on lists that remove on-screen items — see [`UPDATE_ANIMATION_GAP.md`](./UPDATE_ANIMATION_GAP.md).
|
|
50
|
+
- `route` has no deep-link support (an initial path from the host) yet.
|
package/FETCH_INVESTIGATION.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Documento de referencia, no un plan — el plan que consumió esta
|
|
4
4
|
investigación (con su narrativa de spikes y decisiones) está en
|
|
5
|
-
|
|
5
|
+
`.omo/plans/m-request-fetch-lynx.md` (retirado del árbol en `71a6670`; se lee con
|
|
6
|
+
`git show 71a6670^:.omo/plans/m-request-fetch-lynx.md`).
|
|
6
7
|
Este archivo es el consolidado técnico: qué es cada API, qué dice cada
|
|
7
8
|
fuente, y qué se confirmó de verdad en un device real — para consultar
|
|
8
9
|
sin tener que reconstruir la investigación de nuevo.
|
|
@@ -302,6 +303,6 @@ los 4-5 puntos sin equivalente, con una sugerencia directa de usar
|
|
|
302
303
|
- `fetch` de Lynx (doc): <https://lynxjs.org/api/lynx-api/global/fetch.html>
|
|
303
304
|
- Tipos reales: `@lynx-js/types/types/background-thread/{fetch,lynx}.d.ts`
|
|
304
305
|
- Código fuente real de `m.request`: `node_modules/mithril/request/request.js`
|
|
305
|
-
- Plan que usa esta investigación:
|
|
306
|
+
- Plan que usa esta investigación: `.omo/plans/m-request-fetch-lynx.md` (en el historial de git: `git show 71a6670^:.omo/plans/m-request-fetch-lynx.md`)
|
|
306
307
|
- Implementación resultante: `src/request.js` (ver ese archivo para el
|
|
307
308
|
estado actual de qué de esta tabla ya está construido)
|
package/INPUT.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# `<input>`, `<textarea>` and native UI methods
|
|
2
|
+
|
|
3
|
+
Since mithril-lynx 3.0.0.
|
|
4
|
+
|
|
5
|
+
On the web, a Mithril field is just attributes:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
m("input", { value: name, autofocus: true, oninput: (e) => (name = e.target.value) })
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Lynx's native `<input>` and `<textarea>` have no `value` or `autofocus` attribute. The text is set and focus is given only through **UI methods** (`setValue`, `focus`, `blur`, `getValue`, `setSelectionRange`), which native exposes through `lynx.createSelectorQuery().select("#id").invoke(...)`. Until now that meant:
|
|
12
|
+
|
|
13
|
+
- giving the field an `id`;
|
|
14
|
+
- calling the selector query by hand;
|
|
15
|
+
- wrapping the call in a `setTimeout`, because in `oncreate` the element doesn't exist natively yet.
|
|
16
|
+
|
|
17
|
+
mithril-lynx now does this for you, so the web form works as is:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
m("input", {
|
|
21
|
+
value: name, // sent with setValue when it changes
|
|
22
|
+
autofocus: true, // focuses once, on creation
|
|
23
|
+
placeholder: "Name",
|
|
24
|
+
oninput: (e) => (name = e.detail.value), // Lynx puts the payload under `detail`
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## `value`
|
|
29
|
+
|
|
30
|
+
- `value` works like the web's controlled input. When the value you render differs from what the field holds, mithril-lynx sends one `setValue`.
|
|
31
|
+
- The field's `vnode.dom.value` is synced from every event it fires (`input`, `focus`, `blur`, `confirm`) before your handler runs.
|
|
32
|
+
- Echo suppression is built in:
|
|
33
|
+
- Mithril only writes `value` when it differs from `dom.value`, so text the user just typed is never sent back to native, and the cursor doesn't jump.
|
|
34
|
+
- A value your app *changes* (clearing the field, forcing uppercase, trimming) is sent once.
|
|
35
|
+
- Setting `value` to `null`/`undefined`, or dropping the attribute, clears the field (as on the web).
|
|
36
|
+
- To leave the field **uncontrolled**, don't pass `value`. Read the text from `e.detail.value` in `oninput`, from `vnode.dom.value`, or with `vnode.dom.invoke("getValue")`.
|
|
37
|
+
- For an initial text only, pass `value` on the first render and stop passing it afterwards.
|
|
38
|
+
|
|
39
|
+
### Keystrokes racing a programmatic value
|
|
40
|
+
|
|
41
|
+
Your handler runs on the background thread, while the user keeps typing on the main thread. Take a transform like `toUpperCase()`: by the time `"AB"` reaches native, the user may have typed `"abc"`, and sending `"AB"` would erase the `c`.
|
|
42
|
+
|
|
43
|
+
To prevent that:
|
|
44
|
+
- the main thread counts each field's native `input` events and forwards the count with every event;
|
|
45
|
+
- a `setValue` carries the count it was computed from;
|
|
46
|
+
- a `setValue` computed before the latest keystroke is dropped, and that keystroke's own event re-renders and sends the up-to-date value.
|
|
47
|
+
|
|
48
|
+
`@lynx-js/lynx-ui-input` solves the same race by locking the field `readonly` on the main thread while the value round-trips.
|
|
49
|
+
|
|
50
|
+
## Focus
|
|
51
|
+
|
|
52
|
+
- `autofocus: true` focuses the field once, when it is created (HTML semantics). Redraws never refocus it.
|
|
53
|
+
- `vnode.dom.focus()` and `vnode.dom.blur()` call the native methods. They are safe in `oncreate`: the call travels with the patch that creates the element and runs right after that patch is flushed.
|
|
54
|
+
- Called from a timer or a promise, outside any render, the call is sent on its own at the end of the current task.
|
|
55
|
+
- Mithril's post-render focus restoration never triggers them (the fake document has no `activeElement`).
|
|
56
|
+
|
|
57
|
+
Use `autofocus`, not `focus: true`. `focus` is the element's method, and an attribute with that name replaces it.
|
|
58
|
+
|
|
59
|
+
## `vnode.dom.invoke(method, params)` — any element's UI methods
|
|
60
|
+
|
|
61
|
+
This is the promise form of `createSelectorQuery().select(...).invoke(...)`, for any element, with no `id`:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const { value, selectionStart } = await field.invoke("getValue");
|
|
65
|
+
await scroller.invoke("scrollTo", { offset: 0, smooth: true }); // <scroll-view>
|
|
66
|
+
await list.invoke("scrollToPosition", { position: 10 }); // <list>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- It resolves with the method's `data`.
|
|
70
|
+
- It rejects with an `Error` whose `code`/`data` are native's, or when the element is removed before the call runs.
|
|
71
|
+
- `setSelectionRange(start, end)` is a shorthand on fields.
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
`src/fake-dom.js` gives `input`/`textarea` a real `value` property. `focus()`, `blur()` and `invoke()` record two new patch ops:
|
|
76
|
+
- `Op.SetInputValue` (`id, value, seq`);
|
|
77
|
+
- `Op.InvokeUIMethod` (`id, method, params, callbackId`).
|
|
78
|
+
|
|
79
|
+
`src/apply-patch.js` queues them and runs them with the main-thread PAPI `__InvokeUIMethod`:
|
|
80
|
+
- they run after the patch's `__FlushElementTree()`, then flush once more;
|
|
81
|
+
- this is the order ReactLynx's main-thread `Element.invoke()` uses.
|
|
82
|
+
|
|
83
|
+
Results come back on the forwarded-event channel as a reserved `mithrilLynx:invokeResult` event.
|
|
84
|
+
|
|
85
|
+
How other frameworks handle this:
|
|
86
|
+
- **Vue Lynx** 0.5.1: `v-model` writes a `value` attribute and calls `setValue`, with no race guard.
|
|
87
|
+
- **`@lynx-js/lynx-ui-input`**: calls `setValue` from a React effect.
|
|
88
|
+
- **ReactLynx core**: no special handling.
|
|
89
|
+
|
|
90
|
+
## Known limits
|
|
91
|
+
|
|
92
|
+
- **IME composition:** `e.detail.isComposing` is passed through. A controlled field that *transforms* the text while composing (for example, uppercase during CJK input) will interrupt the composition. Skip the transform while `isComposing` is true.
|
|
93
|
+
- **`setValue`'s `cursor` param** (iOS/Android) isn't exposed through `value`. Use `invoke("setValue", { value, cursor })`.
|
|
94
|
+
- `<input>`/`<textarea>` need the XElement input artifacts on the Android/iOS host (see mithril-lynx-ui's README); without them the field renders at zero size.
|
|
95
|
+
- **`input` fires only when the text changes, as on the web.** Native fires `input` events by itself, and mithril-lynx drops them on the main thread before they reach your handler. Two cases seen on Android:
|
|
96
|
+
- every `setValue` is echoed back as an `input`;
|
|
97
|
+
- a `<textarea>` fires `input ""` (twice) while it is first flushed, before the patch's `setValue` runs.
|
|
98
|
+
|
|
99
|
+
Forwarded, the `""` would reach your `oninput` and wipe a controlled field's state. It would also count as a keystroke, so the race guard above would drop the field's initial text. An event that only changes `isComposing` is still forwarded.
|
|
100
|
+
|
|
101
|
+
## Device verification
|
|
102
|
+
|
|
103
|
+
Checked on a Samsung SM-A075M (Android), Lynx Go, over `rspeedy dev`:
|
|
104
|
+
|
|
105
|
+
- **Initial value:** the initial `value` on an `<input>` and on a `<textarea>` shows on first paint, with no `id` or timer.
|
|
106
|
+
- **Autofocus:** `autofocus` leaves the caret in the field.
|
|
107
|
+
- **Fast typing, plain field:** `adb shell input text` with 37 characters produced the exact text, with no lost characters.
|
|
108
|
+
- **Fast typing, uppercase field:** 45 characters, each one round-tripping a `setValue`, came out exactly right, with the caret staying at the end.
|
|
109
|
+
- **Clear:** a `value` cleared from a button tap empties the field, and typing afterwards works.
|
|
110
|
+
- **mithril-lynx-ui components:** its `Input` (controlled, uppercase: 21 fast characters) and its `TextArea` with `defaultValue` both work. The `TextArea` is the case that exposed the spurious `input ""` above.
|
|
111
|
+
- **`getValue`:** `invoke("getValue")` resolves `{ value, selectionStart, selectionEnd, isComposing }`.
|
|
112
|
+
- **Focus and blur:** `dom.blur()` / `dom.focus()` hide and show the keyboard (`dumpsys input_method` reports `mInputShown`).
|
|
113
|
+
- **No crashes:** no `Fatal signal`.
|
|
114
|
+
|
|
115
|
+
Unit tests: `test/input.test.ts`.
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Enrique Illesca Monsalve
|
|
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/PLAN_REVIEW.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Análisis del contrato de mithril-lynx — informe y mejoras priorizadas
|
|
2
|
+
Objetivo y criterios de éxito
|
|
3
|
+
|
|
4
|
+
Ejecutar PROMPT 1 (Maestro) del archivo prompts-analisis-contrato-mithril-lynx.md: analizar el contrato del proyecto en sus 4 capas (API pública, protocolo wire, compatibilidad Mithril upstream, invariantes internos) y proponer mejoras priorizadas, sin reescrituras de arquitectura. Criterio de éxito: cada afirmación anclada a archivo:línea; cada mejora con Problema → Propuesta (boceto si aplica) → Por qué; divergencias doc↔código marcadas como hallazgos de primer orden.
|
|
5
|
+
|
|
6
|
+
Entregable al aprobar: escribir el informe completo en informe-contrato-mithril-lynx.md y marcarlo con present. No se implementan cambios de código en esta tarea (el prompt pide propuestas, no parches).
|
|
7
|
+
1. Mapa del contrato
|
|
8
|
+
Capa Dónde vive Qué define
|
|
9
|
+
API pública del paquete package.json exports (líneas 7–44) + los 9 .d.ts 9 entry points (background, main-thread, plugin, route, request, mount-redraw, testing, list-support, list-cell); el .d.ts ES el contrato
|
|
10
|
+
Protocolo wire bg↔main src/patch-protocol.js (Op 16–53, OP_ARITY 70–89, forEachOp 94–101), src/channel.js (nombres 18–21, payloads 24–41) codificación plana [opcode, ...args]; 4 eventos (MithrilLynx:Patch, MithrilLynx:Event, __RenderPage, __DestroyLifetime); id space espejado 1:1, id 0 = página
|
|
11
|
+
Compat Mithril upstream src/route.js, src/request.js + ROUTE.md/REQUEST.md/FETCH_INVESTIGATION.md fidelidad de m.route/m.request; divergencias documentadas vs ocultas
|
|
12
|
+
Invariantes internos src/commit.js, src/mount-redraw.js, src/background.js hook único de commit, registro/debounce del redraw, ciclo de vida de renderApp
|
|
13
|
+
2. Fortalezas (conservar)
|
|
14
|
+
|
|
15
|
+
Fail-fast en el commit — commit.js: install() lanza si ya hay callback (34–42) y commit() lanza si no hay mount (55–65). Concreto y testeable.
|
|
16
|
+
id space explícito — id 0 reservado para la página (fake-dom.js 396–408, apply-patch.js 208–228); documentado en patch-protocol.js 10–14.
|
|
17
|
+
Codificación plana con tabla de aridad + forEachOp que lanza en opcode desconocido (patch-protocol.js 94–101) — sin asignación por op en el hot path.
|
|
18
|
+
Opciones no soportadas de request lanzan de inmediato con puntero a docs (request.js 27–45) y además son errores de tipo vía never en request.d.ts 15–22. Excelente doble capa.
|
|
19
|
+
Trabajo alrededor del timer de Lynx honestamente documentado — el REDRAW_DELAY_MS=50 se reconoce como margen empírico, no garantía (mount-redraw.js 34–46, REQUEST.md §"timer quirk", FETCH_INVESTIGATION.md §4.6).
|
|
20
|
+
Un solo modelo de hilos impuesto por construcción — background.js es el único que corre render.js; main-thread.js solo replica (background.js 3–5, main-thread.js 1–9).
|
|
21
|
+
back()/forward() declarados explícitamente como adiciones no-upstream (ROUTE.md 43, 66) — honestidad sobre divergencia.
|
|
22
|
+
resolveRoute/Link/SKIP espejan upstream (route.js 50–98, 177–201), minimizando el costo de portar.
|
|
23
|
+
Verificación en device separada de tests unitarios (README.md §Testing, REQUEST.md §Testing).
|
|
24
|
+
|
|
25
|
+
3. Mejoras propuestas, priorizadas
|
|
26
|
+
🔴 Alta — previenen fallos silenciosos o corrupción
|
|
27
|
+
|
|
28
|
+
R1. El protocolo wire no lleva versión ni handshake.
|
|
29
|
+
|
|
30
|
+
Problema (evidencia): Op es un enum numérico congelado (patch-protocol.js 16–53) sin campo de versión; sendPatchToMainThread/sendEventToBackground despachan {type, data} sin versión (channel.js 24–26, 39–41); no hay handshake en setupRenderer() (main-thread.js 35–68). applyPatch lanza en default (apply-patch.js 408–409), pero solo capta opcodes desconocidos, no opcodes reordenados por un bundle main-thread cacheado/stale. Un HMR parcial o un bundle cacheado desincroniza opcodes silenciosamente (un id leído como tag, etc.).
|
|
31
|
+
Propuesta: prefijar cada commit con un entero de versión de protocolo (p. ej. sendPatchToMainThread([PROTOCOL_VERSION, ...ops]) y validarlo en onPatch), o emitir un evento de handshake en setupRenderer; mismatch → throw con mensaje naming.
|
|
32
|
+
Por qué: protege el invariante "id space 1:1 y opcode↔PAPI" contra desync silencioso, coherente con fail-fast. (Mitigación parcial existente: ambos bundles se compilan juntos; aún así un cache parcial rompe la garantía.)
|
|
33
|
+
|
|
34
|
+
R2. Un segundo renderApp() corrompe el id space y pisa el slot de redraw sin protestar.
|
|
35
|
+
|
|
36
|
+
Problema (evidencia): mount-redraw.register sobrescribe currentRedraw silenciosamente (mount-redraw.js 57–59); cada renderApp crea un backend con nextId = 1 (virtual-backend.js 12) y un documento con id 0 (fake-dom.js 401); background.js registra un listener de eventos por cada renderApp (65); el main thread tiene un único applier (main-thread.js 50–56). Dos renderApp → dos espacios de ids colisionando en el mismo Map de handles. commit.js solo protege el mismo controller (34–42); un segundo renderApp crea un controller nuevo y pasa desapercibido.
|
|
37
|
+
Propuesta: register() lanza si currentRedraw != null (fail-fast), y/o renderApp() rehúsa una segunda llamada en el mismo contexto.
|
|
38
|
+
Por qué: convierte un uso indebido en error inmediato en vez de corrupción invisible del estado de render.
|
|
39
|
+
|
|
40
|
+
R3. Op.RemoveEvent es un no-op explícito → handler disparado N veces al re-adjuntar condicionalmente.
|
|
41
|
+
|
|
42
|
+
Problema (evidencia): apply-patch.js 367–375 deja RemoveEvent como no-op + TODO ("PAPI has no documented __RemoveEventListener"); fake-dom.js 299–309 emite AddEvent/RemoveEvent; render.js upstream (fuente real, updateEvent) llama removeEventListener cuando el handler pasa a null y addEventListener al re-añadir. Quitar y volver a poner un handler en un elemento que no se remueve acumula listeners nativos; cada uno reenvía el evento a background, que lo despacha al único handler actual → el handler se ejecuta N veces por tap. (testing.js 45–47 ya implementa __RemoveEventListener, pero apply-patch nunca lo llama → dead code.)
|
|
43
|
+
Propuesta: guardar el handle del listener y llamar __RemoveEventListener cuando exista; si no existe en el runtime real, lanzar en RemoveEvent en vez de no-op.
|
|
44
|
+
Por qué: elimina un fallo de comportamiento silencioso (doble disparo) de la clase que el proyecto dice no cometer. (Inferencia: la cadena remove→re-add→multi-fire se deduce de la fuente upstream + fake-dom; conviene confirmarla con un test antes de parchear.)
|
|
45
|
+
|
|
46
|
+
🟡 Media — divergencias de comportamiento o de documentación
|
|
47
|
+
|
|
48
|
+
M1. route.set() antes de route() pierde la navegación silenciosamente.
|
|
49
|
+
|
|
50
|
+
Problema: route.set muta history incondicionalmente (route.js 135–140) pero la resolución está detrás de if (ready) (141); route() luego resetea history = [defaultRoute] (119). Upstream en este caso navega (location.href = prefix + path). La navegación se descarta sin señal.
|
|
51
|
+
Propuesta: lanzar (o encolar) cuando route.set se llama con ready === false, en vez de descartar.
|
|
52
|
+
Por qué: corrige una pérdida silenciosa de estado que contradice fail-fast.
|
|
53
|
+
|
|
54
|
+
M2. Una segunda llamada a route() resetea el historial → HMR del módulo de rutas devuelve al usuario a la pantalla inicial.
|
|
55
|
+
|
|
56
|
+
Problema: history = [defaultRoute]; historyIndex = 0 se ejecuta incondicionalmente en cada route() (route.js 119–120). ROUTE.md 71 documenta el patrón HMR con re-resolución, así que recompilar la tabla borra la pila back/forward y la ruta actual.
|
|
57
|
+
Propuesta: resetear el historial solo en la primera llamada (p. ej. if (!ready) { history = [defaultRoute]; historyIndex = 0; }).
|
|
58
|
+
Por qué: protege el invariante "historial en memoria = sesión" durante HMR.
|
|
59
|
+
|
|
60
|
+
M3. README dice que gestures + listas virtualizadas "no se portaron", pero están implementados y exportados.
|
|
61
|
+
|
|
62
|
+
Problema: README.md 18 ("Deliberately not carried over… gestures, list virtualization"); pero package.json 36–43 exporta list-support/list-cell; patch-protocol.js 43–52 define ops 14–17; apply-patch.js/list-cell.js/list-support.js los implementan. testing.js 27–28 también dice "no gesture/list support… don't exist yet". Además list-support.d.ts dice "not meant to be imported directly" siendo un export público. Documentación internamente contradictoria; el surface real es mayor que el documentado.
|
|
63
|
+
Propuesta: reconciliar README/testing.js con la realidad; documentar que gestures/listas existen pero no están verificadas en device (ya lo admite apply-patch.js 87–91).
|
|
64
|
+
Por qué: elimina una contradicción doc↔código de primer orden; evita que un consumidor asuma que no existen (o que ya están verificadas).
|
|
65
|
+
|
|
66
|
+
M4. err.message en no-2xx diverge de upstream (statusText vs responseText).
|
|
67
|
+
|
|
68
|
+
Problema: request.js 150 usa new Error(typeof data === "string" ? data : response.statusText); upstream usa ev.target.responseText (cuerpo crudo). Para un body JSON de error, upstream pone el JSON crudo como message; aquí queda statusText. REQUEST.md §"Error shape" dice "Matches real m.request" y describe statusText, lo cual describe este código pero no a upstream.
|
|
69
|
+
Propuesta: usar el texto crudo del body como message (o corregir el claim de REQUEST.md).
|
|
70
|
+
Por qué: la forma del error es parte del contrato documentado como "igual a upstream".
|
|
71
|
+
|
|
72
|
+
M5. preventDefault/stopPropagation del evento entregado a los handlers son no-ops sin documentar.
|
|
73
|
+
|
|
74
|
+
Problema: background.js 69 construye el evento con preventDefault() {}/stopPropagation() {}; ningún doc user-facing lo menciona. Código web portado que llama e.preventDefault() no hace nada silenciosamente (consistente con que dispatchEvent no hace bubbling, fake-dom.js 317–322, pero no documentado).
|
|
75
|
+
Propuesta: documentarlo en README/ROUTE.md (o lanzar donde importe).
|
|
76
|
+
Por qué: cierra una brecha de contrato visible para el usuario.
|
|
77
|
+
|
|
78
|
+
M6. REDRAW_DELAY_MS = 50 fijo y dependiente de dispositivo, no configurable.
|
|
79
|
+
|
|
80
|
+
Problema: mount-redraw.js 47. Documentado como empírico (REQUEST.md), pero sin setter/opción; el timeout de request sí usa el mismo timer con valor del usuario (request.js 120–124), el margen del redraw no.
|
|
81
|
+
Propuesta: exponer un setter de configuración (p. ej. configure({redrawDelayMs})).
|
|
82
|
+
Por qué: convierte una constante mágica observable en configurable, sin tocar el diseño.
|
|
83
|
+
|
|
84
|
+
M7. Op.SetListItems embebe JSON.stringify(cells) en un array que ya viaja como objeto JS nativo.
|
|
85
|
+
|
|
86
|
+
Problema: virtual-backend.js 84 hace JSON.stringify(cells); apply-patch.js 405 hace JSON.parse. Redundante en ambos extremos y obliga a que cells sea JSON-safe (lanza con BigInt/undefined).
|
|
87
|
+
Propuesta: pasar cells directo (eliminar stringify/parse).
|
|
88
|
+
Por qué: coherencia con el resto del protocolo (valores nativos), menos CPU y sin restricción innecesaria.
|
|
89
|
+
|
|
90
|
+
M8. OP_ARITY y el switch inline de apply-patch pueden divergir; no hay test de round-trip.
|
|
91
|
+
|
|
92
|
+
Problema: patch-protocol.js 70–89 declara OP_ARITY como "single source of truth", pero apply-patch.js 249–410 re-implementa la aridad inline (reconocido en 65–69). forEachOp lanza en opcode desconocido (98) pero no detecta aridad incorrecta; list-cell.js findTopLevelChildIds (28–34) depende de OP_ARITY.
|
|
93
|
+
Propuesta: test que recorra cada opcode por OP_ARITY y por applyPatch (round-trip), o derivar una tabla de la otra.
|
|
94
|
+
Por qué: protege el invariante "dos lectores del mismo encoding" contra drift silencioso.
|
|
95
|
+
|
|
96
|
+
🟢 Baja — ergonomía, tipos, docs
|
|
97
|
+
|
|
98
|
+
L1. route.d.ts importa Component de "mithril", que no es dependencia.
|
|
99
|
+
|
|
100
|
+
Problema: route.d.ts 1 import type { Component } from "mithril"; package.json solo declara mithril-runtime (peer 51 / dev 61). Sin mithril instalado, los tipos no resuelven.
|
|
101
|
+
Propuesta: importar de "mithril-runtime".
|
|
102
|
+
|
|
103
|
+
L2. type en request.d.ts tipado como new (data: any) => T (resultado entero), pero se aplica por elemento en arrays.
|
|
104
|
+
|
|
105
|
+
Problema: request.d.ts 13 vs applyType en request.js 54–58 (map por elemento, igual que upstream — verificado en fuente). El tipo miente para respuestas array.
|
|
106
|
+
Propuesta: documentar/tipar el comportamiento por-elemento.
|
|
107
|
+
|
|
108
|
+
L3. extract + type juntos divergen de upstream.
|
|
109
|
+
|
|
110
|
+
Problema: upstream aplica type después de extract (fuente real); request.js 139–143 hace return temprano en extract y salta applyType. Divergencia real no documentada (caso raro).
|
|
111
|
+
Propuesta: aplicar applyType al resultado de extract, o documentar la diferencia.
|
|
112
|
+
|
|
113
|
+
L4. El listener sobre el signal del caller nunca se remueve.
|
|
114
|
+
|
|
115
|
+
Problema: request.js 115–118 añade options.signal.addEventListener("abort", ...) sin removeEventListener; un signal compartido longevo acumula un closure por request.
|
|
116
|
+
Propuesta: remover el listener al settle.
|
|
117
|
+
|
|
118
|
+
L5. background.d.ts tipa document como unknown (background.d.ts 8) — expuesto para tests/HMR, pero obliga a as/cast. Propuesta: exportar un tipo mínimo de LynxDocument.
|
|
119
|
+
4. Orden de implementación sugerido
|
|
120
|
+
|
|
121
|
+
R2 (guard en renderApp/register) — más barato, mayor riesgo de corrupción.
|
|
122
|
+
R1 (versión de protocolo) — barato, previene desync silencioso.
|
|
123
|
+
R3 (RemoveEvent) — requiere test de confirmación + fix real.
|
|
124
|
+
M1 + M2 (historial de route) — correctitud del ciclo de vida.
|
|
125
|
+
M4 + M5 (mensaje de error + no-ops de evento documentados).
|
|
126
|
+
M3 (reconciliación README/testing vs código).
|
|
127
|
+
M6 + M7 + M8 (redraw configurable, serialización de listas, test OP_ARITY).
|
|
128
|
+
L1–L5 (tipos/ergonomía).
|
|
129
|
+
|
|
130
|
+
Supuestos y notas
|
|
131
|
+
|
|
132
|
+
El análisis de compat upstream se hizo contra la fuente real de Mithril 2.3.8 (misma base que mithril-runtime), no contra memoria.
|
|
133
|
+
Hipótesis refutadas con evidencia (del PROMPT 3 del archivo): H5 (deserialize recibe body ya parseado ≠ upstream) es falsa — upstream también pasa el objeto ya parseado con responseType:"json" (default) y texto crudo con "text"; mithril-lynx coincide. H6 (type sobre arrays difiere) es falsa — upstream hace new type(item) por elemento, exactamente como applyType. Ambas se documentan como "verificado y coincidente", no como divergencia.
|
|
134
|
+
Confirmadas: H1 (sin versión/handshake), H2 (segundo renderApp corrompe), H3 (route.set pre-route se pierde), H4 (segundo route() resetea historial), H8 (REDRAW_DELAY_MS fijo), H9 (preventDefault/stopPropagation no-ops), H10 (doble serialización de listas).
|
package/README.md
CHANGED
|
@@ -4,18 +4,37 @@ Mithril.js rendered through [Lynx](https://lynxjs.org)'s Element PAPI, with genu
|
|
|
4
4
|
|
|
5
5
|
> **Note:** this is a complete, from-scratch rewrite of the previous `mithril-lynx` (internally, "v2") — not an incremental patch on top of it. The old implementation is retired; no more fixes land against that design. See "Why a rewrite" below for exactly what justified starting over instead of patching it again.
|
|
6
6
|
|
|
7
|
+
## What's new in 3.0.0
|
|
8
|
+
|
|
9
|
+
- **Native `<list>` / `<list-item>` as ordinary Mithril elements** — keyed items, every attribute with its real type, attached on demand by native. Breaking: replaces `Op.CreateList`/`Op.SetListItems` and the `list-cell`/`list-support` entry points. See below.
|
|
10
|
+
- **`<input>` / `<textarea>` like on the web** — `value`, `autofocus`, and `vnode.dom.focus()`/`blur()`/`invoke(method, params)` for any element's native UI methods, with no `id`, `setTimeout` or selector query. See [`INPUT.md`](./INPUT.md).
|
|
11
|
+
- **`route` parity with Mithril's `m.route`** — asynchronous `set()`, `options.state`, `:key` remounts, `route.Link` fixes, plus `canGoBack()` and an opt-in Android back button (`listenBackButton()`). Breaking: `set()` now resolves on the next microtask. See [`ROUTE.md`](./ROUTE.md).
|
|
12
|
+
- Leak fixes, render coalescing for high-frequency events, and a style-clear fix.
|
|
13
|
+
|
|
14
|
+
Full list: [`CHANGELOG.md`](./CHANGELOG.md). The main-thread and background bundles must be rebuilt together (patch protocol `0x4d4e`).
|
|
15
|
+
|
|
7
16
|
## Why a rewrite
|
|
8
17
|
|
|
9
18
|
The old implementation's core bugs all traced back to the same root cause: whether a redraw actually reached the main thread depended on runtime conditions (`typeof globalThis.__FlushElementTree === "function"`, which render mode was active, load order) instead of a fixed contract. Every fix added another conditional on top of the last one. Reading ReactLynx's own source (not just its docs) showed its actual patch-channel/reload design is a different architecture, not an incremental improvement on the old one — so this rewrite starts over with that contract from the first commit:
|
|
10
19
|
|
|
11
20
|
- **One thread model, not three.** The old version had main-thread-owned, data-channel, and renderer modes. This one has exactly one: the real `mithril/render/render.js` (via [`mithril-runtime`](https://github.com/carlos-sweb/mithril-runtime)) always runs on the **background** thread against a virtual tree; the **main thread** only ever replays patches onto real Element PAPI nodes and forwards native events back. No app view code ever runs on the main thread.
|
|
12
21
|
- **One explicit commit hook, not a conditional global.** `src/commit.js` installs exactly one commit callback per `renderApp()` lifetime, set up once by the code that owns the render. Asking to commit before one is installed throws immediately, on the same tick, with a message naming exactly what's missing — never a silently frozen screen.
|
|
13
|
-
- **Three reload modes, correctly separated** (
|
|
22
|
+
- **Three reload modes, correctly separated** (the full device-verified story is in the rewrite plan — see "Reference docs"):
|
|
14
23
|
- **A — data reload**: text/props/CSS edits. Already validated in the old version; rebuilt on the new core.
|
|
15
24
|
- **B — structural reload**: adding/removing/reordering tree nodes. The old version assumed this *had* to be a full reload; this one lets Mithril's own real diff (running in the background against a real tree) produce the right Create/Insert/Remove ops instead — no separate wire-protocol mode needed, just reconciliation that doesn't discard nodes that didn't change.
|
|
16
25
|
- **C — full reload**: fallback for what A/B can't resolve (new imports, changed dependencies, an unrecoverable error). Same CDP `Page.reload` mechanism stabilized in the old version's 0.0.9, rewritten on the new core.
|
|
17
26
|
|
|
18
|
-
**
|
|
27
|
+
**Carried over as code, not yet device-verified**: gestures (`src/apply-patch.js`'s `Op.SetGestureDetector`). The new arena-claim gesture path is explicitly unverified on a real device (see the note in `apply-patch.js`). The native `<list>` below is device-verified.
|
|
28
|
+
|
|
29
|
+
**Native `<list>` (3.0.0+)**: `list` and `list-item` are ordinary Mithril elements — render them like any other, key the items, and Mithril's own keyed diff drives the native list:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
m("list", { "list-type": "single", "span-count": 1, "scroll-orientation": "vertical",
|
|
33
|
+
"lower-threshold-item-count": 2, onscrolltolower: loadMore },
|
|
34
|
+
items.map((it) => m("list-item", { key: it.id, "item-key": it.id }, row(it))))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Every documented `<list>` attribute (`bounces`, `item-snap`, `sticky`, `update-animation`, …) and `<list-item>` attribute (`full-span`, `sticky-top`/`-bottom`, `estimated-main-axis-size-px`, `reuse-identifier`, `recyclable`) is passed with its real type — booleans and objects included (`src/list-attributes.js` is the catalog). Events use the usual `on*` attrs (`onscroll`, `onscrolltoupper`, `onscrolltolower`, `onscrollstatechange`, `onlayoutcomplete`, `onsnap`); methods (`scrollToPosition`, `scrollBy`, `autoScroll`, `getVisibleCells`) are called with `vnode.dom.invoke(method, params)` (see "Forms and native element methods"), or a selector query on the list's `id`. `item-key` must be unique and stable — use the same value as `key`. **Known gap:** `update-animation="default"` is not safe yet on lists that remove on-screen items — see [`UPDATE_ANIMATION_GAP.md`](./UPDATE_ANIMATION_GAP.md). The main-thread side (`src/list-runtime.js`) follows @lynx-js/react's element-template list: each item keeps its own element tree and is attached only when native asks for it. 3.0.0 removes the previous `Op.CreateList`/`Op.SetListItems` protocol and the `mithril-lynx/list-cell` / `mithril-lynx/list-support` entry points. **Deliberately not carried over at all (yet)**: the old stack-based `navigation` module (in-memory `route` plus the opt-in Android back button cover navigation — see [`ROUTE.md`](./ROUTE.md)); v1's imperative ref helpers are replaced by `vnode.dom.invoke()`/`focus()`/`blur()`. Those were real, device-verified capabilities in v1 — this rewrite's scope so far is specifically the redraw/reload core plus routing and networking (see below). Reimplementing the rest on this core is future work, not something this rewrite claims to already cover.
|
|
19
38
|
|
|
20
39
|
## Usage
|
|
21
40
|
|
|
@@ -53,6 +72,10 @@ renderApp({ root: () => m(Counter) });
|
|
|
53
72
|
|
|
54
73
|
`m.request`, reimplemented as a wrapper over Lynx's own `fetch`. See [`REQUEST.md`](./REQUEST.md) for the full API, and [`FETCH_INVESTIGATION.md`](./FETCH_INVESTIGATION.md) for the complete option-by-option gap analysis against the real `m.request` spec, backed by real-device evidence rather than docs/types alone (which were wrong twice during that investigation).
|
|
55
74
|
|
|
75
|
+
## Forms and native element methods
|
|
76
|
+
|
|
77
|
+
`m("input", { value, autofocus: true, oninput })` works as on the web: `value` is sent with the native `setValue` method only when it differs from what the field holds (text the user typed is never echoed back), `autofocus` focuses once on creation, and every element's `vnode.dom` has `focus()`, `blur()` and a promise-based `invoke(method, params)` for native UI methods — no `id`, `setTimeout` or `createSelectorQuery()`. See [`INPUT.md`](./INPUT.md).
|
|
78
|
+
|
|
56
79
|
## Custom fonts
|
|
57
80
|
|
|
58
81
|
Use a plain CSS `@font-face` rule — not `lynx.addFont()` (that JS API only fires post-mount, too late to win the first-frame race). Three gotchas, all confirmed on real hardware and inherited unchanged from the previous mithril-lynx (none of this is architecture-specific):
|
|
@@ -65,6 +88,7 @@ Use a plain CSS `@font-face` rule — not `lynx.addFont()` (that JS API only fir
|
|
|
65
88
|
|
|
66
89
|
- **`m.trust`** — not present. Stripped from `mithril-runtime` at the source, and Lynx's Element PAPI has no innerHTML-equivalent injection point to reimplement it against anyway (same permanent gap v1 documented).
|
|
67
90
|
- **A handful of `m.request` options with no `fetch` equivalent** (`config`, `async: false`, `user`/`password`, `withCredentials`) throw immediately with a message pointing at `FETCH_INVESTIGATION.md`, rather than silently behaving differently — see `REQUEST.md`.
|
|
91
|
+
- **The event object passed to handlers is a synthesized snapshot, not a live DOM event.** Lynx has no default actions: `preventDefault()` only sets `e.defaultPrevented` (which `route.Link` honors, as upstream does), and `stopPropagation()` is a no-op — events do not bubble; the fake DOM (`src/fake-dom.js`) dispatches directly to the single node the native event targeted. Code ported from the web that relies on `e.preventDefault()` stopping a browser action (e.g. form submit) has nothing to stop. `e.redraw = false` still works, and is how `route.Link` opts out of the post-tap redraw.
|
|
68
92
|
|
|
69
93
|
## Testing
|
|
70
94
|
|
|
@@ -72,12 +96,28 @@ Use a plain CSS `@font-face` rule — not `lynx.addFont()` (that JS API only fir
|
|
|
72
96
|
npm test
|
|
73
97
|
```
|
|
74
98
|
|
|
75
|
-
Runs against `@lynx-js/testing-environment`'s real Element PAPI simulation via `rstest` — `test/end-to-end.test.ts` and `test/structural-reload.test.ts` exercise real Mithril diff + real patch replay, not mocks. Device-only claims (focus/text surviving a structural reload on a real `<input>`, the three reload modes triggering correctly over a live dev session) are verified separately on a connected Android device and logged in
|
|
99
|
+
Runs against `@lynx-js/testing-environment`'s real Element PAPI simulation via `rstest` — `test/end-to-end.test.ts` and `test/structural-reload.test.ts` exercise real Mithril diff + real patch replay, not mocks. Device-only claims (focus/text surviving a structural reload on a real `<input>`, the three reload modes triggering correctly over a live dev session) are verified separately on a connected Android device and logged in the rewrite plan's §8 (see "Reference docs"), not re-asserted here. Device checks of the 3.0.0 features are recorded in each feature's own doc (`INPUT.md`, `ROUTE.md`, `UPDATE_ANIMATION_GAP.md`).
|
|
76
100
|
|
|
77
101
|
## Reference docs
|
|
78
102
|
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
- [`ROUTE.md`](./ROUTE.md)
|
|
103
|
+
User-facing:
|
|
104
|
+
|
|
105
|
+
- [`CHANGELOG.md`](./CHANGELOG.md) — what changed in each release, breaking changes first.
|
|
106
|
+
- [`ROUTE.md`](./ROUTE.md) — `mithril-lynx/route`: the in-memory `m.route`, asynchronous `set()`, `route.Link`, and the opt-in Android back button with its Kotlin host recipe.
|
|
107
|
+
- [`INPUT.md`](./INPUT.md) — `<input>`/`<textarea>` (`value`, `autofocus`) and `vnode.dom.invoke()`/`focus()`/`blur()` for any element's native UI methods.
|
|
108
|
+
- [`REQUEST.md`](./REQUEST.md) — `mithril-lynx/request`: `m.request` over Lynx's `fetch`.
|
|
83
109
|
- [`ANDROID_APK_GUIDE.md`](./ANDROID_APK_GUIDE.md) — building a native Android host and APK from scratch, Gradle-CLI only, including the `.ttf` cold-start hack from "Custom fonts" above. Automated end to end by `create-mithril-lynx --android`.
|
|
110
|
+
|
|
111
|
+
Investigations and known gaps:
|
|
112
|
+
|
|
113
|
+
- [`ROUTE_CONTRACT_ANALYSIS.md`](./ROUTE_CONTRACT_ANALYSIS.md) — `route` checked against Mithril 2.3.8's `m.route`, item by item, plus what Lynx offers for native navigation.
|
|
114
|
+
- [`FETCH_INVESTIGATION.md`](./FETCH_INVESTIGATION.md) — the option-by-option gap analysis of `m.request` against Lynx's `fetch`, backed by real-device evidence.
|
|
115
|
+
- [`UPDATE_ANIMATION_GAP.md`](./UPDATE_ANIMATION_GAP.md) — the open `<list update-animation>` gap: device evidence, hypotheses and next steps.
|
|
116
|
+
|
|
117
|
+
Historical plans (removed from the tree in `71a6670`, still in git history):
|
|
118
|
+
|
|
119
|
+
- `mithril-lynx-v2-desde-cero.md` — the full rewrite plan: architecture decisions, the three reload modes' device verification, and the postmortem on exactly what v1 got wrong.
|
|
120
|
+
- `m-route-en-memoria.md` — how `m.route` was first designed and verified for an in-memory, URL-less environment.
|
|
121
|
+
- `m-request-fetch-lynx.md` — the `m.request`-vs-`fetch` investigation plan and its execution log.
|
|
122
|
+
|
|
123
|
+
Read one with `git show 71a6670^:.omo/plans/<file>`.
|
package/REQUEST.md
CHANGED
|
@@ -28,6 +28,7 @@ Matches real `m.request`: GET by default, `:param` interpolation in the URL (reu
|
|
|
28
28
|
| `extract` | `(response, options) => any` — bypasses the status check entirely, same as upstream's `(xhr, options) => any`. Signature changes (`response` instead of `xhr`) since there's no XHR object; the purpose is identical. |
|
|
29
29
|
| `type` | Constructor applied to the result, unchanged from upstream. |
|
|
30
30
|
| `timeout` | Real cancellation, not just giving up on waiting — backed by `AbortController`, confirmed on device to actually tear down the in-flight connection (aborting 800ms into a 5-second server-side delay rejected at ~805ms, not 5000ms). |
|
|
31
|
+
| `signal` | `AbortSignal` — linked into the request's own `AbortController`, so a caller-provided signal aborts the request exactly like `.abort()`/`timeout`. Not part of real `m.request` (which only reached `xhr.abort()` via `config`); a natural addition here for the same reason `.abort()` is. |
|
|
31
32
|
| `background` | Same as upstream: skip the automatic redraw. |
|
|
32
33
|
| `.abort()` | **Not part of real `m.request`'s API** — a bonus method on the returned promise, since Lynx's `AbortController` makes it a real, working cancellation (real `m.request` only exposes this indirectly, through `config(xhr) => xhr.abort()`, which has no equivalent here — see below). |
|
|
33
34
|
|
|
@@ -45,16 +46,18 @@ These have no `fetch` equivalent on Lynx. Passing any of them throws right away,
|
|
|
45
46
|
|
|
46
47
|
## Error shape
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
`err.code` (the HTTP status) and `err.response` (the already-parsed body) match real `m.request` on a non-2xx response:
|
|
49
50
|
|
|
50
51
|
```js
|
|
51
52
|
request("/missing").catch((err) => {
|
|
52
53
|
err.code; // response.status
|
|
53
|
-
err.message; // response.statusText, unless the body
|
|
54
|
+
err.message; // response.statusText, unless the parsed body is a plain string
|
|
54
55
|
err.response; // the already-parsed body
|
|
55
56
|
});
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
**One small divergence to be aware of:** real `m.request` sets `err.message` to the raw `responseText` (the body as-is), whereas this wrapper uses `response.statusText` unless the parsed body happens to be a string. `fetch`'s `Response` is single-use — once `.json()` has consumed it, the raw text is gone — so matching upstream's raw-body message would require reading the body as text first; that's not done here. See `FETCH_INVESTIGATION.md` for the full option-by-option comparison.
|
|
60
|
+
|
|
58
61
|
## Testing
|
|
59
62
|
|
|
60
63
|
Two separate layers, deliberately not mixed:
|