mithril-lynx 2.5.0 → 2.6.2
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/run-continuation/ses_f2f02336cffeO0ssvuxqZ05iZq.json +10 -0
- package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.json +10 -0
- package/LICENSE.txt +21 -0
- package/PLAN_REVIEW.md +134 -0
- package/README.md +2 -1
- package/REQUEST.md +5 -2
- package/ROUTE.md +1 -1
- package/informe-contrato-mithril-lynx.md +206 -0
- package/package.json +5 -1
- package/prompts-analisis-contrato-mithril-lynx.md +141 -0
- package/src/apply-patch.js +87 -27
- package/src/backends/virtual-backend.js +18 -4
- package/src/background.d.ts +14 -1
- package/src/channel.js +10 -2
- package/src/fake-dom.js +13 -10
- package/src/list-cell.d.ts +20 -0
- package/src/list-cell.js +73 -0
- package/src/list-support.d.ts +12 -9
- package/src/list-support.js +144 -114
- package/src/main-thread.js +15 -2
- package/src/mount-redraw.d.ts +10 -0
- package/src/mount-redraw.js +55 -2
- package/src/patch-protocol.js +61 -6
- package/src/request.d.ts +2 -0
- package/src/request.js +21 -3
- package/src/route.d.ts +22 -3
- package/src/route.js +25 -6
- package/src/testing.js +24 -11
- package/test/list.test.ts +86 -40
- package/test/patch-protocol.test.ts +41 -0
- package/test/remove-event.test.ts +26 -0
- package/test/route-hot-reload.test.ts +2 -1
- package/test/route.test.ts +5 -2
- package/test/setup.ts +10 -0
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
|
@@ -15,7 +15,7 @@ The old implementation's core bugs all traced back to the same root cause: wheth
|
|
|
15
15
|
- **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
16
|
- **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
17
|
|
|
18
|
-
**
|
|
18
|
+
**Carried over as code, not yet device-verified**: gestures and list virtualization (Tier 2) exist in this rewrite — `src/apply-patch.js`'s `Op.SetGestureDetector`/`Op.CreateList` cases, `src/list-cell.js`, `src/list-support.js` — and are exported as `mithril-lynx/list-support` and `mithril-lynx/list-cell`. The new arena-claim gesture path is explicitly unverified on a real device (see the note in `apply-patch.js`). **Deliberately not carried over at all (yet)**: the imperative ref helpers and the old stack-based `navigation` module. 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
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
@@ -65,6 +65,7 @@ Use a plain CSS `@font-face` rule — not `lynx.addFont()` (that JS API only fir
|
|
|
65
65
|
|
|
66
66
|
- **`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
67
|
- **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`.
|
|
68
|
+
- **The event object passed to handlers is a synthesized snapshot, not a live DOM event.** `preventDefault()` and `stopPropagation()` on it are no-ops, and 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 calls `e.preventDefault()` (e.g. form submit) will silently do nothing. `e.redraw = false` still works, and is how `route.Link` opts out of the post-tap redraw.
|
|
68
69
|
|
|
69
70
|
## Testing
|
|
70
71
|
|
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:
|
package/ROUTE.md
CHANGED
|
@@ -40,7 +40,7 @@ route.get(); // current resolved path, e.g. "/detail
|
|
|
40
40
|
route.param("id"); // "42" — or route.param() for the whole params object
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`route.back()` / `route.forward()` walk the same in-memory history stack `route.set` writes to. **These do not exist on real Mithril** — they're new here because Lynx has no hardware/gesture "back" button exposed to JS (only app-lifecycle events like `onAppEnterBackground`, not navigation), so an app's own back affordance has to call something explicit. Wire a screen's back button to `route.back()`.
|
|
43
|
+
`route.back()` / `route.forward()` walk the same in-memory history stack `route.set` writes to. **These do not exist on real Mithril** — they're new here because Lynx has no hardware/gesture "back" button exposed to JS (only app-lifecycle events like `onAppEnterBackground`, not navigation), so an app's own back affordance has to call something explicit. Wire a screen's back button to `route.back()`. Both return `true` when they navigated and `false` at the top/end of the stack (a silent no-op at the boundary), so a back/forward affordance can enable/disable itself from the return value.
|
|
44
44
|
|
|
45
45
|
`route.prefix` exists only so app code defensively ported from a real Mithril app (`m.route.prefix = ""`) doesn't throw on import — there's no URL bar for a prefix to apply to, so setting it does nothing.
|
|
46
46
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Informe de análisis del contrato de mithril-lynx v2.6.1
|
|
2
|
+
|
|
3
|
+
> Método: PROMPT 1 (Maestro) del archivo `prompts-analisis-contrato-mithril-lynx.md`.
|
|
4
|
+
> Toda afirmación está anclada a archivo:línea. Las inferencias se marcan como tales.
|
|
5
|
+
> Compatibilidad upstream verificada contra la fuente real de Mithril **2.3.8**
|
|
6
|
+
> (`request/request.js` y `render/render.js`), la misma base que `mithril-runtime` — no contra memoria.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Mapa del contrato
|
|
11
|
+
|
|
12
|
+
| Capa | Dónde vive | Qué define |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| API pública del paquete | `package.json` `exports` (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; el `.js` es la evidencia de cumplimiento |
|
|
15
|
+
| 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 |
|
|
16
|
+
| Compatibilidad 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 |
|
|
17
|
+
| 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` |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Fortalezas (diseño que conviene conservar)
|
|
22
|
+
|
|
23
|
+
1. **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, testeable, sin estado condicional global.
|
|
24
|
+
2. **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. Espejo 1:1 entre hilos por construcción.
|
|
25
|
+
3. **Codificación plana con tabla de aridad y `forEachOp` que lanza en opcode desconocido** (`patch-protocol.js` 94–101) — sin asignación de objetos por op en el hot path.
|
|
26
|
+
4. **Opciones no soportadas de `request` fallan dos veces**: lanzan en runtime con puntero a docs (`request.js` 27–45) **y** son errores de compilación vía `never` (`request.d.ts` 15–22).
|
|
27
|
+
5. **Workaround del timer de Lynx documentado con honestidad.** `REDRAW_DELAY_MS = 50` se reconoce como margen empírico, no garantía (`mount-redraw.js` 34–46, `REQUEST.md` §"A Lynx timer quirk", `FETCH_INVESTIGATION.md` §4.6).
|
|
28
|
+
6. **Un solo modelo de hilos impuesto por construcción.** `background.js` es el único que ejecuta `render.js` (3–5); `main-thread.js` solo replica patches (1–9). Sin modo-condicional.
|
|
29
|
+
7. **Divergencias de `route` declaradas explícitamente.** `back()`/`forward()` como adiciones no-upstream (`ROUTE.md` 43, 66); `prefix` como no-op asignable (148–151).
|
|
30
|
+
8. **`resolveRoute`/`Link`/`SKIP` espejan upstream** (`route.js` 50–98, 177–201), minimizando el costo de portar.
|
|
31
|
+
9. **Evidencia en device separada de tests unitarios** (`README.md` §Testing, `REQUEST.md` §Testing): no se confunde "pasa el test" con "funciona en hardware".
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. Mejoras propuestas, priorizadas
|
|
36
|
+
|
|
37
|
+
### 🔴 Alta — previenen fallos silenciosos o corrupción
|
|
38
|
+
|
|
39
|
+
#### R1. El protocolo wire no lleva versión ni handshake
|
|
40
|
+
|
|
41
|
+
- **Problema** (evidencia): `Op` es un enum numérico congelado sin campo de versión (`patch-protocol.js` 16–53); `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, un tag leído como id).
|
|
42
|
+
- **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 que nombre las versiones.
|
|
43
|
+
- **Por qué**: protege el invariante "id space 1:1 y opcode↔PAPI" contra desincronización silenciosa. Coherente con fail-fast. (Mitigación parcial existente: ambos bundles se compilan del mismo `patch-protocol.js`; un cache parcial sigue rompiendo la garantía.)
|
|
44
|
+
|
|
45
|
+
#### R2. Un segundo `renderApp()` corrompe el id space y pisa el slot de redraw sin protestar
|
|
46
|
+
|
|
47
|
+
- **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.
|
|
48
|
+
- **Propuesta**: `register()` lanza si `currentRedraw != null` (fail-fast), y/o `renderApp()` rehúsa una segunda llamada en el mismo contexto. *(Inferencia: el segundo `renderApp` es un uso indebido de app, no del HMR — `dev-reload-client.js` 12–19 documenta que el HMR re-renderiza desde el closure del propio app, no con un nuevo `renderApp`.)*
|
|
49
|
+
- **Por qué**: convierte un uso indebido en error inmediato en vez de corrupción invisible del estado de render.
|
|
50
|
+
|
|
51
|
+
#### R3. `Op.RemoveEvent` es un no-op explícito → handler disparado N veces al re-adjuntar condicionalmente
|
|
52
|
+
|
|
53
|
+
- **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`; el `render.js` upstream (`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.)
|
|
54
|
+
- **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.
|
|
55
|
+
- **Por qué**: elimina un fallo de comportamiento silencioso (doble disparo) de la clase que el proyecto declara 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.)*
|
|
56
|
+
|
|
57
|
+
### 🟡 Media — divergencias de comportamiento o de documentación
|
|
58
|
+
|
|
59
|
+
#### M1. `route.set()` antes de `route()` pierde la navegación silenciosamente
|
|
60
|
+
|
|
61
|
+
- **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.
|
|
62
|
+
- **Propuesta**: lanzar (o encolar) cuando `route.set` se llama con `ready === false`, en vez de descartar.
|
|
63
|
+
- **Por qué**: corrige una pérdida silenciosa de estado que contradice fail-fast.
|
|
64
|
+
|
|
65
|
+
#### M2. Una segunda llamada a `route()` resetea el historial Y salta a `defaultRoute`
|
|
66
|
+
|
|
67
|
+
- **Problema**: `history = [defaultRoute]; historyIndex = 0` se ejecuta incondicionalmente en cada `route()` (`route.js` 119–120), **y** `resolveRoute(defaultRoute, null)` corre incondicionalmente (`route.js` 122). Preservar solo la pila no basta: la pantalla salta igual a `defaultRoute` al re-registrar, descartando la ruta actual. `ROUTE.md` 71 documenta el patrón HMR con re-resolución manual (`route.set(route.get(), null, {replace: true})`), que es exactamente el trabajo que `route()` debería hacer solo en el re-registro.
|
|
68
|
+
- **Propuesta** (boceto completo):
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
function route(defaultRoute, routes) {
|
|
72
|
+
compiled = Object.keys(routes).map(/* ... */);
|
|
73
|
+
fallbackRoute = defaultRoute;
|
|
74
|
+
var defaultData = parsePathname(defaultRoute);
|
|
75
|
+
if (!compiled.some((entry) => entry.check(defaultData))) {
|
|
76
|
+
throw new ReferenceError("Default route doesn't match any known routes.");
|
|
77
|
+
}
|
|
78
|
+
if (!ready) {
|
|
79
|
+
history = [defaultRoute];
|
|
80
|
+
historyIndex = 0;
|
|
81
|
+
ready = true;
|
|
82
|
+
resolveRoute(defaultRoute, null);
|
|
83
|
+
} else {
|
|
84
|
+
// Re-registro (HMR del módulo de rutas): conservar la pila y
|
|
85
|
+
// re-resolver la ruta ACTUAL contra la tabla nueva — lo mismo que
|
|
86
|
+
// el patrón documentado route.set(route.get(), null, {replace:true}).
|
|
87
|
+
resolveRoute(currentPath != null ? currentPath : defaultRoute, null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
(`currentPath` se declara en `route.js` 29 y se fija en 71; re-resolverla re-extrae params desde el path ya resuelto. Si la ruta actual desapareció de la tabla nueva, `resolveRoute` cae a `route.set(fallbackRoute, …)` en `route.js` 96, que es el comportamiento correcto.)
|
|
93
|
+
- **Por qué**: protege el invariante "historial en memoria = sesión" y el estado de pantalla durante HMR, haciendo que el re-registro sea idempotente en vez de destructivo.
|
|
94
|
+
|
|
95
|
+
#### M3. README dice que gestures + listas virtualizadas "no se portaron", pero están implementados y exportados
|
|
96
|
+
|
|
97
|
+
- **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.
|
|
98
|
+
- **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).
|
|
99
|
+
- **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).
|
|
100
|
+
|
|
101
|
+
#### M4. `err.message` en no-2xx diverge de upstream (statusText vs responseText)
|
|
102
|
+
|
|
103
|
+
- **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.
|
|
104
|
+
- **Propuesta**: usar el texto crudo del body como message (o corregir el claim de `REQUEST.md`).
|
|
105
|
+
- **Por qué**: la forma del error es parte del contrato documentado como "igual a upstream".
|
|
106
|
+
|
|
107
|
+
#### M5. `preventDefault`/`stopPropagation` del evento entregado a los handlers son no-ops sin documentar
|
|
108
|
+
|
|
109
|
+
- **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).
|
|
110
|
+
- **Propuesta**: documentarlo en README/ROUTE.md (o lanzar donde importe).
|
|
111
|
+
- **Por qué**: cierra una brecha de contrato visible para el usuario.
|
|
112
|
+
|
|
113
|
+
#### M6. `REDRAW_DELAY_MS = 50` fijo y dependiente de dispositivo, no configurable
|
|
114
|
+
|
|
115
|
+
- **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.
|
|
116
|
+
- **Propuesta**: exponer un setter de configuración (p. ej. `configure({redrawDelayMs})`).
|
|
117
|
+
- **Por qué**: convierte una constante mágica observable en configurable, sin tocar el diseño.
|
|
118
|
+
|
|
119
|
+
#### M7. `Op.SetListItems` embebe `JSON.stringify(cells)` en un array que ya viaja como objeto JS nativo
|
|
120
|
+
|
|
121
|
+
- **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).
|
|
122
|
+
- **Propuesta**: pasar `cells` directo (eliminar stringify/parse).
|
|
123
|
+
- **Por qué**: coherencia con el resto del protocolo (valores nativos), menos CPU y sin restricción innecesaria.
|
|
124
|
+
|
|
125
|
+
#### M8. `OP_ARITY` y el switch inline de `apply-patch` pueden divergir; no hay test de round-trip
|
|
126
|
+
|
|
127
|
+
- **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`.
|
|
128
|
+
- **Propuesta**: test que recorra cada opcode por `OP_ARITY` y por `applyPatch` (round-trip), o derivar una tabla de la otra.
|
|
129
|
+
- **Por qué**: protege el invariante "dos lectores del mismo encoding" contra drift silencioso.
|
|
130
|
+
|
|
131
|
+
### 🟢 Baja — ergonomía, tipos, docs
|
|
132
|
+
|
|
133
|
+
#### L1. `route.d.ts` importa `Component` de `"mithril"`, que no es dependencia
|
|
134
|
+
|
|
135
|
+
- **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.
|
|
136
|
+
- **Propuesta**: importar de `"mithril-runtime"`.
|
|
137
|
+
|
|
138
|
+
#### L2. `type` en `request.d.ts` tipado como `new (data: any) => T`, pero se aplica por elemento en arrays
|
|
139
|
+
|
|
140
|
+
- **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.
|
|
141
|
+
- **Propuesta**: documentar/tipar el comportamiento por-elemento.
|
|
142
|
+
|
|
143
|
+
#### L3. `extract` + `type` juntos divergen de upstream
|
|
144
|
+
|
|
145
|
+
- **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).
|
|
146
|
+
- **Propuesta**: aplicar `applyType` al resultado de `extract`, o documentar la diferencia.
|
|
147
|
+
|
|
148
|
+
#### L4. El listener sobre el `signal` del caller nunca se remueve
|
|
149
|
+
|
|
150
|
+
- **Problema**: `request.js` 115–118 añade `options.signal.addEventListener("abort", ...)` sin `removeEventListener`; un signal compartido longevo acumula un closure por request.
|
|
151
|
+
- **Propuesta**: remover el listener al settle.
|
|
152
|
+
|
|
153
|
+
#### L5. `background.d.ts` tipa `document` como `unknown`
|
|
154
|
+
|
|
155
|
+
- **Problema**: `background.d.ts` 8 — expuesto para tests/HMR, pero obliga a `as`/cast.
|
|
156
|
+
- **Propuesta**: exportar un tipo mínimo de `LynxDocument`.
|
|
157
|
+
|
|
158
|
+
#### L6. `options.signal` está tipado y soportado, pero ausente de la tabla de `REQUEST.md`
|
|
159
|
+
|
|
160
|
+
- **Problema**: `request.d.ts` 8 declara `signal?: AbortSignal` y `request.js` 115–118 lo implementa (vincula el `signal` del caller al `AbortController` interno), pero la tabla "Supported options" de `REQUEST.md` (20–32) no lo lista. `REQUEST.md` se presenta como el "practical usage doc" (línea 3) y este hueco deja un comportamiento soportado sin prometer.
|
|
161
|
+
- **Propuesta**: añadir una fila `signal` a la tabla de `REQUEST.md` (comportamiento: aborta la petición, igual que `.abort()`/`timeout`, vinculado al mismo `AbortController`).
|
|
162
|
+
- **Por qué**: completa la promesa documentada. Se relaciona con L4 (el listener sobre ese `signal` nunca se remueve), pero son huecos distintos: L4 es un leak, este es un hueco de documentación.
|
|
163
|
+
|
|
164
|
+
#### L7. `route.param()` tipado `unknown`, poco ergonómico para params de path
|
|
165
|
+
|
|
166
|
+
- **Problema**: `route.d.ts` 22 tipa `param(key?: string): unknown`. Es *técnicamente* correcto — los params de query pueden ser boolean (`"true"`/`"false"` se coaccionan en `parseQueryString` upstream) y `route.set(path, data)` fusiona `data` arbitrario vía `Object.assign(parsed.params, data)` (`route.js` 52) —, pero el caso dominante, un param de path `:id` (que `compileTemplate` produce vía `decodeURIComponent`, siempre string), obliga a un cast en el consumidor.
|
|
167
|
+
- **Propuesta**: documentar la distinción (params de path = string; de query = string|boolean; `data` de `route.set` = cualquier cosa) y, si se quiere, añadir un helper/overload tipado para el caso path.
|
|
168
|
+
- **Por qué**: mejora la ergonomía de tipos sin mentir sobre el caso de `data` arbitrario.
|
|
169
|
+
|
|
170
|
+
#### L8. `route.back()`/`forward()` son no-op silenciosos en el tope del historial
|
|
171
|
+
|
|
172
|
+
- **Problema**: `route.js` 163–172: `back()` hace `if (historyIndex <= 0) return;` y `forward()` hace `if (historyIndex >= history.length - 1) return;` — retorno silencioso en el borde, devolviendo siempre `undefined`. `ROUTE.md` 43 documenta la existencia de ambos pero no el no-op en el borde ni el valor de retorno. Un botón de "atrás" no puede saber si hay historial sin replicar el estado del índice (que no está expuesto).
|
|
173
|
+
- **Propuesta**: devolver `boolean` (`true` si navegó, `false` si estaba en el borde) y actualizar `route.d.ts` 23–24 (`back(): boolean` / `forward(): boolean`); documentar en `ROUTE.md`.
|
|
174
|
+
- **Por qué**: habilita habilitar/deshabilitar la UI (back/forward) sin romper el contrato actual (los callers que ignoran el retorno siguen funcionando).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. Orden de implementación sugerido
|
|
179
|
+
|
|
180
|
+
1. **R2** (guard en `renderApp`/`register`) — más barato, mayor riesgo de corrupción.
|
|
181
|
+
2. **R1** (versión de protocolo) — barato, previene desync silencioso.
|
|
182
|
+
3. **R3** (RemoveEvent) — requiere test de confirmación + fix real.
|
|
183
|
+
4. **M1 + M2** (historial de `route` + re-resolve en re-registro) — correctitud del ciclo de vida.
|
|
184
|
+
5. **M4 + M5** (mensaje de error + no-ops de evento documentados).
|
|
185
|
+
6. **M3** (reconciliación README/testing vs código).
|
|
186
|
+
7. **M6 + M7 + M8** (redraw configurable, serialización de listas, test OP_ARITY).
|
|
187
|
+
8. **L1–L8** (tipos/ergonomía).
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Anexo — Verificación de hipótesis (PROMPT 3 del archivo)
|
|
192
|
+
|
|
193
|
+
Las hipótesis no se creen: se confirman o refutan contra el código.
|
|
194
|
+
|
|
195
|
+
| # | Hipótesis | Veredicto | Evidencia |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| H1 | Sin versión/handshake en el wire | ✅ Confirmada | `patch-protocol.js` 16–53; `channel.js` 24–26, 39–41; `main-thread.js` 35–68 |
|
|
198
|
+
| H2 | Segundo `renderApp()` corrompe el id space | ✅ Confirmada | `mount-redraw.js` 57–59; `virtual-backend.js` 12; `main-thread.js` 50–56 |
|
|
199
|
+
| H3 | `route.set()` pre-`route()` pierde la navegación | ✅ Confirmada | `route.js` 135–141 vs 119 |
|
|
200
|
+
| H4 | Segundo `route()` reinicia historial | ✅ Confirmada (ampliada en M2) | `route.js` 119–122: resetea pila **y** resuelve `defaultRoute` |
|
|
201
|
+
| H5 | `deserialize` recibe body ya parseado ≠ upstream | ❌ **Refutada** | Upstream también pasa el objeto parseado con `responseType:"json"` (default) y texto crudo con `"text"`; `mithril-lynx` (`request.js` 146–148) coincide. `REQUEST.md` es correcto |
|
|
202
|
+
| H6 | `type` sobre arrays diverge de upstream | ❌ **Refutada** | Upstream hace `new type(item)` por elemento; `applyType` (`request.js` 54–58) coincide |
|
|
203
|
+
| H7 | `route.d.ts` importa de `"mithril"`, no `mithril-runtime` | ✅ Confirmada | `route.d.ts` 1 vs `package.json` 51/61 |
|
|
204
|
+
| H8 | `REDRAW_DELAY_MS = 50` fijo | ✅ Confirmada | `mount-redraw.js` 47 |
|
|
205
|
+
| H9 | `preventDefault`/`stopPropagation` no-ops sin documentar | ✅ Confirmada | `background.js` 69 |
|
|
206
|
+
| H10 | Doble serialización en `SetListItems` | ✅ Confirmada | `virtual-backend.js` 84; `apply-patch.js` 405 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Mithril.js on Lynx: real mithril/render/render.js driven through a Lynx-backed fake DOM, with an explicit single commit hook (no conditional global flush) and three reload modes (data-light, structural-light, full). A complete rewrite of the previous mithril-lynx (0.0.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"./list-support": {
|
|
37
37
|
"types": "./src/list-support.d.ts",
|
|
38
38
|
"default": "./src/list-support.js"
|
|
39
|
+
},
|
|
40
|
+
"./list-cell": {
|
|
41
|
+
"types": "./src/list-cell.d.ts",
|
|
42
|
+
"default": "./src/list-cell.js"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"scripts": {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Prompts para DeepSeek V4 Pro — Análisis de contrato de mithril-lynx
|
|
2
|
+
|
|
3
|
+
Repositorio objetivo: https://github.com/carlos-sweb/mithril-lynx/
|
|
4
|
+
Tarea: analizar el contrato del proyecto y proponer mejoras priorizadas.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## PROMPT 1 — Maestro (modelo con acceso al repo / herramientas de lectura)
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
# ROL
|
|
12
|
+
Actúa como arquitecto de software senior especializado en frameworks de UI, contratos de API y sistemas de renderizado multi-hilo. Trabajas en español.
|
|
13
|
+
|
|
14
|
+
# CONTEXTO
|
|
15
|
+
El proyecto es mithril-lynx v2.6.1 (https://github.com/carlos-sweb/mithril-lynx): una reimplementación desde cero de Mithril.js sobre el framework nativo Lynx (Element PAPI). El hilo background ejecuta el diff real de Mithril contra un árbol virtual; el hilo main thread solo replica "patches" sobre nodos nativos y reenvía eventos. Incluye reimplementaciones de m.route (historial en memoria) y m.request (wrapper sobre lynx.fetch). La filosofía declarada del proyecto es fail-fast: las operaciones sin equivalente lanzan errores inmediatos con mensajes que nombran el problema, nunca fallan silenciosamente.
|
|
16
|
+
|
|
17
|
+
# TAREA
|
|
18
|
+
Analiza el CONTRATO del proyecto y propone mejoras priorizadas. "Contrato" = las garantías que el proyecto le da a quien lo consume. Analiza estas 4 capas, en este orden:
|
|
19
|
+
1. API pública del paquete — los entry points de "exports" en package.json y sus .d.ts (background, main-thread, plugin, route, request, mount-redraw, testing, list-support, list-cell).
|
|
20
|
+
2. Protocolo wire background↔main-thread — src/patch-protocol.js (enum Op, tabla OP_ARITY, codificación plana) y src/channel.js (nombres de eventos, payloads).
|
|
21
|
+
3. Compatibilidad con Mithril upstream — qué tan fielmente route.js y request.js reproducen m.route/m.request; divergencias documentadas vs. divergencias reales no documentadas (compara contra tu conocimiento del código fuente de Mithril 1.x).
|
|
22
|
+
4. Invariantes internos — commit.js (hook único de commit), mount-redraw.js (registro y debounce del redraw), ciclo de vida de renderApp.
|
|
23
|
+
|
|
24
|
+
# MÉTODO
|
|
25
|
+
1. Lee primero package.json y el árbol de archivos para mapear la superficie.
|
|
26
|
+
2. Lee los .d.ts de cada entry point ANTES que su .js: el tipo ES el contrato; el .js es la evidencia de cumplimiento.
|
|
27
|
+
3. Lee ROUTE.md y REQUEST.md: son las promesas documentadas. Verifica una por una contra el código.
|
|
28
|
+
4. Lee las implementaciones cruzando cada función exportada con su comportamiento real.
|
|
29
|
+
5. Ancla cada afirmación a archivo y, si puedes, a línea.
|
|
30
|
+
6. No propongas reescrituras ni cambios de arquitectura: el proyecto acaba de hacer una reescritura deliberada (un solo modelo de hilos, commit hook explícito, fail-fast). Las mejoras deben respetar esas decisiones.
|
|
31
|
+
|
|
32
|
+
# FORMATO DE SALIDA
|
|
33
|
+
Informe en español:
|
|
34
|
+
## 1. Mapa del contrato (tabla: capa / dónde vive / qué define)
|
|
35
|
+
## 2. Fortalezas (qué diseño conviene conservar, con evidencia)
|
|
36
|
+
## 3. Mejoras propuestas, priorizadas
|
|
37
|
+
### 🔴 Alta — previenen fallos silenciosos o corrupción
|
|
38
|
+
### 🟡 Media — divergencias de comportamiento o de documentación
|
|
39
|
+
### 🟢 Baja — ergonomía, tipos, docs
|
|
40
|
+
Cada mejora: **Problema** (evidencia archivo:línea) → **Propuesta** concreta (con boceto de patch si aplica) → **Por qué** (qué invariante protege o qué promesa corrige).
|
|
41
|
+
## 4. Orden de implementación sugerido
|
|
42
|
+
|
|
43
|
+
# REGLAS DURAS
|
|
44
|
+
- Nada de afirmaciones sin evidencia en el código leído; lo inferido se marca como inferencia.
|
|
45
|
+
- Distingue siempre "contrato" (visible para el usuario) de "detalle de implementación".
|
|
46
|
+
- No inventes archivos ni exports; si no pudiste leer algo, dilo.
|
|
47
|
+
- Las contradicciones entre documentación (README/ROUTE.md/REQUEST.md) y código real son hallazgos de primer orden: búscalas activamente.
|
|
48
|
+
- Máximo rigor, cero relleno.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## PROMPT 2 — Secuencia por fases (modelo SIN acceso al repo; tú pegas los archivos)
|
|
54
|
+
|
|
55
|
+
### Fase 0 — Contexto y plan
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
# ROL
|
|
59
|
+
Arquitecto de software senior especializado en frameworks de UI y contratos de API. Español.
|
|
60
|
+
|
|
61
|
+
# CONTEXTO
|
|
62
|
+
Analizaré contigo el contrato de mithril-lynx v2.6.1 (Mithril.js renderizado sobre Lynx Element PAPI: diff real en hilo background, main thread solo aplica patches). Lo haremos en 3 fases: (1) inventario de la superficie de contrato desde sus .d.ts y docs, (2) verificación contra las implementaciones, (3) mejoras priorizadas.
|
|
63
|
+
|
|
64
|
+
A continuación te paso package.json y el árbol de archivos. Responde SOLO con:
|
|
65
|
+
a) tabla de entry points públicos y qué exporta cada uno según los datos disponibles;
|
|
66
|
+
b) plan de análisis para las fases 1 y 2 (qué archivo leer antes que cuál, y por qué).
|
|
67
|
+
No propongas mejoras todavía.
|
|
68
|
+
|
|
69
|
+
<PEGA AQUÍ: package.json + árbol de archivos>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Fase 1 — Inventario de la superficie de contrato
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
# TAREA FASE 1 — Inventario de la superficie de contrato
|
|
76
|
+
Te paso los .d.ts de cada entry point y los documentos ROUTE.md y REQUEST.md (las promesas documentadas del proyecto). Para cada entry point reporta:
|
|
77
|
+
- Qué promete exactamente (firma, tipos, invariantes declaradas en comentarios).
|
|
78
|
+
- Qué OMITE declarar (errores lanzados, estados inválidos, casos límite).
|
|
79
|
+
- Contradicciones internas entre el .d.ts, ROUTE.md/REQUEST.md y los comentarios.
|
|
80
|
+
NO propongas mejoras todavía. Salida: tabla por entry point + lista de anomalías detectadas.
|
|
81
|
+
|
|
82
|
+
<PEGA AQUÍ: src/background.d.ts, src/main-thread.d.ts, src/route.d.ts, src/request.d.ts, src/mount-redraw.d.ts, src/testing.d.ts, src/list-cell.d.ts, src/list-support.d.ts, plugin.d.ts, ROUTE.md, REQUEST.md>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Fase 2 — Verificación contra implementación
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
# TAREA FASE 2 — Verificación contra implementación
|
|
89
|
+
Te paso las implementaciones. Verifica cada promesa del inventario (Fase 1) contra el código real. Además, cruza route.js y request.js contra tu conocimiento del código fuente de Mithril 1.x upstream: cualquier diferencia de SEMÁNTICA (no solo de firma) es un hallazgo, y debe clasificarse como [confirmada | divergencia-documentada | divergencia-oculta].
|
|
90
|
+
Presta atención explícita a: ciclo de vida (qué pasa con dobles llamadas, llamadas fuera de orden), errores silenciosos, leaks (listeners/timers), constantes mágicas que forman parte del comportamiento observable, y el id space compartido entre los dos hilos.
|
|
91
|
+
Salida: lista de hallazgos con evidencia archivo:línea y clasificación.
|
|
92
|
+
|
|
93
|
+
<PEGA AQUÍ: src/route.js, src/request.js, src/background.js, src/main-thread.js, src/channel.js, src/patch-protocol.js, src/commit.js, src/mount-redraw.js>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Fase 3 — Síntesis y mejoras priorizadas
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
# TAREA FASE 3 — Informe final de mejoras
|
|
100
|
+
Con el inventario (Fase 1) y la verificación (Fase 2), produce el informe final en español:
|
|
101
|
+
## 1. Mapa del contrato (tabla: capa / dónde vive / qué define)
|
|
102
|
+
## 2. Fortalezas a conservar (con evidencia)
|
|
103
|
+
## 3. Mejoras priorizadas
|
|
104
|
+
### 🔴 Alta — previenen fallos silenciosos o corrupción
|
|
105
|
+
### 🟡 Media — divergencias de comportamiento/documentación
|
|
106
|
+
### 🟢 Baja — ergonomía, tipos, docs
|
|
107
|
+
Cada mejora: **Problema** (evidencia archivo:línea) → **Propuesta** concreta (con boceto de patch si aplica) → **Por qué**.
|
|
108
|
+
## 4. Orden de implementación sugerido
|
|
109
|
+
Reglas: nada de reescrituras arquitectónicas (el proyecto ya reescribió deliberadamente: un solo modelo de hilos, commit hook explícito, fail-fast); cada afirmación anclada al código leído.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## PROMPT 3 — Verificación de hipótesis (opcional, para profundizar)
|
|
115
|
+
|
|
116
|
+
Úsalo después del Prompt 1 o de la Fase 3. Las hipótesis pueden estar parcialmente mal: el modelo debe confirmarlas o refutarlas con evidencia, no creerlas.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
# TAREA — Verificación de hipótesis concretas
|
|
120
|
+
Confirma o refuta cada hipótesis contra el código del repo, con evidencia archivo:línea. Si una hipótesis es cierta pero con matices, dilos. Si es falsa, explica qué hace el código en realidad:
|
|
121
|
+
1. El protocolo wire (patch-protocol.js) no lleva versión ni handshake entre los dos bundles, así que un HMR parcial o un bundle cacheado puede desincronizar opcodes silenciosamente.
|
|
122
|
+
2. Un segundo renderApp() en el mismo contexto corrompe el id space: mount-redraw.register pisa el slot sin protestar y habría dos documentos con contadores de ids que colisionan en el main thread.
|
|
123
|
+
3. route.set() llamado antes de route() muta el historial que route() luego descarta: la navegación se pierde silenciosamente por el gate `if (ready)`.
|
|
124
|
+
4. Una segunda llamada a route() recompila la tabla pero reinicia el historial a [defaultRoute]: en HMR del módulo de rutas el usuario vuelve a la pantalla inicial.
|
|
125
|
+
5. request: `deserialize` recibe el body ya parseado (objeto), no el texto crudo como en m.request upstream — la tabla de REQUEST.md lo documenta incorrectamente como "same as upstream".
|
|
126
|
+
6. request: `type:` sobre respuestas array aplica el constructor por elemento (map), mientras upstream hace `new Type(data)` sobre el resultado completo.
|
|
127
|
+
7. src/route.d.ts importa `Component` de "mithril", pero package.json declara como peerDependency "mithril-runtime" (el fork), no "mithril": los tipos fallan para usuarios sin mithril instalado.
|
|
128
|
+
8. mount-redraw usa un REDRAW_DELAY_MS = 50 fijo y empírico: es parte del comportamiento observable y depende del dispositivo, sin forma de configurarlo.
|
|
129
|
+
9. El objeto evento que reciben los handlers tiene preventDefault/stopPropagation como no-ops sin documentar en ningún doc user-facing.
|
|
130
|
+
10. Op.SetListItems incrusta JSON.stringify(cells) dentro de un array que ya viaja como objeto JS nativo por el canal: serialización redundante en ambos extremos.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Consejos de uso
|
|
136
|
+
|
|
137
|
+
- Temperatura: 0.2–0.4 (tarea de análisis, no creativa).
|
|
138
|
+
- Si el modelo tiene herramientas de lectura de repo → Prompt 1 solo.
|
|
139
|
+
- Si no → Prompts 2 (Fases 0→1→2→3), un mensaje por fase, pegando los archivos desde raw.githubusercontent.com.
|
|
140
|
+
- Si el resultado sale superficial → añade: "Expande cada hallazgo 🔴 hasta que tenga evidencia archivo:línea y boceto de patch".
|
|
141
|
+
- Prompt 3 es ideal como segunda pasada para auditar exhaustividad.
|