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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/list-attributes.js
|
|
2
|
+
//
|
|
3
|
+
// The attribute catalog of Lynx's built-in `<list>` / `<list-item>`
|
|
4
|
+
// elements, shared by both threads. Source: the official element reference
|
|
5
|
+
// (lynxjs.org/api/elements/built-in/list), plus the list-item platform-info
|
|
6
|
+
// set ReactLynx itself sends to native (`platformInfoAttributes` in
|
|
7
|
+
// @lynx-js/react's runtime/lib/snapshot/snapshot/platformInfo.js).
|
|
8
|
+
//
|
|
9
|
+
// Why a catalog at all: these attributes are TYPED on the native side
|
|
10
|
+
// (booleans, numbers, the `item-snap` object). Mithril's plain attribute
|
|
11
|
+
// path can't carry that — it turns `true` into `setAttribute(key, "")` and
|
|
12
|
+
// `false` into `removeAttribute(key)` (render.js `setAttr`), and fake-dom's
|
|
13
|
+
// generic setAttribute stringifies. fake-dom exposes every name below as a
|
|
14
|
+
// real property on `list`/`list-item` elements, which makes Mithril take its
|
|
15
|
+
// property path instead (`key in vnode.dom` -> `vnode.dom[key] = value`) and
|
|
16
|
+
// hand over the raw value, `false` included.
|
|
17
|
+
|
|
18
|
+
/** Every documented `<list>` attribute. The CSS-only `list-main-axis-gap` /
|
|
19
|
+
* `list-cross-axis-gap` go through `style`, not here. */
|
|
20
|
+
export const LIST_ATTRIBUTES = Object.freeze([
|
|
21
|
+
"list-type",
|
|
22
|
+
"span-count",
|
|
23
|
+
"scroll-orientation",
|
|
24
|
+
"enable-scroll",
|
|
25
|
+
"enable-nested-scroll",
|
|
26
|
+
"sticky",
|
|
27
|
+
"sticky-offset",
|
|
28
|
+
"bounces",
|
|
29
|
+
"initial-scroll-index",
|
|
30
|
+
"need-visible-item-info",
|
|
31
|
+
"upper-threshold-item-count",
|
|
32
|
+
"lower-threshold-item-count",
|
|
33
|
+
"scroll-event-throttle",
|
|
34
|
+
"item-snap",
|
|
35
|
+
"update-animation",
|
|
36
|
+
"need-layout-complete-info",
|
|
37
|
+
"layout-id",
|
|
38
|
+
"preload-buffer-count",
|
|
39
|
+
"scroll-bar-enable",
|
|
40
|
+
"harmony-scroll-edge-effect",
|
|
41
|
+
"experimental-recycle-sticky-item",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** `<list-item>` attributes native needs BEFORE it renders an item (they
|
|
45
|
+
* drive layout: spans, sticky slots, placeholder size, reuse pools), so they
|
|
46
|
+
* travel inside `update-list-info`'s insert/update actions, not only as
|
|
47
|
+
* element attributes. */
|
|
48
|
+
export const LIST_ITEM_PLATFORM_ATTRIBUTES = Object.freeze([
|
|
49
|
+
"item-key",
|
|
50
|
+
"full-span",
|
|
51
|
+
"sticky-top",
|
|
52
|
+
"sticky-bottom",
|
|
53
|
+
"estimated-height",
|
|
54
|
+
"estimated-height-px",
|
|
55
|
+
"estimated-main-axis-size-px",
|
|
56
|
+
"reuse-identifier",
|
|
57
|
+
"recyclable",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** Platform-info attributes that exist only inside `update-list-info` and
|
|
61
|
+
* are never set on the element itself (ReactLynx's
|
|
62
|
+
* `platformInfoVirtualAttributes`). */
|
|
63
|
+
export const LIST_ITEM_VIRTUAL_ATTRIBUTES = Object.freeze(["reuse-identifier", "recyclable"]);
|
|
64
|
+
|
|
65
|
+
/** Typed attributes exposed as properties on the fake-dom element, per tag. */
|
|
66
|
+
export const TYPED_ATTRIBUTES_BY_TAG = Object.freeze({
|
|
67
|
+
list: LIST_ATTRIBUTES,
|
|
68
|
+
"list-item": LIST_ITEM_PLATFORM_ATTRIBUTES,
|
|
69
|
+
});
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// src/list-runtime.js
|
|
2
|
+
//
|
|
3
|
+
// Main-thread runtime for Lynx's native `<list>`, driven by apply-patch.js.
|
|
4
|
+
// Runs ONLY on the main thread.
|
|
5
|
+
//
|
|
6
|
+
// The model is the one @lynx-js/react adopted for its element-template
|
|
7
|
+
// runtime (runtime/lib/element-template/runtime/list/list.js): `<list-item>`s
|
|
8
|
+
// are ORDINARY elements of the app's tree — created, diffed and updated by
|
|
9
|
+
// the normal patch stream — and each keeps its own element subtree for its
|
|
10
|
+
// whole life. The list never renders anything inside a native callback:
|
|
11
|
+
//
|
|
12
|
+
// - InsertBefore/RemoveChild ops whose parent is a list do NOT touch the
|
|
13
|
+
// native tree. They only update the list's logical item order and record
|
|
14
|
+
// what changed; at the end of the patch, `flushUpdates()` tells native
|
|
15
|
+
// through `update-list-info` (insert/remove/update actions, each carrying
|
|
16
|
+
// the item's platform info) and re-registers the callbacks.
|
|
17
|
+
// - `componentAtIndex` (native asking for index N) attaches item N's
|
|
18
|
+
// existing element under the list, before the next attached item, and
|
|
19
|
+
// flushes it. `enqueueComponent` (item scrolled away) detaches it again.
|
|
20
|
+
//
|
|
21
|
+
// Only attached items have native UI views; detached ones are plain element
|
|
22
|
+
// trees. Content updates inside any item are ordinary ops.
|
|
23
|
+
//
|
|
24
|
+
// PAPI contract, per ReactLynx:
|
|
25
|
+
// __CreateList(pageId, componentAtIndex, enqueueComponent, {}, componentAtIndexes)
|
|
26
|
+
// __SetAttribute(list, "update-list-info", { insertAction, removeAction, updateAction })
|
|
27
|
+
// __UpdateListCallbacks(list, componentAtIndex, enqueueComponent, componentAtIndexes)
|
|
28
|
+
// componentAtIndex(list, listID, cellIndex, operationID, enableReuseNotification) -> sign
|
|
29
|
+
// componentAtIndexes(list, listID, cellIndexes, operationIDs, enableReuseNotification, asyncFlush)
|
|
30
|
+
// enqueueComponent(list, listID, sign)
|
|
31
|
+
|
|
32
|
+
import { LIST_ITEM_PLATFORM_ATTRIBUTES } from "./list-attributes.js";
|
|
33
|
+
|
|
34
|
+
const PLATFORM_KEYS = new Set(LIST_ITEM_PLATFORM_ATTRIBUTES);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether an attribute name is part of a list item's platform info.
|
|
38
|
+
* @param {string} name - Attribute name.
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function isListItemPlatformAttribute(name) {
|
|
42
|
+
return PLATFORM_KEYS.has(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Shallow equality for two platform-info objects (their values are
|
|
47
|
+
* primitives, as documented for every catalogued list-item attribute).
|
|
48
|
+
* @param {Object<string, *>} a - First info.
|
|
49
|
+
* @param {Object<string, *>} b - Second info.
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function sameInfo(a, b) {
|
|
53
|
+
const aKeys = Object.keys(a);
|
|
54
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
55
|
+
for (const key of aKeys) if (a[key] !== b[key]) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The item's native `type`: its reuse pool. `reuse-identifier` when set,
|
|
61
|
+
* like ReactLynx; otherwise one shared pool.
|
|
62
|
+
* @param {{info: Object<string, *>}} item - The item record.
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function typeOf(item) {
|
|
66
|
+
const reuse = item.info["reuse-identifier"];
|
|
67
|
+
return reuse != null ? String(reuse) : "list-item";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates one native list and the state behind it.
|
|
72
|
+
* @param {number} pageId - The page's unique id.
|
|
73
|
+
* @returns {{handle: *, insert: (id: number, handle: *, info: Object, refId: number) => void, remove: (id: number) => void, has: (id: number) => boolean, updateInfo: (id: number, info: Object) => void, setUpdateAnimation: (value: *) => void, flushUpdates: () => void, detachRemoved: () => boolean, destroy: () => void}}
|
|
74
|
+
*/
|
|
75
|
+
export function createListRuntime(pageId) {
|
|
76
|
+
/** @type {Array<{id: number, handle: *, info: Object<string, *>, attached: boolean, needsAttachMove: boolean, skipNextEnqueue: boolean}>} */
|
|
77
|
+
let items = [];
|
|
78
|
+
const itemsById = new Map();
|
|
79
|
+
const itemsBySign = new Map();
|
|
80
|
+
let hasAttachedMoves = false;
|
|
81
|
+
let destroyed = false;
|
|
82
|
+
// On-screen items removed by the current patch, detached only after the
|
|
83
|
+
// patch's flush (see remove()).
|
|
84
|
+
let removedAttached = [];
|
|
85
|
+
// Mirrors the list's `update-animation` attribute (see remove()).
|
|
86
|
+
let animatesUpdates = false;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Records the list's `update-animation` attribute.
|
|
90
|
+
* @param {*} value - The attribute value.
|
|
91
|
+
* @returns {void}
|
|
92
|
+
*/
|
|
93
|
+
function setUpdateAnimation(value) {
|
|
94
|
+
animatesUpdates = value === "default";
|
|
95
|
+
if (animatesUpdates && typeof console !== "undefined") {
|
|
96
|
+
console.warn(
|
|
97
|
+
"[mithril-lynx] list: update-animation=\"default\" is not fully supported yet — removed items stay " +
|
|
98
|
+
"attached to the list element (detaching them while native animates crashes Lynx). " +
|
|
99
|
+
"See UPDATE_ANIMATION_GAP.md in mithril-lynx.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Set while a patch is changing this list: the item order and infos as
|
|
104
|
+
// they were BEFORE the patch, plus what was inserted/removed since.
|
|
105
|
+
let pending = null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Starts recording changes for the current patch, if not already.
|
|
109
|
+
* @returns {{beforeItems: Array<{id: number, info: Object}>, beforeIndexById: Map<number, number>, insertIds: Set<number>, removeIds: Set<number>}}
|
|
110
|
+
*/
|
|
111
|
+
function markPending() {
|
|
112
|
+
if (pending == null) {
|
|
113
|
+
const beforeItems = items.map((item) => ({ id: item.id, info: Object.assign({}, item.info) }));
|
|
114
|
+
const beforeIndexById = new Map();
|
|
115
|
+
beforeItems.forEach((item, index) => beforeIndexById.set(item.id, index));
|
|
116
|
+
pending = { beforeItems, beforeIndexById, insertIds: new Set(), removeIds: new Set() };
|
|
117
|
+
}
|
|
118
|
+
return pending;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Index of an item in the current order.
|
|
123
|
+
* @param {number} id - Item id.
|
|
124
|
+
* @returns {number} The index, or -1.
|
|
125
|
+
*/
|
|
126
|
+
function indexOf(id) {
|
|
127
|
+
for (let i = 0; i < items.length; i++) if (items[i].id === id) return i;
|
|
128
|
+
return -1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Inserts (or moves) an item before `refId` (-1 = at the end).
|
|
133
|
+
* @param {number} id - Item id.
|
|
134
|
+
* @param {*} handle - The item's `list-item` element.
|
|
135
|
+
* @param {Object<string, *>} info - Its current platform info.
|
|
136
|
+
* @param {number} refId - Id of the item to insert before, or -1.
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function insert(id, handle, info, refId) {
|
|
140
|
+
const p = markPending();
|
|
141
|
+
let item = itemsById.get(id);
|
|
142
|
+
if (item != null) {
|
|
143
|
+
// A move: Mithril re-inserted an item that is already in the list.
|
|
144
|
+
// Native sees it as remove(old index) + insert(new index), like
|
|
145
|
+
// ReactLynx; an attached one is re-placed on the next callback.
|
|
146
|
+
items.splice(indexOf(id), 1);
|
|
147
|
+
if (p.beforeIndexById.has(id)) p.removeIds.add(id);
|
|
148
|
+
if (item.attached) {
|
|
149
|
+
item.needsAttachMove = true;
|
|
150
|
+
hasAttachedMoves = true;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
item = { id, handle, info: Object.assign({}, info), attached: false, needsAttachMove: false, skipNextEnqueue: false };
|
|
154
|
+
itemsById.set(id, item);
|
|
155
|
+
}
|
|
156
|
+
const refIndex = refId === -1 ? -1 : indexOf(refId);
|
|
157
|
+
if (refIndex === -1) items.push(item);
|
|
158
|
+
else items.splice(refIndex, 0, item);
|
|
159
|
+
p.insertIds.add(id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Removes an item from the list's data. An item that is on screen is
|
|
164
|
+
* detached from the list element only AFTER native has processed this
|
|
165
|
+
* patch's `removeAction` (see detachRemoved()). Verified on a real device
|
|
166
|
+
* (Android, Lynx Go):
|
|
167
|
+
* - detaching it here, before `update-list-info` is flushed, makes
|
|
168
|
+
* native fail to find the item's holder and crash (SIGSEGV after
|
|
169
|
+
* "[List] Fail to erase item holder");
|
|
170
|
+
* - never detaching it renders fine (native drops its cell) but leaves
|
|
171
|
+
* its element as an orphan child of the list, growing with every
|
|
172
|
+
* removal — native never calls `enqueueComponent` for removed items;
|
|
173
|
+
* - detaching it after the flush is clean, EXCEPT while an
|
|
174
|
+
* `update-animation` is running on the list: native animates the
|
|
175
|
+
* removed cell and crashes when the animation ends if its element is
|
|
176
|
+
* gone. So with `update-animation="default"` removed items are left
|
|
177
|
+
* attached (see setUpdateAnimation()).
|
|
178
|
+
* @param {number} id - Item id.
|
|
179
|
+
* @returns {void}
|
|
180
|
+
*/
|
|
181
|
+
function remove(id) {
|
|
182
|
+
const item = itemsById.get(id);
|
|
183
|
+
if (item == null) return;
|
|
184
|
+
const p = markPending();
|
|
185
|
+
items.splice(indexOf(id), 1);
|
|
186
|
+
itemsById.delete(id);
|
|
187
|
+
if (p.beforeIndexById.has(id)) p.removeIds.add(id);
|
|
188
|
+
p.insertIds.delete(id);
|
|
189
|
+
if (item.attached) removedAttached.push(item);
|
|
190
|
+
else forgetSigns(item);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Drops every sign that maps to `item`.
|
|
195
|
+
* @param {Object} item - The item record.
|
|
196
|
+
* @returns {void}
|
|
197
|
+
*/
|
|
198
|
+
function forgetSigns(item) {
|
|
199
|
+
for (const [sign, bound] of itemsBySign) if (bound === item) itemsBySign.delete(sign);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Detaches the on-screen items removed by the last patch. Must run after
|
|
204
|
+
* that patch's `update-list-info` has been flushed to native.
|
|
205
|
+
* @returns {boolean} Whether anything was detached (and needs a flush).
|
|
206
|
+
*/
|
|
207
|
+
function detachRemoved() {
|
|
208
|
+
if (removedAttached.length === 0) return false;
|
|
209
|
+
const detached = removedAttached;
|
|
210
|
+
removedAttached = [];
|
|
211
|
+
let any = false;
|
|
212
|
+
for (const item of detached) {
|
|
213
|
+
forgetSigns(item);
|
|
214
|
+
if (!item.attached || destroyed || animatesUpdates) continue;
|
|
215
|
+
__RemoveElement(handle, item.handle);
|
|
216
|
+
item.attached = false;
|
|
217
|
+
any = true;
|
|
218
|
+
}
|
|
219
|
+
return any;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Records a platform-info change on an item already in the list.
|
|
224
|
+
* @param {number} id - Item id.
|
|
225
|
+
* @param {Object<string, *>} info - The new info.
|
|
226
|
+
* @returns {void}
|
|
227
|
+
*/
|
|
228
|
+
function updateInfo(id, info) {
|
|
229
|
+
const item = itemsById.get(id);
|
|
230
|
+
if (item == null) return;
|
|
231
|
+
markPending();
|
|
232
|
+
item.info = Object.assign({}, info);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Sends this patch's changes to native, if any. Must run before the
|
|
237
|
+
* patch's `__FlushElementTree()`.
|
|
238
|
+
* @returns {void}
|
|
239
|
+
*/
|
|
240
|
+
function flushUpdates() {
|
|
241
|
+
if (pending == null || destroyed) {
|
|
242
|
+
pending = null;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const { beforeItems, beforeIndexById, insertIds, removeIds } = pending;
|
|
246
|
+
pending = null;
|
|
247
|
+
const insertAction = [];
|
|
248
|
+
const removeAction = [];
|
|
249
|
+
const updateAction = [];
|
|
250
|
+
beforeItems.forEach((item, index) => {
|
|
251
|
+
if (removeIds.has(item.id)) removeAction.push(index);
|
|
252
|
+
});
|
|
253
|
+
items.forEach((item, index) => {
|
|
254
|
+
if (insertIds.has(item.id)) {
|
|
255
|
+
insertAction.push(Object.assign({ position: index, type: typeOf(item) }, item.info));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const before = beforeItems[beforeIndexById.get(item.id)];
|
|
259
|
+
if (!sameInfo(before.info, item.info)) {
|
|
260
|
+
updateAction.push(Object.assign({}, item.info, { from: index, to: index, type: typeOf(item), flush: false }));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (insertAction.length === 0 && removeAction.length === 0 && updateAction.length === 0) return;
|
|
264
|
+
__SetAttribute(handle, "update-list-info", { insertAction, removeAction, updateAction });
|
|
265
|
+
__UpdateListCallbacks(handle, componentAtIndex, enqueueComponent, componentAtIndexes);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Re-places attached items that moved, walking from the end so each one
|
|
270
|
+
* lands before its already-placed successor. Like ReactLynx, the move
|
|
271
|
+
* makes native enqueue the item once more, which `skipNextEnqueue` absorbs.
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
function moveAttachedItemsIntoFinalOrder() {
|
|
275
|
+
if (!hasAttachedMoves) return;
|
|
276
|
+
let reference = null;
|
|
277
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
278
|
+
const item = items[i];
|
|
279
|
+
if (!item.attached) {
|
|
280
|
+
item.needsAttachMove = false;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (item.needsAttachMove) {
|
|
284
|
+
if (reference != null) __InsertElementBefore(handle, item.handle, reference);
|
|
285
|
+
else __AppendElement(handle, item.handle);
|
|
286
|
+
item.needsAttachMove = false;
|
|
287
|
+
item.skipNextEnqueue = true;
|
|
288
|
+
}
|
|
289
|
+
reference = item.handle;
|
|
290
|
+
}
|
|
291
|
+
hasAttachedMoves = false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The first attached item after `cellIndex`, whose element a newly
|
|
296
|
+
* attached item must be inserted before: native lays cells out in tree
|
|
297
|
+
* order.
|
|
298
|
+
* @param {number} cellIndex - The index being attached.
|
|
299
|
+
* @returns {*} The next attached element, or `null`.
|
|
300
|
+
*/
|
|
301
|
+
function nextAttachedHandle(cellIndex) {
|
|
302
|
+
for (let i = cellIndex + 1; i < items.length; i++) if (items[i].attached) return items[i].handle;
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Attaches item `cellIndex` and returns its sign.
|
|
308
|
+
* @param {number} listID - The list's unique id.
|
|
309
|
+
* @param {number} cellIndex - The requested index.
|
|
310
|
+
* @param {number} operationID - Native operation id.
|
|
311
|
+
* @param {boolean} batch - Whether this is part of `componentAtIndexes`.
|
|
312
|
+
* @param {boolean} [asyncFlush=false] - Batch only: flush this item asynchronously.
|
|
313
|
+
* @returns {number} The item element's unique id, or -1 for an unknown index.
|
|
314
|
+
*/
|
|
315
|
+
function attach(listID, cellIndex, operationID, batch, asyncFlush = false) {
|
|
316
|
+
moveAttachedItemsIntoFinalOrder();
|
|
317
|
+
const item = items[cellIndex];
|
|
318
|
+
if (item == null) {
|
|
319
|
+
if (typeof console !== "undefined") {
|
|
320
|
+
console.error(`[mithril-lynx] list: componentAtIndex(${cellIndex}) is out of range (${items.length} items).`);
|
|
321
|
+
}
|
|
322
|
+
return -1;
|
|
323
|
+
}
|
|
324
|
+
if (!item.attached) {
|
|
325
|
+
const reference = nextAttachedHandle(cellIndex);
|
|
326
|
+
if (reference != null) __InsertElementBefore(handle, item.handle, reference);
|
|
327
|
+
else __AppendElement(handle, item.handle);
|
|
328
|
+
item.attached = true;
|
|
329
|
+
item.needsAttachMove = false;
|
|
330
|
+
}
|
|
331
|
+
const sign = __GetElementUniqueID(item.handle);
|
|
332
|
+
itemsBySign.set(sign, item);
|
|
333
|
+
if (!batch) __FlushElementTree(item.handle, { triggerLayout: true, operationID, elementID: sign, listID });
|
|
334
|
+
else if (asyncFlush) __FlushElementTree(item.handle, { asyncFlush: true });
|
|
335
|
+
return sign;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Native callback: provides the item at one index.
|
|
340
|
+
* @param {*} _list - The list element.
|
|
341
|
+
* @param {number} listID - The list's unique id.
|
|
342
|
+
* @param {number} cellIndex - The requested index.
|
|
343
|
+
* @param {number} operationID - Native operation id.
|
|
344
|
+
* @returns {number} The item's sign, or -1.
|
|
345
|
+
*/
|
|
346
|
+
function componentAtIndex(_list, listID, cellIndex, operationID) {
|
|
347
|
+
if (destroyed) return -1;
|
|
348
|
+
return attach(listID, cellIndex, operationID, false);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Native callback: provides several items with one list flush.
|
|
353
|
+
* @param {*} _list - The list element.
|
|
354
|
+
* @param {number} listID - The list's unique id.
|
|
355
|
+
* @param {number[]} cellIndexes - The requested indexes.
|
|
356
|
+
* @param {number[]} operationIDs - Native operation ids, one per index.
|
|
357
|
+
* @param {boolean} [_enableReuseNotification] - Unused: items are never reused for other data.
|
|
358
|
+
* @param {boolean} [asyncFlush] - Flush each item asynchronously.
|
|
359
|
+
* @returns {void}
|
|
360
|
+
*/
|
|
361
|
+
function componentAtIndexes(_list, listID, cellIndexes, operationIDs, _enableReuseNotification, asyncFlush) {
|
|
362
|
+
if (destroyed) return;
|
|
363
|
+
const elementIDs = cellIndexes.map((cellIndex, i) => attach(listID, cellIndex, operationIDs[i], true, asyncFlush === true));
|
|
364
|
+
__FlushElementTree(handle, { triggerLayout: true, operationIDs, elementIDs, listID });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Native callback: an item scrolled away; detach its element.
|
|
369
|
+
* @param {*} _list - The list element.
|
|
370
|
+
* @param {number} _listID - The list's unique id.
|
|
371
|
+
* @param {number} sign - The item's sign.
|
|
372
|
+
* @returns {void}
|
|
373
|
+
*/
|
|
374
|
+
function enqueueComponent(_list, _listID, sign) {
|
|
375
|
+
if (destroyed) return;
|
|
376
|
+
const item = itemsBySign.get(sign);
|
|
377
|
+
if (item == null) return;
|
|
378
|
+
if (!item.attached) {
|
|
379
|
+
itemsBySign.delete(sign);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (item.skipNextEnqueue) {
|
|
383
|
+
item.skipNextEnqueue = false;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
__RemoveElement(handle, item.handle);
|
|
387
|
+
item.attached = false;
|
|
388
|
+
item.needsAttachMove = false;
|
|
389
|
+
itemsBySign.delete(sign);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Neutralizes the native callbacks and drops all state, like ReactLynx's
|
|
394
|
+
* `snapshotDestroyList`. Safe to call more than once.
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
function destroy() {
|
|
398
|
+
if (destroyed) return;
|
|
399
|
+
destroyed = true;
|
|
400
|
+
__UpdateListCallbacks(handle, () => -1, () => {}, () => {});
|
|
401
|
+
items = [];
|
|
402
|
+
itemsById.clear();
|
|
403
|
+
itemsBySign.clear();
|
|
404
|
+
removedAttached = [];
|
|
405
|
+
pending = null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const handle = __CreateList(pageId, componentAtIndex, enqueueComponent, {}, componentAtIndexes);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
handle,
|
|
412
|
+
insert,
|
|
413
|
+
remove,
|
|
414
|
+
updateInfo,
|
|
415
|
+
setUpdateAnimation,
|
|
416
|
+
/**
|
|
417
|
+
* @param {number} id - Item id.
|
|
418
|
+
* @returns {boolean} Whether the item is in this list.
|
|
419
|
+
*/
|
|
420
|
+
has: (id) => itemsById.has(id),
|
|
421
|
+
flushUpdates,
|
|
422
|
+
detachRemoved,
|
|
423
|
+
destroy,
|
|
424
|
+
};
|
|
425
|
+
}
|
package/src/main-thread.js
CHANGED
|
@@ -32,6 +32,7 @@ Object.assign(globalThis, {
|
|
|
32
32
|
* finish before or after that fires — patches arriving early are buffered
|
|
33
33
|
* and replayed in order once the page exists), then wires the patch/event
|
|
34
34
|
* channel for the lifetime of the page.
|
|
35
|
+
* @returns {void}
|
|
35
36
|
*/
|
|
36
37
|
export function setupRenderer() {
|
|
37
38
|
const engine = lynx.getEngine();
|
|
@@ -39,6 +40,12 @@ export function setupRenderer() {
|
|
|
39
40
|
let pageReady = false;
|
|
40
41
|
let pendingPatches = [];
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Receives a patch, validating its protocol version and buffering it until the page exists.
|
|
45
|
+
* @param {{data: unknown}} event - The patch event.
|
|
46
|
+
* @returns {void}
|
|
47
|
+
* @throws {Error} On a protocol version mismatch.
|
|
48
|
+
*/
|
|
42
49
|
const onPatch = (event) => {
|
|
43
50
|
const data = event.data;
|
|
44
51
|
if (!Array.isArray(data) || data[0] !== PROTOCOL_VERSION) {
|
|
@@ -60,11 +67,15 @@ export function setupRenderer() {
|
|
|
60
67
|
};
|
|
61
68
|
onPatchFromBackground(onPatch);
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Creates the real page and applier, then flushes any buffered patches.
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
63
74
|
const onRenderPage = () => {
|
|
64
75
|
const page = __CreatePage("0", 0);
|
|
65
76
|
const pageId = __GetElementUniqueID(page);
|
|
66
77
|
applier = createPatchApplier(pageId, {
|
|
67
|
-
onEvent: (id, type, nativeEvent) => sendEventToBackground(id, type, nativeEvent),
|
|
78
|
+
onEvent: (id, type, nativeEvent, seq) => sendEventToBackground(id, type, nativeEvent, seq),
|
|
68
79
|
});
|
|
69
80
|
applier.registerPageRoot(page);
|
|
70
81
|
pageReady = true;
|
|
@@ -73,7 +84,13 @@ export function setupRenderer() {
|
|
|
73
84
|
};
|
|
74
85
|
engine.addEventListener(renderPageEventName, onRenderPage);
|
|
75
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Neutralizes native list callbacks and removes the page lifecycle listeners.
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
76
91
|
const onDestroyLifetime = () => {
|
|
92
|
+
// Native lists must not call back into state that is going away.
|
|
93
|
+
if (applier != null) applier.dispose();
|
|
77
94
|
engine.removeEventListener(renderPageEventName, onRenderPage);
|
|
78
95
|
engine.removeEventListener(destroyLifetimeEventName, onDestroyLifetime);
|
|
79
96
|
};
|
package/src/mount-redraw.js
CHANGED
|
@@ -61,6 +61,8 @@ let redrawDelayMs = REDRAW_DELAY_MS;
|
|
|
61
61
|
* which is a safety margin rather than a scheduling guarantee. A device whose
|
|
62
62
|
* timer behaves differently (see FETCH_INVESTIGATION.md §4.6) can raise or
|
|
63
63
|
* lower it here.
|
|
64
|
+
* @param {{redrawDelayMs?: number}} [options] - Settings; `redrawDelayMs` overrides the debounce delay.
|
|
65
|
+
* @returns {void}
|
|
64
66
|
*/
|
|
65
67
|
export function configure(options) {
|
|
66
68
|
if (options && options.redrawDelayMs != null) {
|
|
@@ -68,6 +70,11 @@ export function configure(options) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Schedules a callback after the redraw delay, preferring `lynx.setTimeout`.
|
|
75
|
+
* @param {() => void} fn - The callback to run.
|
|
76
|
+
* @returns {*} The timer handle.
|
|
77
|
+
*/
|
|
71
78
|
function schedule(fn) {
|
|
72
79
|
const timer = typeof lynx !== "undefined" && typeof lynx.setTimeout === "function" ? lynx.setTimeout.bind(lynx) : setTimeout;
|
|
73
80
|
return timer(fn, redrawDelayMs);
|
|
@@ -80,6 +87,9 @@ function schedule(fn) {
|
|
|
80
87
|
* documents corrupt the shared id space (see R2 in
|
|
81
88
|
* informe-contrato-mithril-lynx.md). Call {@link unregister} on teardown
|
|
82
89
|
* or full reload before registering a new one.
|
|
90
|
+
* @param {() => void} redraw - The app's redraw function.
|
|
91
|
+
* @returns {void}
|
|
92
|
+
* @throws {Error} If a redraw is already registered.
|
|
83
93
|
*/
|
|
84
94
|
export function register(redraw) {
|
|
85
95
|
if (currentRedraw != null) {
|
|
@@ -97,6 +107,7 @@ export function register(redraw) {
|
|
|
97
107
|
* Clears the current redraw registration (and the pending debounce flag) —
|
|
98
108
|
* used by a full reload and by the test suite between mounts. After this,
|
|
99
109
|
* `redraw()` is a no-op again until the next `register()`.
|
|
110
|
+
* @returns {void}
|
|
100
111
|
*/
|
|
101
112
|
export function unregister() {
|
|
102
113
|
currentRedraw = null;
|
|
@@ -115,7 +126,9 @@ export function unregister() {
|
|
|
115
126
|
* renderApp()/route() ran has nothing to redraw yet, which isn't
|
|
116
127
|
* necessarily a bug the way calling commit() before mounting is. Multiple
|
|
117
128
|
* calls within the delay window collapse into a single scheduled render,
|
|
118
|
-
* same debounce real Mithril's `redraw()` does with its `pending` flag.
|
|
129
|
+
* same debounce real Mithril's `redraw()` does with its `pending` flag.
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
119
132
|
export function redraw() {
|
|
120
133
|
if (pending) return;
|
|
121
134
|
pending = true;
|