mithril-lynx 0.0.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.omo/plans/m-request-fetch-lynx.md +306 -0
  2. package/.omo/plans/m-route-en-memoria.md +397 -0
  3. package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
  4. package/FETCH_INVESTIGATION.md +307 -0
  5. package/README.md +32 -302
  6. package/REQUEST.md +71 -0
  7. package/ROUTE.md +71 -0
  8. package/package.json +24 -80
  9. package/plugin.d.ts +2 -31
  10. package/plugin.js +107 -436
  11. package/rstest.config.ts +27 -0
  12. package/src/apply-patch.js +179 -0
  13. package/src/backends/virtual-backend.js +80 -0
  14. package/src/background.d.ts +11 -0
  15. package/src/background.js +79 -0
  16. package/src/channel.js +41 -0
  17. package/src/commit.js +67 -0
  18. package/src/dev-reload-client.js +168 -184
  19. package/src/dev-transport-noop.js +10 -0
  20. package/src/fake-dom.js +374 -0
  21. package/src/main-thread.d.ts +1 -0
  22. package/src/main-thread.js +68 -0
  23. package/src/mount-redraw.js +67 -0
  24. package/src/patch-protocol.js +40 -0
  25. package/src/reload/version.js +28 -0
  26. package/src/request.d.ts +37 -0
  27. package/src/request.js +181 -0
  28. package/src/route.d.ts +33 -0
  29. package/src/route.js +207 -0
  30. package/test/end-to-end.test.ts +86 -0
  31. package/test/reload-version.test.ts +17 -0
  32. package/test/request.test.ts +182 -0
  33. package/test/route-hot-reload.test.ts +40 -0
  34. package/test/route.test.ts +152 -0
  35. package/test/setup.ts +25 -0
  36. package/test/structural-reload.test.ts +95 -0
  37. package/CONTRACT.md +0 -151
  38. package/LICENSE +0 -21
  39. package/background.d.ts +0 -54
  40. package/background.js +0 -169
  41. package/element.d.ts +0 -34
  42. package/element.js +0 -83
  43. package/gesture.d.ts +0 -40
  44. package/gesture.js +0 -117
  45. package/internal/constants.js +0 -26
  46. package/internal/virtual-node.js +0 -388
  47. package/list.d.ts +0 -31
  48. package/list.js +0 -185
  49. package/main-thread.d.ts +0 -43
  50. package/main-thread.js +0 -165
  51. package/navigation.d.ts +0 -35
  52. package/navigation.js +0 -76
  53. package/renderer/background.d.ts +0 -21
  54. package/renderer/background.js +0 -84
  55. package/renderer/main-thread.d.ts +0 -12
  56. package/renderer/main-thread.js +0 -175
  57. package/src/lynx-mithril-shim.d.ts +0 -16
  58. package/src/lynx-mithril-shim.js +0 -1505
  59. package/src/worklet-runtime.js +0 -82
  60. package/testing.d.ts +0 -10
  61. package/testing.js +0 -91
@@ -0,0 +1,306 @@
1
+ # Plan — `m.request` para mithril-lynx-v2: ¿alcanza el `fetch` de Lynx?
2
+
3
+ > Estado (2026-09-18): **Investigación + spike en device COMPLETOS,
4
+ > implementación COMPLETA y verificada en device real.** `src/request.js`
5
+ > + `src/mount-redraw.js` implementados, 24 tests unitarios (fake fetch)
6
+ > pasando, y verificación end-to-end contra `lynx.fetch` real en
7
+ > `mithril-lynx-v2-app` (`src/screens/fetch-demo.ts`) confirmando GET real
8
+ > a `httpbin.org/json` con redraw automático. Investigación completa
9
+ > consolidada en [`FETCH_INVESTIGATION.md`](../../FETCH_INVESTIGATION.md)
10
+ > (incluye un bug de plataforma nuevo, no anticipado en el spike inicial:
11
+ > `lynx.setTimeout`/`requestAnimationFrame` no esperan a que la cola de
12
+ > microtasks drene — §4.6 de ese documento).
13
+ > Idioma: español. Insumo de las 2 tareas solicitadas: (1) cómo se hace
14
+ > fetch en Lynx, (2) si el `fetch` de Lynx cumple lo que `m.request`
15
+ > (spec: <https://mithril.js.org/request.html>) espera. Fuentes: doc
16
+ > oficial de Mithril, doc oficial de Lynx
17
+ > (`lynxjs.org/api/lynx-api/global/fetch.html`), los `.d.ts` instalados de
18
+ > `@lynx-js/types`, el código fuente real de `m.request`
19
+ > (`node_modules/mithril/request/request.js`, 199 líneas), **y las 4
20
+ > preguntas del spike respondidas en un device real conectado** (ver §4 —
21
+ > **los tipos mentían en dos de los cuatro puntos**: `AbortController`/
22
+ > `AbortSignal`/`Headers` SÍ existen y funcionan en runtime pese a no
23
+ > estar declarados en `@lynx-js/types`).
24
+
25
+ ---
26
+
27
+ ## Veredicto (primero, para no enterrarlo)
28
+
29
+ **El gap es más chico de lo que sugerían los tipos instalados —
30
+ cancelación y timeout reales SÍ son posibles.** El spike en device
31
+ (§4) corrigió dos de las cuatro preguntas abiertas en la dirección
32
+ optimista: `@lynx-js/types` no declara `AbortController`, pero el device
33
+ real lo tiene, funciona spec-compliant, y **cancela la conexión de
34
+ verdad** (probado: abort a los 800ms de una respuesta que tarda 5000ms →
35
+ la promesa rechazó a los 805ms, no a los 5000ms — no es un timeout de
36
+ mentira que espera igual). Eso cambia la lista de "imposible" a una lista
37
+ más corta:
38
+
39
+ - `config(xhr) => xhr` (el escape hatch de la API real) — sigue sin
40
+ traducción posible: `fetch` no expone un objeto vivo para mutar a mitad
41
+ de vuelo. Esto no lo arregla el spike.
42
+ - `FormData` como body — confirmado ausente en device (§4). **Corrección**:
43
+ `URLSearchParams` sí está soportado como body — se probó en device
44
+ específicamente porque la doc los agrupaba juntos y ya habíamos visto a
45
+ los tipos/doc equivocarse antes en este mismo documento (no había
46
+ motivo para confiar en la doc sin probar, dado el patrón).
47
+ - `responseType: "blob"` — `Body` en Lynx solo tiene `arrayBuffer()`,
48
+ `json()`, `text()`. Sin `.blob()`.
49
+ - `withCredentials`, `user`/`password` (auth básica vía `xhr.open`),
50
+ `async: false` (modo síncrono) — conceptos exclusivos de
51
+ XMLHttpRequest/browser, sin sentido o sin equivalente en Lynx. Además,
52
+ **`btoa`/`atob` confirmados ausentes en runtime** (spike §4) — armar el
53
+ header `Authorization: Basic` a mano necesitaría una implementación
54
+ propia de base64, no solo un one-liner.
55
+ - **Cancelación y timeout**: YA NO están en la lista de imposibles —
56
+ `AbortController` funciona de verdad (§4). Un `m.request` de v2 puede
57
+ ofrecer cancelación real y un `timeout` que efectivamente corta la
58
+ conexión, no una promesa que se rinde mientras la red sigue trabajando
59
+ de fondo (que era el peor escenario que planteaba la versión anterior de
60
+ este documento).
61
+
62
+ **Recomendación**: implementar un `m.request` de v2 que cubra el caso
63
+ común (lo que de verdad usa el 90% de las apps: JSON in/out, params,
64
+ headers, redraw automático) y **documentar en voz alta, no esconder**,
65
+ que `config`/abort/FormData/blob/timeout/credentials/auth básica no están
66
+ soportados — con una sugerencia explícita de usar el `fetch` nativo de
67
+ Lynx directo para esos casos. No es un "abort total" de la feature, pero
68
+ tampoco es un wrapper que un "mithril fan" pueda usar a ciegas asumiendo
69
+ paridad completa — hay que ser honesto en el primer párrafo del README de
70
+ esta pieza.
71
+
72
+ ---
73
+
74
+ ## 1. Cómo se hace fetch en Lynx
75
+
76
+ - **API real**: `fetch(input, init?)`, descrita como *"subset of Fetch
77
+ API"* — **es la propia doc de tipos oficial la que dice "subconjunto"**,
78
+ no una inferencia mía (`@lynx-js/types/types/background-thread/fetch.d.ts`,
79
+ línea 185).
80
+ - **Solo en el hilo background** — el archivo vive en
81
+ `types/background-thread/`, no en `types/common/` ni `types/main-thread/`.
82
+ Encaja perfecto con la arquitectura de v2: todo el código de vista (y
83
+ por lo tanto cualquier `m.request`) ya corre exclusivamente en
84
+ background (plan `mithril-lynx-v2-desde-cero.md` §3.1) — no hace falta
85
+ ningún puente cross-thread para esto, a diferencia de casi todo lo demás
86
+ en este proyecto.
87
+ - **`RequestInit` real (tipos instalados), completo, sin recortar**:
88
+ ```ts
89
+ export interface RequestInit {
90
+ body?: BodyInit | null;
91
+ headers?: HeadersInit;
92
+ method?: string;
93
+ lynxExtension?: { useStreaming?: boolean };
94
+ }
95
+ ```
96
+ Eso es TODO. Compárese con el `RequestInit` real del navegador (que
97
+ tiene además `mode`, `credentials`, `cache`, `redirect`, `referrer`,
98
+ `referrerPolicy`, `integrity`, `keepalive`, `signal`, `window`, ...) —
99
+ Lynx implementa 3 campos más una extensión propia.
100
+ - **`Response`/`Request extends Body`**: `arrayBuffer()`, `json()`,
101
+ `text()`. **No `blob()`.** `Response` tiene `headers`, `ok`, `status`,
102
+ `statusText`, `url`, `body` (ReadableStream), `clone()` — esa parte SÍ
103
+ es fiel al spec real de `Response`.
104
+ - **Ni `Headers` ni `AbortController` están declarados en
105
+ `@lynx-js/types`** — se usan como tipos referenciados
106
+ (`HeadersInit`, futuro `signal`) pero no existen como constructor/clase
107
+ en ningún `.d.ts` del paquete. **Confirmado en device (§4): los tipos
108
+ mienten acá — ambos existen y funcionan en runtime.** Los tipos de
109
+ `@lynx-js/types` están incompletos/desactualizados en este punto, no son
110
+ la fuente de verdad final — un recordatorio general para el resto de
111
+ este proyecto, no solo para `m.request`.
112
+ - Confirmado por la doc oficial en texto plano: *"Lynx does not support
113
+ Web-only features like: CORS, redirect, keepalive related APIs.
114
+ FormData/Blob related APIs are not supported."*
115
+ - **`fetch` NO es un global usable — hay que llamar `lynx.fetch(...)`
116
+ explícito.** Corrección respecto a una suposición anterior de este
117
+ documento: aunque `RuntimeWrapperWebpackPlugin` incluye `"fetch"` en su
118
+ lista `defaultInjectVars`, el spike en device (§4) confirmó
119
+ `typeof fetch === "undefined"` mientras `typeof lynx.fetch ===
120
+ "function"` en el mismo contexto. `indicadores-app` (la app de
121
+ referencia que el usuario señaló) ya usa `lynx.fetch(...)` por esta
122
+ misma razón — coincide con la evidencia del spike, no es una casualidad
123
+ de esa app.
124
+
125
+ ## 2. El contrato real de `m.request` (lo que hay que igualar o declarar no soportado)
126
+
127
+ Extraído de `node_modules/mithril/request/request.js` (199 líneas, no la
128
+ doc — el código real):
129
+
130
+ | Opción | Mecanismo real (XHR) | ¿Traducible a `fetch` de Lynx? |
131
+ |---|---|---|
132
+ | `method`, `url`, `params` | `xhr.open(method, url)` + interpolación de `:params` en la URL | **Sí** — `fetch(url, {method})`; interpolación reusa `mithril-runtime/pathname/build.js` (ya vendorizado para `m.route`, cero código nuevo) |
133
+ | `body` (objeto plano → JSON) | `xhr.send(JSON.stringify(body))` | **Sí** — `body: JSON.stringify(body)` en `RequestInit`, mismo `Content-Type` header |
134
+ | `body` (`FormData`) | `xhr.send(body)` directo | **No** — confirmado ausente en runtime (§4) |
135
+ | `body` (`URLSearchParams`) | `xhr.send(body)` directo | **Sí** — confirmado en device (§4): funciona igual que en un browser real, `Content-Type: application/x-www-form-urlencoded` automático |
136
+ | `headers` | `xhr.setRequestHeader(k, v)` por cada key | **Sí** — `RequestInit.headers` acepta un objeto plano (`HeadersInit`) |
137
+ | `responseType` (`json`/`text`) | `xhr.responseType` + `xhr.response` | **Sí** — `response.json()` / `response.text()` |
138
+ | `responseType: "blob"`/`"document"` | `xhr.responseType = "blob"` | **No** — sin `.blob()` en `Body`; `"document"` no tiene sentido fuera de un DOM de browser |
139
+ | `deserialize` | función sobre `xhr.response` | **Sí** — misma función, aplicada al resultado de `.json()`/`.text()` |
140
+ | `extract` | `(xhr, options) => any`, salta el flujo normal | **Parcial** — hay que cambiar la firma a `(response, options) => any` (recibe el `Response` de fetch, no un XHR) — **rompe código portado literal**, aunque el propósito (post-proceso custom) se preserva |
141
+ | `type` | constructor aplicado al resultado | **Sí**, sin cambios |
142
+ | `background` | si es `true`, salta el redraw automático | **Sí** — es lógica pura nuestra (mount-redraw), no toca XHR/fetch para nada |
143
+ | Forma del error (`error.code`, `.message`, `.response`) | de `xhr.status`/`xhr.responseText` | **Sí** — `response.status`, `response.statusText`, mismo body ya extraído |
144
+ | `config(xhr) => xhr` | mutar el XHR vivo antes de `.send()` | **No hay traducción real** — lo más cercano es un `config(requestInit) => requestInit` que mute el `RequestInit` ANTES de llamar `fetch()`, pero es una firma y un momento distintos; código que use `config` para engancharse a `xhr.onprogress`, reemplazar el XHR, etc. **no tiene forma de portarse** |
145
+ | `timeout` | `xhr.timeout` nativo, aborta la conexión real | **Sí, de verdad** — `AbortController` + `lynx.setTimeout(() => ctrl.abort(), ms)` **cancela la conexión real**, confirmado en device (§4): abort a los 800ms de una respuesta de 5000ms rechazó a los 805ms, no a los 5000ms. Firma distinta a `xhr.timeout` (hay que armar el controller nosotros) pero el resultado observable es el mismo |
146
+ | Cancelación (`.abort()` vía `config`) | `xhr.abort()` | **Sí** — `AbortController`/`AbortSignal` existen y funcionan en runtime pese a no estar en `@lynx-js/types` (confirmado en device, §4). `m.request` de v2 puede exponer su propio `.abort()`/aceptar un `signal` propio |
147
+ | `withCredentials` | `xhr.withCredentials = true` (cookies cross-origin) | **No aplica** — Lynx no tiene modelo de origen/CORS; la opción quedaría como no-op silencioso si se acepta tal cual |
148
+ | `user`/`password` | pasados a `xhr.open(...)`, auth básica HTTP | **No hay equivalente directo** — habría que armar el header `Authorization: Basic ...` a mano, y eso requiere `btoa()`, cuya disponibilidad en Lynx no está confirmada (no se investigó en esta sesión) |
149
+ | `async: false` (modo síncrono) | `xhr.open(..., false, ...)` | **Imposible** — no existe un `fetch` síncrono en ningún entorno, browser o Lynx |
150
+
151
+ ## 3. No-objetivos explícitos (documentados, no escondidos)
152
+
153
+ Si se implementa (fase futura, no esta sesión), el `README`/`.d.ts` de
154
+ `mithril-lynx-v2/request` debe decir, en la primera pantalla, sin que haga
155
+ falta buscarlo:
156
+
157
+ - `config` cambia de firma (`RequestInit`, no `XMLHttpRequest`) — código
158
+ portado de un `m.request` real que use `config` para algo más que setear
159
+ un header necesita revisión manual, no es un cambio de import.
160
+ - Sin soporte de `FormData`/`Blob` — cualquier caso de subida de archivos
161
+ o multipart queda fuera de alcance por completo. (`URLSearchParams` SÍ
162
+ está soportado — confirmado en device, §4 — así que un body tipo
163
+ formulario simple `key=value` no cae en esta lista.)
164
+ - Sin `withCredentials` (no aplica, Lynx no tiene modelo de CORS/origen) y
165
+ sin `user`/`password` inline — si hace falta auth básica, armar el
166
+ header `Authorization` a mano (y una función de base64 propia, ya que
167
+ `btoa` no existe — confirmado en device, §4).
168
+ - Sin modo síncrono (`async: false`) — no existe en ningún fetch, browser
169
+ o Lynx.
170
+ - `response.url`/`response.redirected` no reflejan la URL final tras un
171
+ redirect (confirmado en device, §4) — si el código de la app depende de
172
+ saber a qué URL terminó yendo la request tras redirects, esto no
173
+ funciona igual que en un browser real.
174
+ - `Headers.get()` no confirmado como confiable para LEER de vuelta
175
+ (ver §4, hallazgo extra) — construir/enviar headers con `new Headers()`
176
+ sí funciona.
177
+
178
+ **Ya NO son no-objetivos** (el spike de §4 los movió a la columna de "sí
179
+ se puede"): cancelación de requests en vuelo, y `timeout` con corte real
180
+ de la conexión — ambos funcionan de verdad vía `AbortController`.
181
+
182
+ ## 4. Spike en device — las 4 preguntas, respondidas (2026-09-18, adb R8YYC0VV0PV)
183
+
184
+ Corrido con `agent-lynx evaluate` contra el background thread de
185
+ `mithril-lynx-v2-app` en el device conectado — no contra
186
+ `indicadores-app` (el usuario señaló esa app como referencia de que
187
+ "fetch funciona ahí", y es cierto, pero solo ejercita el camino feliz
188
+ básico: `lynx.fetch(url)` + `.ok` + `.json()`, sin tocar ninguna de las
189
+ 4 preguntas de este spike).
190
+
191
+ **Nota de herramienta**: `agent-lynx evaluate` espera una única
192
+ EXPRESIÓN, no una secuencia de sentencias con `;` — expresiones con
193
+ `;` al nivel superior tiran `SyntaxError: expecting ')'` desde el wrapper
194
+ interno de la herramienta. Solución: envolver todo en un IIFE
195
+ `(function(){ ...sentencias...; return valor; })()`, que sí es una sola
196
+ expresión. Documentado acá para la próxima sesión que use este spike.
197
+
198
+ 1. **¿Existe `Headers`/`AbortController`/`AbortSignal` en runtime pese a
199
+ no estar en `@lynx-js/types`?** `typeof Headers` → `"function"`,
200
+ `typeof AbortController` → `"function"`, `typeof AbortSignal` →
201
+ `"function"`. **Los tres existen.** Los tipos instalados están
202
+ incompletos en este punto, no son la fuente de verdad final.
203
+ 2. **¿Qué pasa con un 3xx?** `lynx.fetch("https://httpbin.org/redirect-to?url=https://example.com")`
204
+ devolvió `status: 200`, `ok: true`, y el body fue el HTML real de
205
+ `example.com` — **Lynx sigue el redirect solo**, transparente, como un
206
+ browser real. Pero `response.url` quedó con la URL ORIGINAL
207
+ (`httpbin.org/redirect-to?...`), no la final — y `response.redirected`
208
+ vino `undefined`. El redirect en sí funciona; los metadatos sobre el
209
+ redirect no son confiables.
210
+ 3. **¿Existen `btoa`/`atob`?** `typeof btoa` → `"undefined"`, `typeof atob`
211
+ → `"undefined"`. **Confirmado ausentes.** Un helper de auth básica
212
+ necesitaría una implementación propia de base64.
213
+ 4. **¿`AbortController` funciona de verdad (no solo existe)?** Sí,
214
+ spec-compliant: `ctrl.abort()` inmediato → la promesa rechaza con
215
+ `{name: "AbortError", message: "This operation was aborted"}`.
216
+ Prueba más fuerte — abort a mitad de vuelo: `lynx.fetch(".../delay/5",
217
+ {signal: ctrl.signal})` + `lynx.setTimeout(() => ctrl.abort(), 800)` →
218
+ la promesa rechazó a los **805ms**, no a los 5000ms del delay real del
219
+ servidor — **la conexión se cortó de verdad**, no fue una promesa que
220
+ se rindió mientras la red seguía trabajando.
221
+
222
+ **Hallazgo extra, no una de las 4 preguntas originales pero relevante**:
223
+ `fetch` global (bare, sin `lynx.`) es `undefined` — hay que llamar
224
+ `lynx.fetch(...)` siempre. Se había asumido lo contrario en una versión
225
+ anterior de este documento (por estar `"fetch"` en la lista
226
+ `defaultInjectVars` de `RuntimeWrapperWebpackPlugin`) — el spike lo
227
+ corrigió. Coincide con que `indicadores-app` (la app que el usuario
228
+ señaló) ya usa `lynx.fetch(...)` explícito, no por casualidad.
229
+
230
+ Segundo hallazgo extra, **re-confirmado con un test limpio y deliberado**
231
+ (el primer intento coincidió con un corte de sesión — pantalla del device
232
+ bloqueada — y no era confiable por sí solo, así que se repitió antes de
233
+ darlo por bueno): `new Headers({...})` pasado a `fetch(url, {headers})`
234
+ **sí llega bien al servidor** — pero **`Headers.get()`/`.has()` son
235
+ case-*sensitive*** en Lynx: `h.set("X-Test", "abc"); h.get("x-test")` →
236
+ `null`; `h.has("x-test")` → `false`. El spec real de `Headers` es
237
+ explícitamente case-**in**sensitive (`Content-Type` y `content-type` son
238
+ la misma clave) — esta es una desviación real y confirmada, no un
239
+ artefacto de sesión. Construir/enviar headers funciona perfecto; leer de
240
+ vuelta con la clave en otra capitalización que la usada para escribir,
241
+ no.
242
+
243
+ **Tercer hallazgo extra**: aunque `FormData` está confirmado ausente
244
+ (`typeof FormData` → `"undefined"`), **`URLSearchParams` SÍ existe y
245
+ funciona como body real** — `lynx.fetch(url, {method:"POST", body: new
246
+ URLSearchParams({foo:"bar"})})` llegó al servidor como
247
+ `application/x-www-form-urlencoded` con el `Content-Type` seteado
248
+ automático y los campos bien parseados (`{foo:"bar"}`). Esto **mejora**
249
+ la fila de la tabla §2 sobre `body` no-JSON: la mitad de esa opción real
250
+ de `m.request` (`FormData`) sigue sin soporte, pero la otra mitad
251
+ (`URLSearchParams`) **sí se puede replicar fiel**, sin necesitar
252
+ `JSON.stringify` ni tocar el `Content-Type` a mano.
253
+
254
+ ## 5. Si se decide implementar: forma concreta (fase futura, no esta sesión)
255
+
256
+ Mismo patrón que `mithril-lynx-v2/route`: un subpath export nuevo
257
+ (`mithril-lynx-v2/request`), NO agregado al core (`renderApp`) — es
258
+ opcional, un consumidor que no lo importe no paga nada por él.
259
+
260
+ ```js
261
+ // forma esperada, análoga a m.request real
262
+ import request from "mithril-lynx-v2/request";
263
+
264
+ request("/api/users/:id", { params: { id: 42 } })
265
+ .then((user) => { ... });
266
+ ```
267
+
268
+ Reutiliza `mithril-runtime/pathname/build.js` (ya vendorizado para
269
+ `m.route`) para la interpolación de `:params` en la URL — mismo motor,
270
+ cero código nuevo ahí. El resto (armar `RequestInit`, llamar `fetch`,
271
+ aplicar `deserialize`/`extract`/`type`, construir el error con
272
+ `response.status`) es código nuevo pero acotado — el propio
273
+ `request/request.js` real (199 líneas) da la plantilla exacta de qué
274
+ casos cubrir, con las opciones de la tabla de §2 marcadas como
275
+ "soportado"/"no soportado" desde el día uno.
276
+
277
+ **Integración con redraw**: igual que `m.route`, el flag `background`
278
+ decide si se llama al único `performRender`/commit de la app
279
+ (`background.js`) al resolver la promesa — reusa el mismo mecanismo,
280
+ no inventa un segundo camino de redraw.
281
+
282
+ **Cancelación/timeout** (nuevo respecto a la versión anterior de este
283
+ plan, gracias al spike de §4): cada llamada arma su propio
284
+ `AbortController` internamente. `timeout` (si se pasa) hace
285
+ `lynx.setTimeout(() => ctrl.abort(), timeout)` y limpia el timer si la
286
+ promesa ya resolvió. La función devuelta por `request(...)` puede colgar
287
+ un `.abort()` propio (no existe en la promesa real de mithril, pero es
288
+ gratis tenerlo acá y es justo lo que un "mithril fan" pediría después de
289
+ la falta de `config`) que llama `ctrl.abort()` directo — capacidad nueva
290
+ que ni el `m.request` real ofrece de forma tan directa (ahí hay que pasar
291
+ por `config` para llegar al `xhr.abort()`).
292
+
293
+ ## 6. Referencias
294
+
295
+ - Spec real de `m.request`: <https://mithril.js.org/request.html>
296
+ - `fetch` de Lynx (doc): <https://lynxjs.org/api/lynx-api/global/fetch.html>
297
+ - Tipos reales instalados (evidencia, no doc):
298
+ `@lynx-js/types/types/background-thread/fetch.d.ts`,
299
+ `@lynx-js/types/types/background-thread/lynx.d.ts`
300
+ - Código fuente real de `m.request`:
301
+ `node_modules/mithril/request/request.js` (mithril 2.3.8, mismo
302
+ checkout que ya usa `mithril-runtime`)
303
+ - Precedente directo de esta sesión: `m-route-en-memoria.md` — mismo
304
+ patrón de "vendorizar la parte pura de Mithril, reimplementar la parte
305
+ atada a browser sobre la primitiva de Lynx equivalente, documentar la
306
+ brecha sin esconderla".