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
|
@@ -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": "
|
|
3
|
+
"version": "3.0.0",
|
|
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",
|
|
@@ -32,14 +32,6 @@
|
|
|
32
32
|
"./testing": {
|
|
33
33
|
"types": "./src/testing.d.ts",
|
|
34
34
|
"default": "./src/testing.js"
|
|
35
|
-
},
|
|
36
|
-
"./list-support": {
|
|
37
|
-
"types": "./src/list-support.d.ts",
|
|
38
|
-
"default": "./src/list-support.js"
|
|
39
|
-
},
|
|
40
|
-
"./list-cell": {
|
|
41
|
-
"types": "./src/list-cell.d.ts",
|
|
42
|
-
"default": "./src/list-cell.js"
|
|
43
35
|
}
|
|
44
36
|
},
|
|
45
37
|
"scripts": {
|
package/plugin.js
CHANGED
|
@@ -52,6 +52,10 @@ const DEV_TRANSPORT_NOOP_PATH = path.join(
|
|
|
52
52
|
* Builds the query string the in-bundle dev-reload client reads its config
|
|
53
53
|
* from — unchanged from v1's version (build-tooling glue, not part of the
|
|
54
54
|
* bug this rewrite is about).
|
|
55
|
+
* @param {Object} api - The Rsbuild plugin API.
|
|
56
|
+
* @param {Object} environment - The current build environment.
|
|
57
|
+
* @param {string} entryName - The entry (page) name.
|
|
58
|
+
* @returns {string} The query string, without the leading `?`.
|
|
55
59
|
*/
|
|
56
60
|
function createDevReloadClientQuery(api, environment, entryName) {
|
|
57
61
|
const config = environment.config ?? {};
|
|
@@ -83,6 +87,12 @@ function createDevReloadClientQuery(api, environment, entryName) {
|
|
|
83
87
|
return params.toString();
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Finds the first existing file among `candidates` in `dir`.
|
|
92
|
+
* @param {string} dir - The directory to search.
|
|
93
|
+
* @param {string[]} candidates - File names in priority order.
|
|
94
|
+
* @returns {string|null} The full path of the first match, or `null`.
|
|
95
|
+
*/
|
|
86
96
|
function findSibling(dir, candidates) {
|
|
87
97
|
for (const name of candidates) {
|
|
88
98
|
const candidate = path.join(dir, name);
|
|
@@ -91,6 +101,12 @@ function findSibling(dir, candidates) {
|
|
|
91
101
|
return null;
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Walks up from a resolved file to the root of the named package.
|
|
106
|
+
* @param {string} resolvedFile - A file inside the package.
|
|
107
|
+
* @param {string} expectedName - The package `name` to look for.
|
|
108
|
+
* @returns {string|null} The package root directory, or `null` when not found within 10 levels.
|
|
109
|
+
*/
|
|
94
110
|
function packageRootOf(resolvedFile, expectedName) {
|
|
95
111
|
let dir = path.dirname(resolvedFile);
|
|
96
112
|
for (let i = 0; i < 10; i++) {
|
|
@@ -107,12 +123,24 @@ function packageRootOf(resolvedFile, expectedName) {
|
|
|
107
123
|
return null;
|
|
108
124
|
}
|
|
109
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Creates the Rsbuild plugin that builds a mithril-lynx app's background and main-thread bundles.
|
|
128
|
+
* @param {Object} [options] - Plugin options.
|
|
129
|
+
* @param {string} [options.targetSdkVersion="3.5"] - Lynx target SDK version.
|
|
130
|
+
* @param {boolean} [options.liveReload=true] - Whether to inject the dev live-reload client in dev builds.
|
|
131
|
+
* @returns {{name: string, setup: (api: Object) => void}} The Rsbuild plugin.
|
|
132
|
+
*/
|
|
110
133
|
export function pluginMithrilLynx(options = {}) {
|
|
111
134
|
const targetSdkVersion = options.targetSdkVersion ?? "3.5";
|
|
112
135
|
const liveReload = options.liveReload ?? true;
|
|
113
136
|
|
|
114
137
|
return {
|
|
115
138
|
name: PLUGIN_NAME,
|
|
139
|
+
/**
|
|
140
|
+
* @param {Object} api - The Rsbuild plugin API.
|
|
141
|
+
* @returns {void}
|
|
142
|
+
* @throws {Error} If an entry has no sibling `background.ts`/`background.js`.
|
|
143
|
+
*/
|
|
116
144
|
setup(api) {
|
|
117
145
|
api.expose(Symbol.for("LynxTemplatePlugin"), { LynxTemplatePlugin });
|
|
118
146
|
|
|
@@ -240,6 +268,11 @@ export function pluginMithrilLynx(options = {}) {
|
|
|
240
268
|
chain.plugin("encode").use(LynxEncodePlugin, []);
|
|
241
269
|
|
|
242
270
|
chain.plugin("before-encode").use({
|
|
271
|
+
/**
|
|
272
|
+
* Taps the template plugin's `beforeEncode` hook to attach the background and main-thread assets.
|
|
273
|
+
* @param {Object} compiler - The bundler compiler.
|
|
274
|
+
* @returns {void}
|
|
275
|
+
*/
|
|
243
276
|
apply(compiler) {
|
|
244
277
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
245
278
|
const hooks = LynxTemplatePlugin.getLynxTemplatePluginHooks(compilation);
|
|
@@ -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.
|