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
package/list.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// list.js
|
|
2
|
-
//
|
|
3
|
-
// List virtualization/recycling (project plan, Phase 8, Tier 2), main-thread
|
|
4
|
-
// only. __CreateList's componentAtIndex/enqueueComponent callbacks are a
|
|
5
|
-
// native-driven recycler: native owns scroll position and cell lifecycle,
|
|
6
|
-
// calling back into JS to fetch or recycle a cell's content by TYPE
|
|
7
|
-
// (matching RecyclerView/UICollectionView semantics), not a JS-side
|
|
8
|
-
// windowing calculation.
|
|
9
|
-
//
|
|
10
|
-
// The sign/recycle map design and the exact __FlushElementTree({triggerLayout,
|
|
11
|
-
// operationID, elementID, listID}) call shape are ported from
|
|
12
|
-
// @lynx-js/react's OWN shipped runtime/lib/snapshot/list/list.js — real,
|
|
13
|
-
// proven code, not guesswork (unlike gesture.js's callback-shape question,
|
|
14
|
-
// this part IS verified against a real implementation). What list.js narrows
|
|
15
|
-
// down for v1, deliberately:
|
|
16
|
-
// - No deferred list items (ReactLynx's `defer`/`isReady` promise dance —
|
|
17
|
-
// ties into a whole separate lifecycle-event protocol not otherwise
|
|
18
|
-
// needed here).
|
|
19
|
-
// - No componentAtIndexes batching — native's single-cell componentAtIndex
|
|
20
|
-
// callback only.
|
|
21
|
-
// - No independent per-item redraw after the initial bind/recycle — a list
|
|
22
|
-
// item's content is (re)computed fresh from renderItem(index) every time
|
|
23
|
-
// native calls componentAtIndex for it (on scroll-driven reuse), not
|
|
24
|
-
// whenever the app's own state changes. An app wanting a currently-bound
|
|
25
|
-
// visible item to reflect a data change needs its OWN mechanism to ask
|
|
26
|
-
// native to re-request that cell; list.js does not provide one in v1.
|
|
27
|
-
//
|
|
28
|
-
// This is an imperative escape hatch, like element.js/gesture.js — call it
|
|
29
|
-
// from oncreate(vnode) and attach the result yourself
|
|
30
|
-
// (parentNode.appendChild(list)), rather than mounting it through m().
|
|
31
|
-
// Tier 1 (no code here at all — just m("list", ...)/m("list-item", ...) as
|
|
32
|
-
// ordinary tags through the existing shim, relying on its already-tested
|
|
33
|
-
// keyed/LIS diff) covers the common case and should be preferred unless the
|
|
34
|
-
// app specifically needs native-driven recycling for very large lists.
|
|
35
|
-
|
|
36
|
-
import shim from "./src/lynx-mithril-shim.js";
|
|
37
|
-
|
|
38
|
-
// A render function dedicated to list-item content, independent of
|
|
39
|
-
// shim.render()/redraw()'s own single-root convenience-API state (which a
|
|
40
|
-
// main-thread-owned or data-channel-mode app may already be using for the
|
|
41
|
-
// rest of the page). Each item wrapper carries its own `.vnodes`, so reusing
|
|
42
|
-
// one render function sequentially across many items/lists is safe — Mithril
|
|
43
|
-
// diffs against whatever `.vnodes` is already on the target wrapper.
|
|
44
|
-
const renderItemContent = shim();
|
|
45
|
-
|
|
46
|
-
function typeKeyOf(vnode) {
|
|
47
|
-
return typeof vnode.tag === "string" ? vnode.tag : (vnode.tag && vnode.tag.name) || "default";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Creates a native-recycled `<list>` element, scoped to the same component
|
|
52
|
-
* as `parentNode` (anything with an `ownerDocument`, e.g. a real
|
|
53
|
-
* LynxNodeWrapper). `renderItem(index)` must return a fresh Mithril vnode
|
|
54
|
-
* for that cell's content every time it's called — it may be called more
|
|
55
|
-
* than once for the same index (recycling).
|
|
56
|
-
*
|
|
57
|
-
* `scrollOrientation`/`listType`/`spanCount` and each item's `item-key` are
|
|
58
|
-
* NOT optional in practice, even though native's __CreateList/componentAtIndex
|
|
59
|
-
* types don't require them: every real @lynx-js/react list example sets all
|
|
60
|
-
* of these unconditionally (verified by reading lynx-examples/examples/list's
|
|
61
|
-
* base/recyclable/async-rendering demos — see ../LIST_INVESTIGATION.md), and
|
|
62
|
-
* a real device never called componentAtIndex at all without them. Defaults
|
|
63
|
-
* here match what those examples use for the simple single-column case.
|
|
64
|
-
*/
|
|
65
|
-
export function createList(parentNode, options) {
|
|
66
|
-
const {
|
|
67
|
-
itemCount,
|
|
68
|
-
renderItem,
|
|
69
|
-
itemKey,
|
|
70
|
-
className,
|
|
71
|
-
scrollOrientation = "vertical",
|
|
72
|
-
listType = "single",
|
|
73
|
-
spanCount = 1,
|
|
74
|
-
} = options;
|
|
75
|
-
let count = itemCount;
|
|
76
|
-
const pageId = parentNode.ownerDocument._pageId;
|
|
77
|
-
const keyOf = typeof itemKey === "function" ? itemKey : (index) => String(index);
|
|
78
|
-
|
|
79
|
-
// itemType -> Map<sign, ItemEntry>, the reuse pool.
|
|
80
|
-
const recycleMap = new Map();
|
|
81
|
-
// sign -> ItemEntry, currently-bound (visible) items.
|
|
82
|
-
const signMap = new Map();
|
|
83
|
-
|
|
84
|
-
function bindFreshItem(listHandle, listId, cellIndex, opId, vnode, typeKey) {
|
|
85
|
-
const itemWrapper = parentNode.ownerDocument.createElement("list-item");
|
|
86
|
-
__SetAttribute(itemWrapper._handle, "item-key", keyOf(cellIndex));
|
|
87
|
-
__AppendElement(listHandle, itemWrapper._handle);
|
|
88
|
-
renderItemContent(itemWrapper, vnode);
|
|
89
|
-
|
|
90
|
-
const sign = __GetElementUniqueID(itemWrapper._handle);
|
|
91
|
-
signMap.set(sign, { wrapper: itemWrapper, typeKey });
|
|
92
|
-
__FlushElementTree(itemWrapper._handle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
|
|
93
|
-
return sign;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function bindRecycledItem(listId, cellIndex, opId, vnode, pool) {
|
|
97
|
-
const [sign, entry] = pool.entries().next().value;
|
|
98
|
-
pool.delete(sign);
|
|
99
|
-
// The recycled wrapper is now bound to a DIFFERENT index — its
|
|
100
|
-
// item-key must be updated to match, the same way its content does.
|
|
101
|
-
__SetAttribute(entry.wrapper._handle, "item-key", keyOf(cellIndex));
|
|
102
|
-
renderItemContent(entry.wrapper, vnode);
|
|
103
|
-
signMap.set(sign, entry);
|
|
104
|
-
__FlushElementTree(entry.wrapper._handle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
|
|
105
|
-
return sign;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function componentAtIndex(listHandle, listId, cellIndex, opId) {
|
|
109
|
-
if (cellIndex < 0 || cellIndex >= count) {
|
|
110
|
-
throw new Error(`mithril-lynx list: cellIndex ${cellIndex} out of range (itemCount=${count})`);
|
|
111
|
-
}
|
|
112
|
-
const vnode = renderItem(cellIndex);
|
|
113
|
-
const typeKey = typeKeyOf(vnode);
|
|
114
|
-
const pool = recycleMap.get(typeKey);
|
|
115
|
-
if (pool && pool.size > 0) return bindRecycledItem(listId, cellIndex, opId, vnode, pool);
|
|
116
|
-
return bindFreshItem(listHandle, listId, cellIndex, opId, vnode, typeKey);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function enqueueComponent(_listHandle, _listId, sign) {
|
|
120
|
-
const entry = signMap.get(sign);
|
|
121
|
-
if (entry == null) return;
|
|
122
|
-
signMap.delete(sign);
|
|
123
|
-
if (!recycleMap.has(entry.typeKey)) recycleMap.set(entry.typeKey, new Map());
|
|
124
|
-
recycleMap.get(entry.typeKey).set(sign, entry);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// THE missing piece (see LIST_INVESTIGATION.md): __CreateList + attributes
|
|
128
|
-
// alone never triggers componentAtIndex — native only starts requesting
|
|
129
|
-
// cells once it's told, via a special "update-list-info" attribute, which
|
|
130
|
-
// positions currently exist. Ported from @lynx-js/react's OWN
|
|
131
|
-
// runtime/lib/snapshot/list/listUpdateInfo.js (ListUpdateInfoRecording.flush()),
|
|
132
|
-
// which sends this on every list-children change, alongside a matching
|
|
133
|
-
// __UpdateListCallbacks call. Each insertAction entry needs an "item-key"
|
|
134
|
-
// (dash-case — confirmed by a real device rejecting the first attempt,
|
|
135
|
-
// which omitted it, with "Error for illegal list item-key in parse
|
|
136
|
-
// insertAction"; the exact key name matches ReactLynx's own
|
|
137
|
-
// `__listItemPlatformInfo['item-key']`, a raw JSX-prop-shaped bag it
|
|
138
|
-
// spreads into each entry). `type` per entry is still UNCONFIRMED —
|
|
139
|
-
// ReactLynx's version uses its own component-type reference there
|
|
140
|
-
// (meaningful only to its own recycling, not to native, as far as this
|
|
141
|
-
// file's reading of the source can tell); a constant string is used
|
|
142
|
-
// here since this port's own type-based recycling (typeKeyOf(), above)
|
|
143
|
-
// is tracked independently, in this module, not through native's copy
|
|
144
|
-
// of `type`.
|
|
145
|
-
function sendListInfo(insertAction, removeAction, updateAction) {
|
|
146
|
-
__SetAttribute(listHandle, "update-list-info", { insertAction, removeAction, updateAction });
|
|
147
|
-
__UpdateListCallbacks(listHandle, componentAtIndex, enqueueComponent);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function insertEntry(position) {
|
|
151
|
-
return { position, type: "cell", "item-key": keyOf(position) };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const listHandle = __CreateList(pageId, componentAtIndex, enqueueComponent, {});
|
|
155
|
-
__SetAttribute(listHandle, "scroll-orientation", scrollOrientation);
|
|
156
|
-
__SetAttribute(listHandle, "list-type", listType);
|
|
157
|
-
__SetAttribute(listHandle, "span-count", String(spanCount));
|
|
158
|
-
if (className != null) __SetClasses(listHandle, className);
|
|
159
|
-
|
|
160
|
-
sendListInfo(
|
|
161
|
-
Array.from({ length: count }, (_, position) => insertEntry(position)),
|
|
162
|
-
[],
|
|
163
|
-
[],
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
_handle: listHandle,
|
|
168
|
-
nodeType: 1,
|
|
169
|
-
/** Call after mutating the underlying data so a subsequently-scrolled-into-view cell reflects the new count. */
|
|
170
|
-
setItemCount(nextCount) {
|
|
171
|
-
if (nextCount === count) return;
|
|
172
|
-
if (nextCount > count) {
|
|
173
|
-
sendListInfo(
|
|
174
|
-
Array.from({ length: nextCount - count }, (_, i) => insertEntry(count + i)),
|
|
175
|
-
[],
|
|
176
|
-
[],
|
|
177
|
-
);
|
|
178
|
-
} else {
|
|
179
|
-
const removed = Array.from({ length: count - nextCount }, (_, i) => nextCount + i);
|
|
180
|
-
sendListInfo([], removed, []);
|
|
181
|
-
}
|
|
182
|
-
count = nextCount;
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
package/main-thread.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Ambient declaration for the ESM main-thread.js (the file itself is not
|
|
2
|
-
// type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
-
|
|
4
|
-
/** Returns the most recently received data (from __RenderPage/__UpdatePage or a background push). */
|
|
5
|
-
export function getData<T = unknown>(): T | undefined;
|
|
6
|
-
|
|
7
|
-
/** Sends a named action to the background thread's setBackgroundEventHandler. */
|
|
8
|
-
export function dispatchToBackground(handlerName: string, data?: unknown): void;
|
|
9
|
-
|
|
10
|
-
export interface SetupAppOptions<TInput = unknown, TData = TInput> {
|
|
11
|
-
/** Called exactly once, on the first __RenderPage, to build the root Mithril vnode. */
|
|
12
|
-
root(): unknown;
|
|
13
|
-
/** Optional transform applied to raw engine/background data before storing it. */
|
|
14
|
-
processData?(data: TInput): TData;
|
|
15
|
-
/**
|
|
16
|
-
* When true (default), pushes processed data to the background thread on
|
|
17
|
-
* every __RenderPage/__UpdatePage, and listens for background-pushed
|
|
18
|
-
* updates, redrawing via Mithril's own shim.redraw().
|
|
19
|
-
*/
|
|
20
|
-
enableBackgroundSync?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Wires the Lynx engine's page lifecycle (__RenderPage/__UpdatePage/
|
|
25
|
-
* __DestroyLifetime) to a Mithril app: renders `root()` once, then calls
|
|
26
|
-
* the shim's redraw() on every subsequent data push. See the project plan,
|
|
27
|
-
* Phase 3 ("data-channel mode").
|
|
28
|
-
*/
|
|
29
|
-
export function setupApp<TInput = unknown, TData = TInput>(
|
|
30
|
-
options: SetupAppOptions<TInput, TData>,
|
|
31
|
-
): void;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Registers a handler background.js's runOnMainThread(key, ...args) can
|
|
35
|
-
* call by name. See the project plan, Phase 6.
|
|
36
|
-
*/
|
|
37
|
-
export function registerHandler(key: string, fn: (...args: unknown[]) => unknown): void;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Calls a handler background.js registered via registerHandler(key, fn).
|
|
41
|
-
* Args and the resolved value must be JSON-serializable.
|
|
42
|
-
*/
|
|
43
|
-
export function runOnBackground<T = unknown>(key: string, ...args: unknown[]): Promise<T>;
|
package/main-thread.js
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// main-thread.js
|
|
2
|
-
//
|
|
3
|
-
// Cross-thread "data-channel mode" adapter for the main thread (see the
|
|
4
|
-
// project plan, Phase 3). Ports the engine-lifecycle wiring from
|
|
5
|
-
// lynx-examples/examples/vanilla/src/common/main-thread/setup.ts, adapted so
|
|
6
|
-
// the app supplies a Mithril root component instead of hand-written
|
|
7
|
-
// renderPage/updatePage functions: Mithril's own redraw/diff machinery
|
|
8
|
-
// (already ported in ../src/lynx-mithril-shim.js) does the update work.
|
|
9
|
-
//
|
|
10
|
-
// The app is responsible for reading live data inside its Mithril view()
|
|
11
|
-
// functions via getData() — exactly like any plain-JS Mithril store, no
|
|
12
|
-
// special re-invocation of a "mount" function is needed on every update.
|
|
13
|
-
|
|
14
|
-
import shim from "./src/lynx-mithril-shim.js";
|
|
15
|
-
import {
|
|
16
|
-
callBackgroundEventName,
|
|
17
|
-
callBackgroundResultEventName,
|
|
18
|
-
callMainThreadEventName,
|
|
19
|
-
callMainThreadResultEventName,
|
|
20
|
-
destroyLifetimeEventName,
|
|
21
|
-
dispatchEventToBackgroundEventName,
|
|
22
|
-
renderPageEventName,
|
|
23
|
-
updateDataFromBackgroundEventName,
|
|
24
|
-
updateDataFromMainThreadEventName,
|
|
25
|
-
updatePageEventName,
|
|
26
|
-
} from "./internal/constants.js";
|
|
27
|
-
|
|
28
|
-
let latestData;
|
|
29
|
-
|
|
30
|
-
// The native engine unconditionally invokes a global `processData(initData)`
|
|
31
|
-
// hook on every __RenderPage/__UpdatePage, regardless of framework. Install a
|
|
32
|
-
// pass-through default immediately so apps that don't need to transform
|
|
33
|
-
// incoming data (the common case) don't have to think about this at all.
|
|
34
|
-
Object.assign(globalThis, {
|
|
35
|
-
processData: (data) => data,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export function getData() {
|
|
39
|
-
return latestData;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function dispatchToBackground(handlerName, data) {
|
|
43
|
-
lynx.getJSContext().dispatchEvent({
|
|
44
|
-
type: dispatchEventToBackgroundEventName,
|
|
45
|
-
data: { handlerName, data },
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function setupApp(options) {
|
|
50
|
-
const { root, processData, enableBackgroundSync = true } = options;
|
|
51
|
-
const engine = lynx.getEngine();
|
|
52
|
-
const background = enableBackgroundSync ? lynx.getJSContext() : undefined;
|
|
53
|
-
let rendered = false;
|
|
54
|
-
|
|
55
|
-
const applyData = (data) => {
|
|
56
|
-
latestData = typeof processData === "function" ? processData(data) : data;
|
|
57
|
-
if (enableBackgroundSync) {
|
|
58
|
-
lynx.getJSContext().dispatchEvent({
|
|
59
|
-
type: updateDataFromMainThreadEventName,
|
|
60
|
-
data: latestData,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
return latestData;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const onRenderPage = (event) => {
|
|
67
|
-
const [data] = event.data;
|
|
68
|
-
applyData(data);
|
|
69
|
-
const page = __CreatePage("0", 0);
|
|
70
|
-
shim.renderToPage(page, root());
|
|
71
|
-
rendered = true;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const onUpdatePage = (event) => {
|
|
75
|
-
const [data] = event.data;
|
|
76
|
-
applyData(data);
|
|
77
|
-
if (rendered) shim.redraw();
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const onDataFromBackground = (event) => {
|
|
81
|
-
latestData = { ...latestData, ...event.data };
|
|
82
|
-
if (rendered) shim.redraw();
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const onDestroyLifetime = () => {
|
|
86
|
-
if (enableBackgroundSync) {
|
|
87
|
-
// The background thread has no lynx.getEngine() of its own, so relay
|
|
88
|
-
// the native lifecycle event across the channel explicitly — this is
|
|
89
|
-
// what lets background.js's own setupBackground() cleanup run.
|
|
90
|
-
background.dispatchEvent({ type: destroyLifetimeEventName, data: undefined });
|
|
91
|
-
background.removeEventListener(updateDataFromBackgroundEventName, onDataFromBackground);
|
|
92
|
-
}
|
|
93
|
-
engine.removeEventListener(renderPageEventName, onRenderPage);
|
|
94
|
-
engine.removeEventListener(updatePageEventName, onUpdatePage);
|
|
95
|
-
engine.removeEventListener(destroyLifetimeEventName, onDestroyLifetime);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
engine.addEventListener(renderPageEventName, onRenderPage);
|
|
99
|
-
engine.addEventListener(updatePageEventName, onUpdatePage);
|
|
100
|
-
engine.addEventListener(destroyLifetimeEventName, onDestroyLifetime);
|
|
101
|
-
if (enableBackgroundSync) {
|
|
102
|
-
background.addEventListener(updateDataFromBackgroundEventName, onDataFromBackground);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Cross-thread function registry (project plan, Phase 6 — worklet
|
|
107
|
-
// substitute). The common worklet use case (a gesture/tap handler running
|
|
108
|
-
// on the thread it's authored on) needs NONE of this — it's already just an
|
|
109
|
-
// ordinary function in main-thread.ts. This is only for the remaining case:
|
|
110
|
-
// background-owned code needs to trigger a main-thread action outside the
|
|
111
|
-
// normal render/data cycle. Equal *capability* to upstream ReactLynx's own
|
|
112
|
-
// runOnMainThread/runOnBackground (both are async serialized RPC under the
|
|
113
|
-
// hood there too) — worse *ergonomics* only, since there's no compiler to
|
|
114
|
-
// extract an inline closure; handlers must be named and pre-registered on
|
|
115
|
-
// the thread they run on. Listener setup is lazy (on first use), not
|
|
116
|
-
// module-top-level, so importing this module never assumes a particular
|
|
117
|
-
// thread is active yet.
|
|
118
|
-
const mainThreadHandlers = new Map();
|
|
119
|
-
const pendingBackgroundCalls = new Map();
|
|
120
|
-
let nextCallId = 1;
|
|
121
|
-
let crossThreadCallsReady = false;
|
|
122
|
-
|
|
123
|
-
function ensureCrossThreadCalls() {
|
|
124
|
-
if (crossThreadCallsReady) return;
|
|
125
|
-
crossThreadCallsReady = true;
|
|
126
|
-
const background = lynx.getJSContext();
|
|
127
|
-
|
|
128
|
-
background.addEventListener(callMainThreadEventName, (event) => {
|
|
129
|
-
const { callId, key, args } = event.data;
|
|
130
|
-
const fn = mainThreadHandlers.get(key);
|
|
131
|
-
let result;
|
|
132
|
-
let error;
|
|
133
|
-
try {
|
|
134
|
-
result = fn ? fn(...args) : undefined;
|
|
135
|
-
} catch (e) {
|
|
136
|
-
error = e instanceof Error ? e.message : String(e);
|
|
137
|
-
}
|
|
138
|
-
background.dispatchEvent({ type: callMainThreadResultEventName, data: { callId, result, error } });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
background.addEventListener(callBackgroundResultEventName, (event) => {
|
|
142
|
-
const { callId, result, error } = event.data;
|
|
143
|
-
const pending = pendingBackgroundCalls.get(callId);
|
|
144
|
-
if (pending == null) return;
|
|
145
|
-
pendingBackgroundCalls.delete(callId);
|
|
146
|
-
if (error != null) pending.reject(new Error(error));
|
|
147
|
-
else pending.resolve(result);
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Registers a handler background.runOnMainThread(key, ...) can call by name. */
|
|
152
|
-
export function registerHandler(key, fn) {
|
|
153
|
-
ensureCrossThreadCalls();
|
|
154
|
-
mainThreadHandlers.set(key, fn);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Calls a handler background.js registered via registerHandler(key, fn), by name. Args must be JSON-serializable. */
|
|
158
|
-
export function runOnBackground(key, ...args) {
|
|
159
|
-
ensureCrossThreadCalls();
|
|
160
|
-
return new Promise((resolve, reject) => {
|
|
161
|
-
const callId = nextCallId++;
|
|
162
|
-
pendingBackgroundCalls.set(callId, { resolve, reject });
|
|
163
|
-
lynx.getJSContext().dispatchEvent({ type: callBackgroundEventName, data: { callId, key, args } });
|
|
164
|
-
});
|
|
165
|
-
}
|
package/navigation.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Ambient declaration for the ESM navigation.js (the file itself is not
|
|
2
|
-
// type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
-
|
|
4
|
-
import type { Component, ComponentTypes } from "mithril";
|
|
5
|
-
|
|
6
|
-
export interface Nav {
|
|
7
|
-
/** Pushes a new screen onto the stack and redraws. */
|
|
8
|
-
push(component: ComponentTypes<any, any>, attrs?: Record<string, unknown>): void;
|
|
9
|
-
/** Replaces the current top screen without growing the stack, and redraws. */
|
|
10
|
-
replace(component: ComponentTypes<any, any>, attrs?: Record<string, unknown>): void;
|
|
11
|
-
/** Pops the top screen and redraws. Returns false (a no-op) at the root screen. */
|
|
12
|
-
pop(): boolean;
|
|
13
|
-
/** True if pop() would actually pop something (stack depth > 1). */
|
|
14
|
-
canGoBack(): boolean;
|
|
15
|
-
/** Current stack depth (1 at the root screen). */
|
|
16
|
-
depth(): number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface Navigator extends Nav {
|
|
20
|
-
/** Mithril component that always renders whichever screen is on top of the stack. */
|
|
21
|
-
Navigator: Component;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CreateNavigatorOptions {
|
|
25
|
-
/** The root screen, mounted first. */
|
|
26
|
-
initial: ComponentTypes<any, any>;
|
|
27
|
-
initialAttrs?: Record<string, unknown>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Creates a stack-based, in-memory navigator (no m.route, no URLs — see
|
|
32
|
-
* navigation.js's header comment for why). Every screen the navigator
|
|
33
|
-
* renders receives its own attrs plus a `nav` prop shaped like {@link Nav}.
|
|
34
|
-
*/
|
|
35
|
-
export function createNavigator(options: CreateNavigatorOptions): Navigator;
|
package/navigation.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// navigation.js
|
|
2
|
-
//
|
|
3
|
-
// Stack-based, in-memory screen navigation (project plan follow-up, "basic
|
|
4
|
-
// Activity" template support) — deliberately NOT built on m.route. Real
|
|
5
|
-
// `m.route` is hard-wired to the browser's URL/History API (see README.md's
|
|
6
|
-
// "Known permanent gaps"), which has no Lynx equivalent: a Lynx page has no
|
|
7
|
-
// address bar, no back/forward, nothing URL-addressable. Android's own
|
|
8
|
-
// Activity navigation doesn't need URLs either — it's a plain back-stack —
|
|
9
|
-
// so this module ports that idea directly instead of forcing a URL-shaped
|
|
10
|
-
// abstraction onto an environment that has no URLs.
|
|
11
|
-
//
|
|
12
|
-
// Only the top of the stack is ever mounted (previous screens are torn
|
|
13
|
-
// down, not kept alive offscreen) — matching how most single-activity /
|
|
14
|
-
// single-page navigators actually behave, and avoiding the cost of keeping
|
|
15
|
-
// arbitrarily many past screens' DOM trees around. A popped screen that
|
|
16
|
-
// needs to remember its own state should keep that state somewhere the app
|
|
17
|
-
// already owns (a module-level store, background.js's data store, etc.),
|
|
18
|
-
// not rely on the screen's own component instance surviving the pop.
|
|
19
|
-
|
|
20
|
-
import shim from "./src/lynx-mithril-shim.js";
|
|
21
|
-
import m from "mithril";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates a navigator: a stack of {component, attrs} screens, plus a
|
|
25
|
-
* `Navigator` Mithril component that always renders whichever screen is on
|
|
26
|
-
* top. Every screen receives its own `attrs` PLUS a `nav` prop (this
|
|
27
|
-
* navigator's push/pop/replace/canGoBack), so screens don't need to import
|
|
28
|
-
* the navigator instance separately to navigate onward.
|
|
29
|
-
*/
|
|
30
|
-
export function createNavigator(options) {
|
|
31
|
-
const { initial, initialAttrs } = options;
|
|
32
|
-
if (initial == null) throw new Error("mithril-lynx/navigation: createNavigator() requires an `initial` screen");
|
|
33
|
-
|
|
34
|
-
const stack = [{ component: initial, attrs: initialAttrs }];
|
|
35
|
-
|
|
36
|
-
function top() {
|
|
37
|
-
return stack[stack.length - 1];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function push(component, attrs) {
|
|
41
|
-
stack.push({ component, attrs });
|
|
42
|
-
shim.redraw();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function replace(component, attrs) {
|
|
46
|
-
stack[stack.length - 1] = { component, attrs };
|
|
47
|
-
shim.redraw();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Returns false (a no-op) at the root screen, true otherwise. */
|
|
51
|
-
function pop() {
|
|
52
|
-
if (stack.length <= 1) return false;
|
|
53
|
-
stack.pop();
|
|
54
|
-
shim.redraw();
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function canGoBack() {
|
|
59
|
-
return stack.length > 1;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function depth() {
|
|
63
|
-
return stack.length;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const nav = { push, pop, replace, canGoBack, depth };
|
|
67
|
-
|
|
68
|
-
const Navigator = {
|
|
69
|
-
view() {
|
|
70
|
-
const { component, attrs } = top();
|
|
71
|
-
return m(component, Object.assign({}, attrs, { nav }));
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
return Object.assign({ Navigator }, nav);
|
|
76
|
-
}
|
package/renderer/background.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Ambient declaration for the ESM renderer/background.js (the file itself is
|
|
2
|
-
// not type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
-
|
|
4
|
-
export interface RenderAppOptions {
|
|
5
|
-
/** Called exactly once to build the root Mithril vnode against the virtual tree. */
|
|
6
|
-
root(): unknown;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Renders `root()` against a virtual (op-log-recording) tree and sends the
|
|
11
|
-
* resulting patch to the main thread. Call once, at background.ts's top
|
|
12
|
-
* level. See the project plan, Phase 4 ("renderer mode").
|
|
13
|
-
*/
|
|
14
|
-
export function renderApp(options: RenderAppOptions): void;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Re-invokes the app's view() (via the shim's own redraw()) and flushes the
|
|
18
|
-
* resulting patch to the main thread. Call this instead of importing the
|
|
19
|
-
* shim's redraw() directly in renderer-mode event handlers.
|
|
20
|
-
*/
|
|
21
|
-
export function redraw(): void;
|
package/renderer/background.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// renderer/background.js
|
|
2
|
-
//
|
|
3
|
-
// "Renderer mode" (project plan, Phase 4): runs Mithril's UNMODIFIED diff
|
|
4
|
-
// algorithm on the background thread against a VirtualNodeWrapper tree
|
|
5
|
-
// (internal/virtual-node.js) instead of a real Element PAPI tree. Every
|
|
6
|
-
// mutation the diff makes is recorded as a serializable op; ops accumulated
|
|
7
|
-
// during one render/redraw pass are flushed as a single batch to the main
|
|
8
|
-
// thread, which replays them against a real LynxNodeWrapper tree (see
|
|
9
|
-
// renderer/main-thread.js's applyPatch()).
|
|
10
|
-
//
|
|
11
|
-
// This reuses the shim's OWN render()/redraw() convenience API unmodified —
|
|
12
|
-
// the algorithm never knows it's talking to a virtual tree, because it only
|
|
13
|
-
// ever calls generic dom.* methods (see ../CONTRACT.md). Note: render(), not
|
|
14
|
-
// renderToPage() — renderToPage() expects a RAW native page handle and calls
|
|
15
|
-
// __GetElementUniqueID() on it internally; render() takes an
|
|
16
|
-
// already-constructed wrapper directly, which is what the virtual root is.
|
|
17
|
-
|
|
18
|
-
import shim from "../src/lynx-mithril-shim.js";
|
|
19
|
-
import { createVirtualDocument } from "../internal/virtual-node.js";
|
|
20
|
-
import { rendererEventEventName, rendererPatchEventName } from "../internal/constants.js";
|
|
21
|
-
|
|
22
|
-
// A real app calls renderApp() exactly once, establishing this state for the
|
|
23
|
-
// lifetime of the app; redraw() operates on whatever the latest renderApp()
|
|
24
|
-
// call set up. Kept in module scope (rather than passed around) only because
|
|
25
|
-
// redraw() is a separate export with no other way to reach it — NOT shared
|
|
26
|
-
// across independent virtual trees the way a naively module-level
|
|
27
|
-
// styleProxies/vid-counter would be (createVirtualDocument() is called AFRESH
|
|
28
|
-
// inside renderApp(), so each call gets its own independent document, vid
|
|
29
|
-
// counter, and style-proxy registry).
|
|
30
|
-
let opLog = [];
|
|
31
|
-
let flushStyleProxies = () => {};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Renders `root()` once against the virtual tree and sends the initial
|
|
35
|
-
* patch. Call exactly once, at background.ts's top level (mirrors
|
|
36
|
-
* setupApp()'s root() contract in data-channel mode: subsequent updates
|
|
37
|
-
* flow through redraw(), which re-invokes the component's view(), not
|
|
38
|
-
* root() again).
|
|
39
|
-
*/
|
|
40
|
-
export function renderApp(options) {
|
|
41
|
-
const { root } = options;
|
|
42
|
-
opLog = [];
|
|
43
|
-
// vid -> VirtualNodeWrapper, so forwarded main-thread events (addressed by
|
|
44
|
-
// vid) can be dispatched to the right node's listener.
|
|
45
|
-
const nodesByVid = new Map();
|
|
46
|
-
|
|
47
|
-
const virtualDocument = createVirtualDocument(
|
|
48
|
-
(op) => opLog.push(op),
|
|
49
|
-
(wrapper) => nodesByVid.set(wrapper._vid, wrapper),
|
|
50
|
-
);
|
|
51
|
-
flushStyleProxies = virtualDocument.flushStyleProxies;
|
|
52
|
-
|
|
53
|
-
const rootWrapper = virtualDocument.createRootWrapper();
|
|
54
|
-
|
|
55
|
-
shim.render(rootWrapper, root());
|
|
56
|
-
flush();
|
|
57
|
-
|
|
58
|
-
lynx.getCoreContext().addEventListener(rendererEventEventName, (event) => {
|
|
59
|
-
const { vid, type, payload } = event.data;
|
|
60
|
-
const node = nodesByVid.get(vid);
|
|
61
|
-
if (node == null) return;
|
|
62
|
-
node.dispatchEvent({
|
|
63
|
-
type,
|
|
64
|
-
currentTarget: node,
|
|
65
|
-
preventDefault() {},
|
|
66
|
-
stopPropagation() {},
|
|
67
|
-
...payload,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function flush() {
|
|
73
|
-
flushStyleProxies();
|
|
74
|
-
if (opLog.length === 0) return;
|
|
75
|
-
const ops = opLog;
|
|
76
|
-
opLog = [];
|
|
77
|
-
lynx.getCoreContext().dispatchEvent({ type: rendererPatchEventName, data: ops });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Re-invokes the app's view() and flushes the resulting patch. Call this — not shim.redraw() directly — after mutating state in an event handler. */
|
|
81
|
-
export function redraw() {
|
|
82
|
-
shim.redraw();
|
|
83
|
-
flush();
|
|
84
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Ambient declaration for the ESM renderer/main-thread.js (the file itself
|
|
2
|
-
// is not type-checked; this describes its runtime export shape for TS
|
|
3
|
-
// consumers).
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Wires the Lynx engine's page lifecycle and the renderer-mode patch/event
|
|
7
|
-
* channel: replays op-log patches from the background thread's virtual tree
|
|
8
|
-
* against a real Element PAPI tree, and forwards real PAPI events back to
|
|
9
|
-
* the background thread by vid. Call once, at main-thread.ts's top level.
|
|
10
|
-
* See the project plan, Phase 4 ("renderer mode").
|
|
11
|
-
*/
|
|
12
|
-
export function setupRenderer(): void;
|