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.
- package/CHANGELOG.md +50 -0
- package/FETCH_INVESTIGATION.md +3 -2
- package/INPUT.md +115 -0
- package/README.md +47 -8
- package/ROUTE.md +111 -16
- package/ROUTE_CONTRACT_ANALYSIS.md +224 -0
- package/UPDATE_ANIMATION_GAP.md +133 -0
- package/package.json +1 -9
- package/plugin.js +33 -0
- package/src/apply-patch.js +369 -60
- package/src/backends/virtual-backend.js +120 -22
- package/src/background.d.ts +26 -0
- package/src/background.js +116 -7
- package/src/channel.js +22 -5
- 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 +18 -1
- package/src/mount-redraw.js +14 -1
- package/src/patch-protocol.js +41 -16
- package/src/reload/version.js +10 -1
- package/src/request.js +38 -0
- package/src/route.d.ts +30 -2
- package/src/route.js +214 -56
- package/src/testing.d.ts +14 -1
- package/src/testing.js +56 -1
- 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 +417 -113
- package/test/remove-subtree-cleanup.test.ts +75 -0
- package/test/route-hot-reload.test.ts +2 -1
- package/test/route.test.ts +201 -4
- 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_f2f02336cffeO0ssvuxqZ05iZq.json +0 -10
- package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.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 -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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|
package/src/background.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
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) `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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) {
|
package/src/dev-reload-client.js
CHANGED
|
@@ -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 {}
|