mithril-lynx 2.6.2 → 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/FETCH_INVESTIGATION.md +3 -2
  3. package/INPUT.md +115 -0
  4. package/README.md +47 -8
  5. package/ROUTE.md +111 -16
  6. package/ROUTE_CONTRACT_ANALYSIS.md +224 -0
  7. package/UPDATE_ANIMATION_GAP.md +133 -0
  8. package/package.json +1 -9
  9. package/plugin.js +33 -0
  10. package/src/apply-patch.js +369 -60
  11. package/src/backends/virtual-backend.js +120 -22
  12. package/src/background.d.ts +26 -0
  13. package/src/background.js +116 -7
  14. package/src/channel.js +22 -5
  15. package/src/commit.js +9 -0
  16. package/src/dev-reload-client.js +46 -0
  17. package/src/dev-transport-noop.js +3 -0
  18. package/src/fake-dom.js +559 -61
  19. package/src/list-attributes.js +69 -0
  20. package/src/list-runtime.js +425 -0
  21. package/src/main-thread.js +18 -1
  22. package/src/mount-redraw.js +14 -1
  23. package/src/patch-protocol.js +41 -16
  24. package/src/reload/version.js +10 -1
  25. package/src/request.js +38 -0
  26. package/src/route.d.ts +30 -2
  27. package/src/route.js +214 -56
  28. package/src/testing.d.ts +14 -1
  29. package/src/testing.js +56 -1
  30. package/test/event-render-coalescing.test.ts +117 -0
  31. package/test/gesture.test.ts +75 -0
  32. package/test/input.test.ts +388 -0
  33. package/test/list.test.ts +417 -113
  34. package/test/remove-subtree-cleanup.test.ts +75 -0
  35. package/test/route-hot-reload.test.ts +2 -1
  36. package/test/route.test.ts +201 -4
  37. package/test/style-clear.test.ts +97 -0
  38. package/.omo/plans/m-request-fetch-lynx.md +0 -306
  39. package/.omo/plans/m-route-en-memoria.md +0 -397
  40. package/.omo/plans/mithril-lynx-v2-desde-cero.md +0 -548
  41. package/.omo/run-continuation/ses_f2f02336cffeO0ssvuxqZ05iZq.json +0 -10
  42. package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.json +0 -10
  43. package/src/list-cell.d.ts +0 -20
  44. package/src/list-cell.js +0 -73
  45. package/src/list-support.d.ts +0 -13
  46. package/src/list-support.js +0 -197
@@ -8,81 +8,192 @@
8
8
 
9
9
  import { Op, pushOp } from "../patch-protocol.js";
10
10
 
11
+ /**
12
+ * Creates the background-thread patch backend: every call appends one op to a flat buffer.
13
+ * @returns {{allocId: () => number, createElement: (tag: string) => number, createElementNS: (ns: string, tag: string) => number, createText: (text: string) => number, insertBefore: (parentId: number, childId: number, refId?: number) => void, removeChild: (parentId: number, childId: number) => void, setAttribute: (id: number, name: string, value: *) => void, removeAttribute: (id: number, name: string) => void, setAttributeNS: (id: number, ns: string|null, name: string, value: string|null) => void, setClasses: (id: number, value: string) => void, setStyleProperty: (id: number, name: string, value: string) => void, removeStyleProperty: (id: number, name: string) => void, setText: (id: number, value: string) => void, addEvent: (id: number, type: string) => void, removeEvent: (id: number, type: string) => void, setGestureDetector: (id: number, gestureId: number, gestureType: string, arenaPolicy: string) => void, removeGestureDetector: (id: number, gestureId: number) => void, invokeUIMethod: (id: number, method: string, params: Object, callbackId: number) => void, setInputValue: (id: number, value: string, seq: number) => void, onUIMethodOp: (() => void)|null, takeOps: () => unknown[]|null}} The backend object.
14
+ */
11
15
  export function createVirtualBackend() {
12
16
  let nextId = 1;
13
17
  let ops = [];
14
18
 
15
- return {
19
+ const backend = {
16
20
  // Exposed for tests that want to assert on id allocation directly;
17
21
  // application code should never need it.
22
+ /**
23
+ * @returns {number} A fresh element id.
24
+ */
18
25
  allocId() {
19
26
  return nextId++;
20
27
  },
28
+ /**
29
+ * @param {string} tag - The tag name.
30
+ * @returns {number} The new element's id.
31
+ */
21
32
  createElement(tag) {
22
33
  const id = nextId++;
23
34
  pushOp(ops, Op.CreateElement, tag, id);
24
35
  return id;
25
36
  },
37
+ /**
38
+ * @param {string} ns - The namespace URI.
39
+ * @param {string} tag - The tag name.
40
+ * @returns {number} The new element's id.
41
+ */
26
42
  createElementNS(ns, tag) {
27
43
  const id = nextId++;
28
44
  pushOp(ops, Op.CreateElementNS, ns, tag, id);
29
45
  return id;
30
46
  },
47
+ /**
48
+ * @param {string} text - The initial text.
49
+ * @returns {number} The new text node's id.
50
+ */
31
51
  createText(text) {
32
52
  const id = nextId++;
33
53
  pushOp(ops, Op.CreateText, text, id);
34
54
  return id;
35
55
  },
56
+ /**
57
+ * @param {number} parentId - Parent element id.
58
+ * @param {number} childId - Child id to insert.
59
+ * @param {number} [refId] - Id to insert before; `-1` (or omitted) appends.
60
+ * @returns {void}
61
+ */
36
62
  insertBefore(parentId, childId, refId) {
37
63
  pushOp(ops, Op.InsertBefore, parentId, childId, refId ?? -1);
38
64
  },
65
+ /**
66
+ * @param {number} parentId - Parent element id.
67
+ * @param {number} childId - Child id to remove.
68
+ * @returns {void}
69
+ */
39
70
  removeChild(parentId, childId) {
40
71
  pushOp(ops, Op.RemoveChild, parentId, childId);
41
72
  },
73
+ /**
74
+ * @param {number} id - Element id.
75
+ * @param {string} name - Attribute name.
76
+ * @param {*} value - Attribute value: a string, or a raw typed value for a catalogued list/list-item attribute.
77
+ * @returns {void}
78
+ */
42
79
  setAttribute(id, name, value) {
43
80
  pushOp(ops, Op.SetAttribute, id, name, value);
44
81
  },
82
+ /**
83
+ * @param {number} id - Element id.
84
+ * @param {string} name - Attribute name.
85
+ * @returns {void}
86
+ */
45
87
  removeAttribute(id, name) {
46
88
  pushOp(ops, Op.RemoveAttribute, id, name);
47
89
  },
90
+ /**
91
+ * @param {number} id - Element id.
92
+ * @param {string|null} ns - Attribute namespace.
93
+ * @param {string} name - Attribute name.
94
+ * @param {string|null} value - Attribute value.
95
+ * @returns {void}
96
+ */
48
97
  setAttributeNS(id, ns, name, value) {
49
98
  pushOp(ops, Op.SetAttributeNS, id, ns, name, value);
50
99
  },
100
+ /**
101
+ * @param {number} id - Element id.
102
+ * @param {string} value - The class string.
103
+ * @returns {void}
104
+ */
51
105
  setClasses(id, value) {
52
106
  // Encoded as a SetAttribute on the synthetic "class" name — the
53
107
  // patch applier special-cases that name into `__SetClasses`, the
54
108
  // same split fake-dom.js already does on the way in.
55
109
  pushOp(ops, Op.SetAttribute, id, "class", value);
56
110
  },
111
+ /**
112
+ * @param {number} id - Element id.
113
+ * @param {string} name - Dash-case style property name.
114
+ * @param {string} value - Style value.
115
+ * @returns {void}
116
+ */
57
117
  setStyleProperty(id, name, value) {
58
118
  pushOp(ops, Op.SetStyleProperty, id, name, value);
59
119
  },
120
+ /**
121
+ * @param {number} id - Element id.
122
+ * @param {string} name - Dash-case style property name (`"*"` requests a bulk clear).
123
+ * @returns {void}
124
+ */
60
125
  removeStyleProperty(id, name) {
61
126
  pushOp(ops, Op.RemoveStyleProperty, id, name);
62
127
  },
128
+ /**
129
+ * @param {number} id - Text node id.
130
+ * @param {string} value - The new text.
131
+ * @returns {void}
132
+ */
63
133
  setText(id, value) {
64
134
  pushOp(ops, Op.SetText, id, value);
65
135
  },
136
+ /**
137
+ * @param {number} id - Element id.
138
+ * @param {string} type - Event type.
139
+ * @returns {void}
140
+ */
66
141
  addEvent(id, type) {
67
142
  pushOp(ops, Op.AddEvent, id, type);
68
143
  },
144
+ /**
145
+ * @param {number} id - Element id.
146
+ * @param {string} type - Event type.
147
+ * @returns {void}
148
+ */
69
149
  removeEvent(id, type) {
70
150
  pushOp(ops, Op.RemoveEvent, id, type);
71
151
  },
152
+ /**
153
+ * @param {number} id - Element id.
154
+ * @param {number} gestureId - Unique gesture id.
155
+ * @param {string} gestureType - Native gesture type name.
156
+ * @param {string} arenaPolicy - Claim/release policy.
157
+ * @returns {void}
158
+ */
72
159
  setGestureDetector(id, gestureId, gestureType, arenaPolicy) {
73
160
  pushOp(ops, Op.SetGestureDetector, id, gestureId, gestureType, arenaPolicy);
74
161
  },
162
+ /**
163
+ * @param {number} id - Element id.
164
+ * @param {number} gestureId - Gesture id to remove.
165
+ * @returns {void}
166
+ */
75
167
  removeGestureDetector(id, gestureId) {
76
168
  pushOp(ops, Op.RemoveGestureDetector, id, gestureId);
77
169
  },
78
- createList(scrollOrientation, listType, spanCount) {
79
- const id = nextId++;
80
- pushOp(ops, Op.CreateList, id, scrollOrientation, listType, spanCount);
81
- return id;
82
- },
83
- setListItems(id, cells) {
84
- pushOp(ops, Op.SetListItems, id, cells);
170
+ /**
171
+ * Records a native UI method call (Op.InvokeUIMethod).
172
+ * @param {number} id - Element id.
173
+ * @param {string} method - UI method name.
174
+ * @param {Object} params - Its parameters.
175
+ * @param {number} callbackId - `0` for none, else the document's pending-invoke id.
176
+ * @returns {void}
177
+ */
178
+ invokeUIMethod(id, method, params, callbackId) {
179
+ pushOp(ops, Op.InvokeUIMethod, id, method, params, callbackId);
180
+ backend.onUIMethodOp?.();
85
181
  },
182
+ /**
183
+ * Records an <input>/<textarea> value change (Op.SetInputValue).
184
+ * @param {number} id - Element id.
185
+ * @param {string} value - The new text.
186
+ * @param {number} seq - The field's input-event count when the value was computed.
187
+ * @returns {void}
188
+ */
189
+ setInputValue(id, value, seq) {
190
+ pushOp(ops, Op.SetInputValue, id, value, seq);
191
+ backend.onUIMethodOp?.();
192
+ },
193
+ /** Called after a UI method op is recorded — set by renderApp() to
194
+ * flush calls made outside a render pass (a timer, a promise), which
195
+ * no render would otherwise commit. */
196
+ onUIMethodOp: null,
86
197
  /** Drains and returns the accumulated ops. Called once per commit. */
87
198
  takeOps() {
88
199
  if (ops.length === 0) return null;
@@ -90,19 +201,6 @@ export function createVirtualBackend() {
90
201
  ops = [];
91
202
  return out;
92
203
  },
93
- /**
94
- * Runs `fn` (a DOM mutation against a node from THIS backend's own
95
- * document — same id space as everything else, so event dispatch
96
- * keeps working normally) and returns just the ops it produced,
97
- * removing them from the shared buffer so they never also go out
98
- * with the next `takeOps()`. Used by list-cell.js to render one list
99
- * item off-tree and ship its construction ops separately, instead of
100
- * as part of the app's own visible-tree patch.
101
- */
102
- captureOps(fn) {
103
- const start = ops.length;
104
- fn();
105
- return ops.splice(start, ops.length - start);
106
- },
107
204
  };
205
+ return backend;
108
206
  }
@@ -10,6 +10,32 @@ export interface LynxNode {
10
10
  dispatchEvent(event: unknown): unknown;
11
11
  }
12
12
 
13
+ /** A fake-DOM element (`vnode.dom` of any element vnode). Its UI methods run
14
+ * on the main thread right after the patch that carries them is flushed, so
15
+ * they are safe from `oncreate`; no `id` or selector query is involved. */
16
+ export interface LynxElement extends LynxNode {
17
+ /** Calls a native UI method (`getValue`, `scrollTo`, `autoScroll`, …).
18
+ * Rejects with an `Error` carrying the native `code`/`data`, or when the
19
+ * element is removed before the call runs. */
20
+ invoke<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
21
+ /** The native `focus` UI method (fire-and-forget). */
22
+ focus(): void;
23
+ /** The native `blur` UI method (fire-and-forget). */
24
+ blur(): void;
25
+ }
26
+
27
+ /** `vnode.dom` of an `input` or `textarea`. `value` mirrors the native text:
28
+ * synced from every event of the field, and sent with `setValue` when set to
29
+ * something else. */
30
+ export interface LynxFormFieldElement extends LynxElement {
31
+ value: string;
32
+ readonly selectionStart: number;
33
+ readonly selectionEnd: number;
34
+ /** Focuses the field once, on creation (set through the `autofocus` attribute). */
35
+ autofocus: boolean;
36
+ setSelectionRange(selectionStart: number, selectionEnd: number): Promise<unknown>;
37
+ }
38
+
13
39
  /** The subset of the fake-DOM document exposed on the render handle — used
14
40
  * by tests and by an app's own HMR glue, never by the channel wiring. */
15
41
  export interface LynxDocument {
package/src/background.js CHANGED
@@ -10,8 +10,19 @@
10
10
  // by which "a tap handler that mutates state repaints the screen" works,
11
11
  // with no cooperation from app code. v1's bug was that the function on the
12
12
  // other end of that call was sometimes undefined depending on load order;
13
- // here it is `performRender` — a plain closure, captured once, always the
13
+ // here it is `requestRender` — a plain closure, captured once, always the
14
14
  // same reference, never looked up through a global.
15
+ //
16
+ // `requestRender` renders synchronously (it calls `performRender`) for every
17
+ // redraw except one kind: the automatic redraw after a HIGH-FREQUENCY event
18
+ // (`gesturemove`, `touchmove`, `scroll`). Those fire 60-120 times a second
19
+ // during a drag, and a synchronous render after each one means a full diff,
20
+ // commit and patch back to the main thread per sample. Instead, the handler
21
+ // still runs for every single event (every sample reaches app code, in
22
+ // order), but the render it asks for is coalesced into at most one per
23
+ // frame. Any other render — a tap, `gestureup`, `redraw()`, a promise
24
+ // handler's later redraw — renders immediately and turns a pending frame
25
+ // render into a no-op, so the final state is always painted in order.
15
26
 
16
27
  import renderFactory from "mithril-runtime/render/render.js";
17
28
  import { createLynxDocument } from "./fake-dom.js";
@@ -19,6 +30,28 @@ import { createVirtualBackend } from "./backends/virtual-backend.js";
19
30
  import { createCommitController } from "./commit.js";
20
31
  import { onEventFromMainThread, sendPatchToMainThread } from "./channel.js";
21
32
  import { register as registerRedraw } from "./mount-redraw.js";
33
+ import { INVOKE_RESULT_EVENT } from "./patch-protocol.js";
34
+
35
+ /** Event types whose automatic redraw is coalesced to one per frame. Only
36
+ * add types that fire continuously during one interaction (every sample of
37
+ * a drag/scroll): their handlers still run per event, but the screen only
38
+ * catches up once per frame. Discrete events (tap, input, gestureup) must
39
+ * stay out so they keep rendering synchronously. */
40
+ const HIGH_FREQUENCY_EVENTS = new Set(["gesturemove", "touchmove", "scroll"]);
41
+
42
+ /**
43
+ * Runs `fn` on the next frame: `lynx.requestAnimationFrame` when available,
44
+ * otherwise a ~16ms timer.
45
+ * @param {() => void} fn - The callback to run.
46
+ * @returns {void}
47
+ */
48
+ function scheduleFrame(fn) {
49
+ if (typeof lynx !== "undefined" && typeof lynx.requestAnimationFrame === "function") {
50
+ lynx.requestAnimationFrame(fn);
51
+ } else {
52
+ setTimeout(fn, 16);
53
+ }
54
+ }
22
55
 
23
56
  /**
24
57
  * @param {object} options
@@ -29,44 +62,120 @@ import { register as registerRedraw } from "./mount-redraw.js";
29
62
  * worth of ops across the thread boundary. Defaults to the real channel
30
63
  * (channel.js, F0.1's decision) — tests override it to capture ops
31
64
  * in-process instead.
65
+ * @param {(handler: (event: {data: {id: number, type: string, payload?: object}}) => void) => void} [options.subscribeEvents] -
66
+ * Subscribes the forwarded-event router. Defaults to the real channel's
67
+ * `onEventFromMainThread` — tests override it to drive the router directly.
68
+ * @returns {{redraw: () => void, document: import("./fake-dom.js").LynxDocument}} The redraw function and the app's fake document.
32
69
  */
33
- export function renderApp({ root, sendPatch = sendPatchToMainThread }) {
70
+ export function renderApp({ root, sendPatch = sendPatchToMainThread, subscribeEvents = onEventFromMainThread }) {
34
71
  const backend = createVirtualBackend();
35
72
  const document = createLynxDocument(backend);
36
73
  const render = renderFactory();
37
74
  const commitController = createCommitController();
38
75
 
76
+ /**
77
+ * Drains the backend's ops and sends them, if there are any.
78
+ * @returns {void}
79
+ */
39
80
  function flush() {
40
81
  const ops = backend.takeOps();
41
82
  if (ops) sendPatch(ops);
42
83
  }
43
84
  commitController.install(flush);
44
85
 
86
+ // A UI method call made outside a render pass (`dom.focus()` in a timer,
87
+ // `dom.invoke()` after a fetch) has no commit to travel with: send it at
88
+ // the end of the current task. Inside a render (or an event handler,
89
+ // which is followed by one) the render's own commit sends it first and
90
+ // this finds nothing left to send.
91
+ let uiFlushScheduled = false;
92
+ backend.onUIMethodOp = () => {
93
+ if (uiFlushScheduled) return;
94
+ uiFlushScheduled = true;
95
+ Promise.resolve().then(() => {
96
+ uiFlushScheduled = false;
97
+ flush();
98
+ });
99
+ };
100
+
45
101
  // This is Mithril's actual redraw service pattern (the same shape as
46
102
  // upstream `mithril/api/mount-redraw.js`'s internal `run()`): the
47
103
  // "redraw" callback IS "call render() again", not "send a patch"
48
104
  // directly — flushing is a side effect of every render pass completing,
49
105
  // whether that pass was the first one, an auto-redraw after an event, a
50
106
  // manual `redraw()` call, or a hot-update re-render (see reload/*.js).
107
+ /**
108
+ * Runs one full render pass and commits the resulting patch, synchronously.
109
+ * @returns {void}
110
+ */
51
111
  function performRender() {
52
- render(document, root(), performRender);
112
+ framePending = false;
113
+ render(document, root(), requestRender);
53
114
  commitController.commit();
54
115
  }
55
116
 
117
+ // True only while a high-frequency event's handler is running.
118
+ let dispatchingHighFrequency = false;
119
+ // True while a coalesced render is scheduled for the next frame. Any
120
+ // render in between clears it, so the scheduled one becomes a no-op
121
+ // instead of needing a cancel API.
122
+ let framePending = false;
123
+
124
+ /**
125
+ * Mithril's redraw callback: renders now, except after a high-frequency
126
+ * event, where it schedules at most one render for the next frame.
127
+ * @returns {void}
128
+ */
129
+ function requestRender() {
130
+ if (!dispatchingHighFrequency) {
131
+ performRender();
132
+ return;
133
+ }
134
+ if (framePending) return;
135
+ framePending = true;
136
+ scheduleFrame(() => {
137
+ if (framePending) performRender();
138
+ });
139
+ }
140
+
56
141
  performRender();
57
142
  registerRedraw(performRender);
58
143
 
59
144
  // Wires every forwarded native event straight to the fake-dom node it
60
145
  // targets — `dispatchEvent` (fake-dom.js) then invokes Mithril's own
61
146
  // EventDict exactly as a real DOM would, and (per the contract at the
62
- // top of this file) `performRender` auto-fires afterward if the
147
+ // top of this file) `requestRender` auto-fires afterward if the
63
148
  // handler doesn't opt out. This is the ONLY consumer of
64
149
  // `onEventFromMainThread` — app code never touches the channel directly.
65
- onEventFromMainThread((event) => {
66
- const { id, type, payload } = event.data;
150
+ subscribeEvents((event) => {
151
+ const { id, type, payload, seq } = event.data;
152
+ if (type === INVOKE_RESULT_EVENT) {
153
+ document.resolveInvoke(payload);
154
+ return;
155
+ }
67
156
  const node = document.getNodeById(id);
68
157
  if (!node) return;
69
- node.dispatchEvent({ type, currentTarget: node, preventDefault() {}, stopPropagation() {}, ...payload });
158
+ // An <input>/<textarea> learns its native value before any handler
159
+ // runs, as a browser input would (see fake-dom.js).
160
+ if (typeof node._syncFromEvent === "function") node._syncFromEvent(payload, seq);
161
+ dispatchingHighFrequency = HIGH_FREQUENCY_EVENTS.has(type);
162
+ try {
163
+ node.dispatchEvent({
164
+ type,
165
+ currentTarget: node,
166
+ defaultPrevented: false,
167
+ // Lynx has no default action to cancel; recording the call lets
168
+ // handlers that check `e.defaultPrevented` (route.Link, like
169
+ // upstream m.route.Link) behave as on the web.
170
+ preventDefault() {
171
+ this.defaultPrevented = true;
172
+ },
173
+ stopPropagation() {},
174
+ ...payload,
175
+ });
176
+ } finally {
177
+ dispatchingHighFrequency = false;
178
+ }
70
179
  });
71
180
 
72
181
  return {
package/src/channel.js CHANGED
@@ -28,22 +28,39 @@ import { PROTOCOL_VERSION } from "./patch-protocol.js";
28
28
  * The patch is prefixed with `PROTOCOL_VERSION` so the main thread can
29
29
  * detect a stale/desynced bundle (partial HMR, cache) and fail loudly
30
30
  * instead of re-interpreting reordered opcodes against the shared id space.
31
+ * @param {unknown[]} ops - The flat op array for one commit.
32
+ * @returns {void}
31
33
  */
32
34
  export function sendPatchToMainThread(ops) {
33
35
  lynx.getCoreContext().dispatchEvent({ type: patchEventName, data: [PROTOCOL_VERSION, ...ops] });
34
36
  }
35
37
 
36
- /** Background thread: receive a forwarded native event `{ id, type, payload }`. */
38
+ /** Background thread: receive a forwarded native event `{ id, type, payload, seq? }`
39
+ * (`seq`: see apply-patch.js's `fields`), or an Op.InvokeUIMethod result
40
+ * (`type` = `INVOKE_RESULT_EVENT`, see patch-protocol.js).
41
+ * @param {(event: {data: {id: number, type: string, payload: unknown, seq?: number}}) => void} handler - Called for every forwarded event.
42
+ * @returns {void}
43
+ */
37
44
  export function onEventFromMainThread(handler) {
38
45
  lynx.getCoreContext().addEventListener(eventFromMainThreadEventName, handler);
39
46
  }
40
47
 
41
- /** Main thread: receive a patch (an ops array) from the background thread. */
48
+ /** Main thread: receive a patch (an ops array) from the background thread.
49
+ * @param {(event: {data: unknown}) => void} handler - Called with each version-prefixed patch.
50
+ * @returns {void}
51
+ */
42
52
  export function onPatchFromBackground(handler) {
43
53
  lynx.getJSContext().addEventListener(patchEventName, handler);
44
54
  }
45
55
 
46
- /** Main thread: forward a native event on element `id` back to the background thread. */
47
- export function sendEventToBackground(id, type, payload) {
48
- lynx.getJSContext().dispatchEvent({ type: eventFromMainThreadEventName, data: { id, type, payload } });
56
+ /** Main thread: forward a native event on element `id` back to the background thread.
57
+ * @param {number} id - The element id.
58
+ * @param {string} type - The event type.
59
+ * @param {unknown} payload - The native event payload.
60
+ * @param {number} [seq] - For an <input>/<textarea>, its native `input` event count.
61
+ * @returns {void}
62
+ */
63
+ export function sendEventToBackground(id, type, payload, seq) {
64
+ const data = seq === undefined ? { id, type, payload } : { id, type, payload, seq };
65
+ lynx.getJSContext().dispatchEvent({ type: eventFromMainThreadEventName, data });
49
66
  }
package/src/commit.js CHANGED
@@ -20,6 +20,10 @@
20
20
 
21
21
  const NOT_MOUNTED = Symbol("mithril-lynx:not-mounted");
22
22
 
23
+ /**
24
+ * Creates the single-use commit controller for one `renderApp()` call.
25
+ * @returns {{install: (fn: () => void) => void, commit: () => void}} The controller.
26
+ */
23
27
  export function createCommitController() {
24
28
  let commitFn = NOT_MOUNTED;
25
29
 
@@ -29,6 +33,9 @@ export function createCommitController() {
29
33
  * `renderApp()` (background.js), before Mithril's `render()` is ever
30
34
  * invoked — so nothing can observe the "not mounted yet" state from
31
35
  * inside a redraw.
36
+ * @param {() => void} fn - The commit callback.
37
+ * @returns {void}
38
+ * @throws {Error} If a commit callback was already installed.
32
39
  */
33
40
  install(fn) {
34
41
  if (commitFn !== NOT_MOUNTED) {
@@ -51,6 +58,8 @@ export function createCommitController() {
51
58
  * after any event handler runs, with no cooperation required from
52
59
  * app code — that automatic call is what makes redraw "just work"
53
60
  * like it does in React/Preact.
61
+ * @returns {void}
62
+ * @throws {Error} If called before a commit callback was installed.
54
63
  */
55
64
  commit() {
56
65
  if (commitFn === NOT_MOUNTED) {
@@ -28,6 +28,11 @@
28
28
 
29
29
  var MRL = "[mrl-trace]";
30
30
 
31
+ /**
32
+ * Parses a webpack `__resourceQuery` string into a key/value map.
33
+ * @param {string} query - The query string, beginning with `?`.
34
+ * @returns {Object<string, string>} The decoded values (empty when `query` is not a `?` string).
35
+ */
31
36
  function parseResourceQuery(query) {
32
37
  var values = {};
33
38
  if (typeof query !== "string" || !query.startsWith("?")) return values;
@@ -41,6 +46,11 @@ function parseResourceQuery(query) {
41
46
  return values;
42
47
  }
43
48
 
49
+ /**
50
+ * Builds the dev-server WebSocket URL.
51
+ * @param {{protocol?: string, hostname?: string, port?: string, pathname?: string, token?: string}} options - Connection settings parsed from the resource query.
52
+ * @returns {string} The `ws(s)://` URL.
53
+ */
44
54
  function socketURL(options) {
45
55
  var hostname = options.hostname || "";
46
56
  var port = options.port ? ":" + options.port : "";
@@ -53,6 +63,9 @@ function socketURL(options) {
53
63
  * Lynx keys both the HTTP layer and its bytecode cache by URL — a
54
64
  * `Page.reload` with the SAME url re-runs the previous bundle byte-for-byte
55
65
  * even with `ignoreCache: true` (measured on-device against v1, 0.0.8).
66
+ * @param {string} url - The bundle URL.
67
+ * @param {number} [now=Date.now()] - Timestamp used for the `t` parameter.
68
+ * @returns {string|undefined} The URL with a `t` parameter, or `undefined` when `url` is not an http(s) URL.
56
69
  */
57
70
  function cacheBustedUrl(url, now) {
58
71
  if (typeof url !== "string" || !/^https?:\/\//i.test(url)) return undefined;
@@ -94,6 +107,10 @@ var reloading = false;
94
107
  // against this hash once it settles.
95
108
  var pendingRecheckHash;
96
109
 
110
+ /**
111
+ * Asks the DevTool native module to reload the page (`Page.reload`, cache ignored).
112
+ * @returns {boolean} `true` when the reload request was sent, `false` when the native module is unavailable.
113
+ */
97
114
  function invokeCdpReload() {
98
115
  var upperCase = typeof NativeModules !== "undefined" ? NativeModules.LynxDevToolSetModule : undefined;
99
116
  var lowerCase = typeof NativeModules !== "undefined" ? NativeModules.LynxDevtoolSetModule : undefined;
@@ -110,6 +127,10 @@ function invokeCdpReload() {
110
127
 
111
128
  invokeCdp(
112
129
  JSON.stringify({ method: "Page.reload", params }),
130
+ /**
131
+ * @param {string} [data] - The raw CDP response; logged when it carries an error.
132
+ * @returns {void}
133
+ */
113
134
  function (data) {
114
135
  if (!data) return;
115
136
  try {
@@ -123,6 +144,11 @@ function invokeCdpReload() {
123
144
  return true;
124
145
  }
125
146
 
147
+ /**
148
+ * Closes the socket and triggers a full page reload.
149
+ * @param {string} [reason] - Why the reload was requested (logged).
150
+ * @returns {void}
151
+ */
126
152
  function reload(reason) {
127
153
  console.info(MRL + ":8 reload-called", JSON.stringify({ reason: reason || "unspecified" }));
128
154
  reloading = true;
@@ -134,6 +160,11 @@ function reload(reason) {
134
160
  }
135
161
  }
136
162
 
163
+ /**
164
+ * Runs `module.hot.check(true)`, coalescing a queued newer hash or falling back to a full reload.
165
+ * @param {string} hash - The build hash being checked.
166
+ * @returns {void}
167
+ */
137
168
  function runCheck(hash) {
138
169
  console.info(MRL + ":5 hmr-check-calling", "module.hot.check(true)");
139
170
  module.hot.check(true).then(function (updatedModules) {
@@ -165,6 +196,11 @@ function runCheck(hash) {
165
196
  });
166
197
  }
167
198
 
199
+ /**
200
+ * Starts an HMR check for `hash`, queues it if another check is in flight, or reloads when HMR is unavailable.
201
+ * @param {string} hash - The build hash to check.
202
+ * @returns {void}
203
+ */
168
204
  function maybeCheck(hash) {
169
205
  var hasHot = typeof module !== "undefined" && module.hot;
170
206
  var hotStatus = hasHot ? module.hot.status() : "n/a";
@@ -186,6 +222,11 @@ function maybeCheck(hash) {
186
222
  runCheck(hash);
187
223
  }
188
224
 
225
+ /**
226
+ * Handles one dev-server message (`hash`, `ok`, `warnings`, `errors`).
227
+ * @param {string} rawMessage - The raw JSON message text; invalid JSON is ignored with a warning.
228
+ * @returns {void}
229
+ */
189
230
  function handleMessage(rawMessage) {
190
231
  var message;
191
232
  try {
@@ -224,6 +265,11 @@ function handleMessage(rawMessage) {
224
265
  }
225
266
  }
226
267
 
268
+ /**
269
+ * Opens the dev-server WebSocket and reconnects with exponential backoff (up to 10 retries).
270
+ * @param {number} [retries=0] - The number of reconnect attempts made so far.
271
+ * @returns {void}
272
+ */
227
273
  function connect(retries) {
228
274
  if (retries === undefined) retries = 0;
229
275
  console.info(MRL + ":1 ws-connecting", JSON.stringify({ url: socketURL(options), retry: retries }));
@@ -7,4 +7,7 @@
7
7
  // (order "post", so it wins over the alias @lynx-js/rsbuild-plugin
8
8
  // registers) so `module.hot` stays live without a competing channel. Same
9
9
  // fix as mithril-lynx v1's F2 (src/dev-transport-noop.js).
10
+ /**
11
+ * No-op replacement for the official dev transport client.
12
+ */
10
13
  export default class DevTransportClientNoop {}