mithril-lynx 0.0.8 → 2.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/.omo/plans/m-request-fetch-lynx.md +306 -0
- package/.omo/plans/m-route-en-memoria.md +397 -0
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
- package/FETCH_INVESTIGATION.md +307 -0
- package/README.md +32 -284
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +4 -27
- package/plugin.js +142 -359
- package/rstest.config.ts +27 -0
- package/src/apply-patch.js +179 -0
- package/src/backends/virtual-backend.js +80 -0
- package/src/background.d.ts +11 -0
- package/src/background.js +79 -0
- package/src/channel.js +41 -0
- package/src/commit.js +67 -0
- package/src/dev-reload-client.js +245 -0
- package/src/dev-transport-noop.js +10 -0
- package/src/fake-dom.js +374 -0
- package/src/main-thread.d.ts +1 -0
- package/src/main-thread.js +68 -0
- package/src/mount-redraw.js +67 -0
- package/src/patch-protocol.js +40 -0
- package/src/reload/version.js +28 -0
- package/src/request.d.ts +37 -0
- package/src/request.js +181 -0
- package/src/route.d.ts +33 -0
- package/src/route.js +207 -0
- package/test/end-to-end.test.ts +86 -0
- package/test/reload-version.test.ts +17 -0
- package/test/request.test.ts +182 -0
- package/test/route-hot-reload.test.ts +40 -0
- package/test/route.test.ts +152 -0
- package/test/setup.ts +25 -0
- package/test/structural-reload.test.ts +95 -0
- package/CONTRACT.md +0 -151
- package/LICENSE +0 -21
- package/background.d.ts +0 -54
- package/background.js +0 -169
- package/element.d.ts +0 -34
- package/element.js +0 -83
- package/gesture.d.ts +0 -40
- package/gesture.js +0 -117
- package/internal/constants.js +0 -26
- package/internal/virtual-node.js +0 -388
- package/list.d.ts +0 -31
- package/list.js +0 -185
- package/main-thread.d.ts +0 -43
- package/main-thread.js +0 -165
- package/navigation.d.ts +0 -35
- package/navigation.js +0 -76
- package/renderer/background.d.ts +0 -21
- package/renderer/background.js +0 -84
- package/renderer/main-thread.d.ts +0 -12
- package/renderer/main-thread.js +0 -175
- package/src/lynx-mithril-shim.d.ts +0 -16
- package/src/lynx-mithril-shim.js +0 -1505
- package/src/worklet-runtime.js +0 -82
- package/testing.d.ts +0 -10
- package/testing.js +0 -91
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/apply-patch.js
|
|
2
|
+
//
|
|
3
|
+
// The main-thread half of the patch protocol. Deliberately NOT a DOM — it
|
|
4
|
+
// never runs Mithril's render.js (only the background thread does, see
|
|
5
|
+
// background.js) — it is a direct, low-level interpreter of the flat op
|
|
6
|
+
// array straight onto the real Element PAPI, in the spirit of ReactLynx's
|
|
7
|
+
// own `snapshotPatchApply.js` (see rspeedy-react-analysis/LYNX_PAPI_SPEC.md
|
|
8
|
+
// §4.3): a switch over op codes, one real PAPI call per case, nothing else.
|
|
9
|
+
//
|
|
10
|
+
// The exact `__Create*`/pageId contract below (one `pageId` shared by every
|
|
11
|
+
// element on a page, `__CreateView`/`__CreateText`/generic `__CreateElement`
|
|
12
|
+
// for tags, `__CreateRawText` + `__SetAttribute(id,"text",v)` for raw text
|
|
13
|
+
// content) is not a guess — it's the same contract `mithril-lynx/CONTRACT.md`
|
|
14
|
+
// + `mithril-lynx/src/lynx-mithril-shim.js` already validated on a real
|
|
15
|
+
// device (see mithril-lynx/DEVICE_VERIFICATION.md). Reusing a validated
|
|
16
|
+
// mapping here is exactly the kind of "concept, not code" reuse the v2 plan
|
|
17
|
+
// allows (§2 non-goals) — the bug we're rewriting away lives in the
|
|
18
|
+
// commit/reload layer (commit.js, reload/*.js), never in this mapping.
|
|
19
|
+
|
|
20
|
+
import { Op } from "./patch-protocol.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {number} pageId - `__GetElementUniqueID(pageElement)` of the real
|
|
24
|
+
* page this applier is attached to. Every element this applier creates
|
|
25
|
+
* belongs to that one page — see CONTRACT.md / lynx-mithril-shim.js.
|
|
26
|
+
*/
|
|
27
|
+
export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
28
|
+
// id (as allocated by the background's virtual backend) -> real PAPI
|
|
29
|
+
// element handle. id 0 is reserved for "the page itself" (see
|
|
30
|
+
// fake-dom.js's LynxDocument) — pre-seeded here so the very first
|
|
31
|
+
// InsertBefore/AppendChild targeting id 0 has somewhere real to land.
|
|
32
|
+
const handles = new Map();
|
|
33
|
+
|
|
34
|
+
function registerPageRoot(pageElementHandle) {
|
|
35
|
+
handles.set(0, pageElementHandle);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createElementHandle(tag) {
|
|
39
|
+
if (tag === "view") return __CreateView(pageId);
|
|
40
|
+
if (tag === "text") return __CreateText(pageId);
|
|
41
|
+
return __CreateElement(tag, pageId, {});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Applies one commit's worth of ops, then flushes exactly once —
|
|
46
|
+
* `__FlushElementTree` is the real commit; nothing before it is visible.
|
|
47
|
+
* This function itself is the ONLY caller of `__FlushElementTree` on
|
|
48
|
+
* this applier's page — never called conditionally, never looked up
|
|
49
|
+
* through a global (mirrors the fix in commit.js on the background
|
|
50
|
+
* side: one explicit call site, not an implicit one).
|
|
51
|
+
*/
|
|
52
|
+
function applyPatch(ops) {
|
|
53
|
+
for (let i = 0; i < ops.length; ) {
|
|
54
|
+
const opcode = ops[i++];
|
|
55
|
+
switch (opcode) {
|
|
56
|
+
case Op.CreateElement: {
|
|
57
|
+
const tag = ops[i++];
|
|
58
|
+
const id = ops[i++];
|
|
59
|
+
handles.set(id, createElementHandle(tag));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case Op.CreateElementNS: {
|
|
63
|
+
// Lynx has no XML-namespaced element PAPI distinct from
|
|
64
|
+
// the generic one — CONTRACT.md's createElementNS exists
|
|
65
|
+
// only to satisfy render.js's SVG/MathML path, which
|
|
66
|
+
// mithril-lynx apps don't exercise (no SVG on Lynx).
|
|
67
|
+
const _ns = ops[i++];
|
|
68
|
+
const tag = ops[i++];
|
|
69
|
+
const id = ops[i++];
|
|
70
|
+
handles.set(id, createElementHandle(tag));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case Op.CreateText: {
|
|
74
|
+
const value = ops[i++];
|
|
75
|
+
const id = ops[i++];
|
|
76
|
+
handles.set(id, __CreateRawText(String(value)));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case Op.InsertBefore: {
|
|
80
|
+
const parentId = ops[i++];
|
|
81
|
+
const childId = ops[i++];
|
|
82
|
+
const refId = ops[i++];
|
|
83
|
+
const parent = handles.get(parentId);
|
|
84
|
+
const child = handles.get(childId);
|
|
85
|
+
if (refId === -1) {
|
|
86
|
+
__AppendElement(parent, child);
|
|
87
|
+
} else {
|
|
88
|
+
__InsertElementBefore(parent, child, handles.get(refId));
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case Op.RemoveChild: {
|
|
93
|
+
const parentId = ops[i++];
|
|
94
|
+
const childId = ops[i++];
|
|
95
|
+
__RemoveElement(handles.get(parentId), handles.get(childId));
|
|
96
|
+
handles.delete(childId);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case Op.SetAttribute: {
|
|
100
|
+
const id = ops[i++];
|
|
101
|
+
const name = ops[i++];
|
|
102
|
+
const value = ops[i++];
|
|
103
|
+
const handle = handles.get(id);
|
|
104
|
+
if (name === "class") __SetClasses(handle, value == null ? "" : value);
|
|
105
|
+
else __SetAttribute(handle, name, value);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case Op.RemoveAttribute: {
|
|
109
|
+
const id = ops[i++];
|
|
110
|
+
const name = ops[i++];
|
|
111
|
+
const handle = handles.get(id);
|
|
112
|
+
if (name === "class") __SetClasses(handle, "");
|
|
113
|
+
else __SetAttribute(handle, name, null);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case Op.SetAttributeNS: {
|
|
117
|
+
const id = ops[i++];
|
|
118
|
+
const _ns = ops[i++];
|
|
119
|
+
const name = ops[i++];
|
|
120
|
+
const value = ops[i++];
|
|
121
|
+
__SetAttribute(handles.get(id), name, value);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case Op.SetStyleProperty: {
|
|
125
|
+
const id = ops[i++];
|
|
126
|
+
const name = ops[i++];
|
|
127
|
+
const value = ops[i++];
|
|
128
|
+
__AddInlineStyle(handles.get(id), name, value);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case Op.RemoveStyleProperty: {
|
|
132
|
+
const id = ops[i++];
|
|
133
|
+
const name = ops[i++];
|
|
134
|
+
// `"*"` is fake-dom.js's encoding of `element.style = ""`
|
|
135
|
+
// (clear everything) — there is no bulk-clear PAPI call
|
|
136
|
+
// validated yet, so this case is a documented gap for
|
|
137
|
+
// F3, not a silent no-op: it throws so the gap surfaces
|
|
138
|
+
// as a test failure rather than a mystery on-device.
|
|
139
|
+
if (name === "*") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"[mithril-lynx-v2] Clearing the whole `style` object at once is not implemented yet (F3 TODO) — set individual properties to \"\" instead.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
__AddInlineStyle(handles.get(id), name, "");
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case Op.SetText: {
|
|
148
|
+
const id = ops[i++];
|
|
149
|
+
const value = ops[i++];
|
|
150
|
+
__SetAttribute(handles.get(id), "text", value);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case Op.AddEvent: {
|
|
154
|
+
const id = ops[i++];
|
|
155
|
+
const type = ops[i++];
|
|
156
|
+
const handle = handles.get(id);
|
|
157
|
+
__AddEventListener(handle, type, (nativeEvent) => {
|
|
158
|
+
onEvent?.(id, type, nativeEvent);
|
|
159
|
+
}, {});
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case Op.RemoveEvent: {
|
|
163
|
+
// PAPI has no documented `__RemoveEventListener` in the
|
|
164
|
+
// validated v1 surface (CONTRACT.md never needed it,
|
|
165
|
+
// since mithril-lynx v1 never tore down individual
|
|
166
|
+
// listeners outside of removing the whole element).
|
|
167
|
+
// Left as an explicit no-op + TODO rather than a guess.
|
|
168
|
+
i += 2;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
throw new Error(`[mithril-lynx-v2] Unknown patch opcode: ${opcode}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
__FlushElementTree();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { registerPageRoot, applyPatch };
|
|
179
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/backends/virtual-backend.js
|
|
2
|
+
//
|
|
3
|
+
// The background-thread half of the patch protocol: `fake-dom.js` calls
|
|
4
|
+
// these methods instead of touching real elements, and every call appends
|
|
5
|
+
// one op to a flat array (see ../patch-protocol.js for the op format and
|
|
6
|
+
// why it's flat). `takeOps()` is called once per commit (see ../commit.js)
|
|
7
|
+
// to drain the array for sending across the thread boundary.
|
|
8
|
+
|
|
9
|
+
import { Op, pushOp } from "../patch-protocol.js";
|
|
10
|
+
|
|
11
|
+
export function createVirtualBackend() {
|
|
12
|
+
let nextId = 1;
|
|
13
|
+
let ops = [];
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
// Exposed for tests that want to assert on id allocation directly;
|
|
17
|
+
// application code should never need it.
|
|
18
|
+
allocId() {
|
|
19
|
+
return nextId++;
|
|
20
|
+
},
|
|
21
|
+
createElement(tag) {
|
|
22
|
+
const id = nextId++;
|
|
23
|
+
pushOp(ops, Op.CreateElement, tag, id);
|
|
24
|
+
return id;
|
|
25
|
+
},
|
|
26
|
+
createElementNS(ns, tag) {
|
|
27
|
+
const id = nextId++;
|
|
28
|
+
pushOp(ops, Op.CreateElementNS, ns, tag, id);
|
|
29
|
+
return id;
|
|
30
|
+
},
|
|
31
|
+
createText(text) {
|
|
32
|
+
const id = nextId++;
|
|
33
|
+
pushOp(ops, Op.CreateText, text, id);
|
|
34
|
+
return id;
|
|
35
|
+
},
|
|
36
|
+
insertBefore(parentId, childId, refId) {
|
|
37
|
+
pushOp(ops, Op.InsertBefore, parentId, childId, refId ?? -1);
|
|
38
|
+
},
|
|
39
|
+
removeChild(parentId, childId) {
|
|
40
|
+
pushOp(ops, Op.RemoveChild, parentId, childId);
|
|
41
|
+
},
|
|
42
|
+
setAttribute(id, name, value) {
|
|
43
|
+
pushOp(ops, Op.SetAttribute, id, name, value);
|
|
44
|
+
},
|
|
45
|
+
removeAttribute(id, name) {
|
|
46
|
+
pushOp(ops, Op.RemoveAttribute, id, name);
|
|
47
|
+
},
|
|
48
|
+
setAttributeNS(id, ns, name, value) {
|
|
49
|
+
pushOp(ops, Op.SetAttributeNS, id, ns, name, value);
|
|
50
|
+
},
|
|
51
|
+
setClasses(id, value) {
|
|
52
|
+
// Encoded as a SetAttribute on the synthetic "class" name — the
|
|
53
|
+
// patch applier special-cases that name into `__SetClasses`, the
|
|
54
|
+
// same split fake-dom.js already does on the way in.
|
|
55
|
+
pushOp(ops, Op.SetAttribute, id, "class", value);
|
|
56
|
+
},
|
|
57
|
+
setStyleProperty(id, name, value) {
|
|
58
|
+
pushOp(ops, Op.SetStyleProperty, id, name, value);
|
|
59
|
+
},
|
|
60
|
+
removeStyleProperty(id, name) {
|
|
61
|
+
pushOp(ops, Op.RemoveStyleProperty, id, name);
|
|
62
|
+
},
|
|
63
|
+
setText(id, value) {
|
|
64
|
+
pushOp(ops, Op.SetText, id, value);
|
|
65
|
+
},
|
|
66
|
+
addEvent(id, type) {
|
|
67
|
+
pushOp(ops, Op.AddEvent, id, type);
|
|
68
|
+
},
|
|
69
|
+
removeEvent(id, type) {
|
|
70
|
+
pushOp(ops, Op.RemoveEvent, id, type);
|
|
71
|
+
},
|
|
72
|
+
/** Drains and returns the accumulated ops. Called once per commit. */
|
|
73
|
+
takeOps() {
|
|
74
|
+
if (ops.length === 0) return null;
|
|
75
|
+
const out = ops;
|
|
76
|
+
ops = [];
|
|
77
|
+
return out;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RenderAppOptions {
|
|
2
|
+
root: () => unknown;
|
|
3
|
+
sendPatch?: (ops: unknown[]) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RenderAppHandle {
|
|
7
|
+
redraw: () => void;
|
|
8
|
+
document: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function renderApp(options: RenderAppOptions): RenderAppHandle;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/background.js
|
|
2
|
+
//
|
|
3
|
+
// Entry point for the background (JS) thread — the ONLY thread that ever
|
|
4
|
+
// runs a component's view() or Mithril's diff (architecture decision
|
|
5
|
+
// §3.1 of the plan: one thread model, not three like v1 had).
|
|
6
|
+
//
|
|
7
|
+
// The redraw contract, precisely: `render(dom, vnodes, redraw)`'s third
|
|
8
|
+
// argument is what real Mithril calls automatically after any event
|
|
9
|
+
// (CONTRACT.md §e, `EventDict.handleEvent`) — that is the ENTIRE mechanism
|
|
10
|
+
// by which "a tap handler that mutates state repaints the screen" works,
|
|
11
|
+
// with no cooperation from app code. v1's bug was that the function on the
|
|
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
|
|
14
|
+
// same reference, never looked up through a global.
|
|
15
|
+
|
|
16
|
+
import renderFactory from "mithril-runtime/render/render.js";
|
|
17
|
+
import { createLynxDocument } from "./fake-dom.js";
|
|
18
|
+
import { createVirtualBackend } from "./backends/virtual-backend.js";
|
|
19
|
+
import { createCommitController } from "./commit.js";
|
|
20
|
+
import { onEventFromMainThread, sendPatchToMainThread } from "./channel.js";
|
|
21
|
+
import { register as registerRedraw } from "./mount-redraw.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} options
|
|
25
|
+
* @param {() => unknown} options.root - Returns the current top-level vnode
|
|
26
|
+
* (a fresh hyperscript tree). Called on every render pass, including the
|
|
27
|
+
* very first — there is no separate "mount" vnode.
|
|
28
|
+
* @param {(ops: unknown[]) => void} [options.sendPatch] - Ships one commit's
|
|
29
|
+
* worth of ops across the thread boundary. Defaults to the real channel
|
|
30
|
+
* (channel.js, F0.1's decision) — tests override it to capture ops
|
|
31
|
+
* in-process instead.
|
|
32
|
+
*/
|
|
33
|
+
export function renderApp({ root, sendPatch = sendPatchToMainThread }) {
|
|
34
|
+
const backend = createVirtualBackend();
|
|
35
|
+
const document = createLynxDocument(backend);
|
|
36
|
+
const render = renderFactory();
|
|
37
|
+
const commitController = createCommitController();
|
|
38
|
+
|
|
39
|
+
function flush() {
|
|
40
|
+
const ops = backend.takeOps();
|
|
41
|
+
if (ops) sendPatch(ops);
|
|
42
|
+
}
|
|
43
|
+
commitController.install(flush);
|
|
44
|
+
|
|
45
|
+
// This is Mithril's actual redraw service pattern (the same shape as
|
|
46
|
+
// upstream `mithril/api/mount-redraw.js`'s internal `run()`): the
|
|
47
|
+
// "redraw" callback IS "call render() again", not "send a patch"
|
|
48
|
+
// directly — flushing is a side effect of every render pass completing,
|
|
49
|
+
// whether that pass was the first one, an auto-redraw after an event, a
|
|
50
|
+
// manual `redraw()` call, or a hot-update re-render (see reload/*.js).
|
|
51
|
+
function performRender() {
|
|
52
|
+
render(document, root(), performRender);
|
|
53
|
+
commitController.commit();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
performRender();
|
|
57
|
+
registerRedraw(performRender);
|
|
58
|
+
|
|
59
|
+
// Wires every forwarded native event straight to the fake-dom node it
|
|
60
|
+
// targets — `dispatchEvent` (fake-dom.js) then invokes Mithril's own
|
|
61
|
+
// EventDict exactly as a real DOM would, and (per the contract at the
|
|
62
|
+
// top of this file) `performRender` auto-fires afterward if the
|
|
63
|
+
// handler doesn't opt out. This is the ONLY consumer of
|
|
64
|
+
// `onEventFromMainThread` — app code never touches the channel directly.
|
|
65
|
+
onEventFromMainThread((event) => {
|
|
66
|
+
const { id, type, payload } = event.data;
|
|
67
|
+
const node = document.getNodeById(id);
|
|
68
|
+
if (!node) return;
|
|
69
|
+
node.dispatchEvent({ type, currentTarget: node, preventDefault() {}, stopPropagation() {}, ...payload });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
redraw: performRender,
|
|
74
|
+
/** Exposed for tests and for an app's `module.hot.accept` glue (see
|
|
75
|
+
* the demo app's background.ts) — never needed by the channel
|
|
76
|
+
* wiring above, which is already fully self-contained. */
|
|
77
|
+
document,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/channel.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/channel.js
|
|
2
|
+
//
|
|
3
|
+
// The cross-thread transport (F0.1's decision, plan §3.2). Uses the exact
|
|
4
|
+
// same mechanism mithril-lynx v1's already-on-device-verified renderer mode
|
|
5
|
+
// used (`lynx.getCoreContext()`/`lynx.getJSContext()` event pub/sub with a
|
|
6
|
+
// namespaced event name) rather than the untested `lynx.triggerLepusGlobalEvent`
|
|
7
|
+
// candidate F0.1 turned up — that candidate is real and worth trying in a
|
|
8
|
+
// later iteration (see the plan's §8 F0.1 note), but shipping a first real
|
|
9
|
+
// device build on a validated channel was the priority for F3, not
|
|
10
|
+
// re-verifying the channel choice from scratch.
|
|
11
|
+
//
|
|
12
|
+
// Naming note: `getCoreContext()` (called from the background thread) and
|
|
13
|
+
// `getJSContext()` (called from the main thread) are two different global
|
|
14
|
+
// accessors for the SAME underlying native pub/sub context — confirmed by
|
|
15
|
+
// v1's renderer/background.js and renderer/main-thread.js dispatching to
|
|
16
|
+
// and listening on each other via exactly this pairing.
|
|
17
|
+
|
|
18
|
+
export const patchEventName = "MithrilLynxV2:Patch";
|
|
19
|
+
export const eventFromMainThreadEventName = "MithrilLynxV2:Event";
|
|
20
|
+
export const renderPageEventName = "__RenderPage";
|
|
21
|
+
export const destroyLifetimeEventName = "__DestroyLifetime";
|
|
22
|
+
|
|
23
|
+
/** Background thread: ship one commit's ops to the main thread. */
|
|
24
|
+
export function sendPatchToMainThread(ops) {
|
|
25
|
+
lynx.getCoreContext().dispatchEvent({ type: patchEventName, data: ops });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Background thread: receive a forwarded native event `{ id, type, payload }`. */
|
|
29
|
+
export function onEventFromMainThread(handler) {
|
|
30
|
+
lynx.getCoreContext().addEventListener(eventFromMainThreadEventName, handler);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Main thread: receive a patch (an ops array) from the background thread. */
|
|
34
|
+
export function onPatchFromBackground(handler) {
|
|
35
|
+
lynx.getJSContext().addEventListener(patchEventName, handler);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Main thread: forward a native event on element `id` back to the background thread. */
|
|
39
|
+
export function sendEventToBackground(id, type, payload) {
|
|
40
|
+
lynx.getJSContext().dispatchEvent({ type: eventFromMainThreadEventName, data: { id, type, payload } });
|
|
41
|
+
}
|
package/src/commit.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/commit.js
|
|
2
|
+
//
|
|
3
|
+
// The single, explicit, non-conditional flush contract — this is the actual
|
|
4
|
+
// fix for the regression that motivated the whole v2 rewrite (see
|
|
5
|
+
// mithril-lynx-v2-desde-cero.md §3.4 and mithril-lynx/AGENTS.md's "Estado
|
|
6
|
+
// actual" section for the v1 postmortem).
|
|
7
|
+
//
|
|
8
|
+
// v1's bug in one sentence: whether a redraw actually reached the main
|
|
9
|
+
// thread depended on `typeof globalThis.__FlushElementTree === "function"`
|
|
10
|
+
// — a question whose answer depended on thread/test/mode ordering. That is
|
|
11
|
+
// a race condition baked into the architecture, not an edge case to patch.
|
|
12
|
+
//
|
|
13
|
+
// v2's rule: there is exactly one commit callback for the lifetime of one
|
|
14
|
+
// `renderApp()` call (see background.js). It is installed explicitly, once,
|
|
15
|
+
// by the code that owns the render — never discovered implicitly by
|
|
16
|
+
// whoever happens to ask first. Asking to commit before installing one is a
|
|
17
|
+
// programmer error and throws immediately and loudly, on the same tick,
|
|
18
|
+
// with a message that says exactly what's missing — never a silently
|
|
19
|
+
// frozen screen (which is what v1 did instead).
|
|
20
|
+
|
|
21
|
+
const NOT_MOUNTED = Symbol("mithril-lynx-v2:not-mounted");
|
|
22
|
+
|
|
23
|
+
export function createCommitController() {
|
|
24
|
+
let commitFn = NOT_MOUNTED;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
/**
|
|
28
|
+
* Registers the one and only commit callback. Called exactly once by
|
|
29
|
+
* `renderApp()` (background.js), before Mithril's `render()` is ever
|
|
30
|
+
* invoked — so nothing can observe the "not mounted yet" state from
|
|
31
|
+
* inside a redraw.
|
|
32
|
+
*/
|
|
33
|
+
install(fn) {
|
|
34
|
+
if (commitFn !== NOT_MOUNTED) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"[mithril-lynx-v2] commit callback already installed. " +
|
|
37
|
+
"A shim instance is single-use: one renderApp() call, one " +
|
|
38
|
+
"commit callback, for the lifetime of that background " +
|
|
39
|
+
"context. If you're re-mounting for a reload, create a new " +
|
|
40
|
+
"commit controller instead of reusing this one.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
commitFn = fn;
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* The ONLY path anything (an event, `m.redraw()`, an HMR apply) uses
|
|
47
|
+
* to signal "a render pass just happened, push it". Passed as the
|
|
48
|
+
* `redraw` argument to every call of Mithril's real `render()` — see
|
|
49
|
+
* background.js. Mithril's own `EventDict.handleEvent` (render.js,
|
|
50
|
+
* documented in CONTRACT.md §e) already calls this automatically
|
|
51
|
+
* after any event handler runs, with no cooperation required from
|
|
52
|
+
* app code — that automatic call is what makes redraw "just work"
|
|
53
|
+
* like it does in React/Preact.
|
|
54
|
+
*/
|
|
55
|
+
commit() {
|
|
56
|
+
if (commitFn === NOT_MOUNTED) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"[mithril-lynx-v2] commit() called before renderApp() mounted " +
|
|
59
|
+
"the app. This is always a bug in the framework's own " +
|
|
60
|
+
"wiring, never something app code can trigger by accident " +
|
|
61
|
+
"— app code never calls commit() directly.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
commitFn();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|