mithril-lynx 0.0.9 → 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.
Files changed (61) hide show
  1. package/.omo/plans/m-request-fetch-lynx.md +306 -0
  2. package/.omo/plans/m-route-en-memoria.md +397 -0
  3. package/.omo/plans/mithril-lynx-v2-desde-cero.md +548 -0
  4. package/FETCH_INVESTIGATION.md +307 -0
  5. package/README.md +32 -302
  6. package/REQUEST.md +71 -0
  7. package/ROUTE.md +71 -0
  8. package/package.json +24 -80
  9. package/plugin.d.ts +4 -33
  10. package/plugin.js +108 -438
  11. package/rstest.config.ts +27 -0
  12. package/src/apply-patch.js +179 -0
  13. package/src/backends/virtual-backend.js +80 -0
  14. package/src/background.d.ts +11 -0
  15. package/src/background.js +79 -0
  16. package/src/channel.js +41 -0
  17. package/src/commit.js +67 -0
  18. package/src/dev-reload-client.js +171 -187
  19. package/src/dev-transport-noop.js +10 -0
  20. package/src/fake-dom.js +374 -0
  21. package/src/main-thread.d.ts +1 -0
  22. package/src/main-thread.js +68 -0
  23. package/src/mount-redraw.js +67 -0
  24. package/src/patch-protocol.js +40 -0
  25. package/src/reload/version.js +28 -0
  26. package/src/request.d.ts +37 -0
  27. package/src/request.js +181 -0
  28. package/src/route.d.ts +33 -0
  29. package/src/route.js +207 -0
  30. package/test/end-to-end.test.ts +86 -0
  31. package/test/reload-version.test.ts +17 -0
  32. package/test/request.test.ts +182 -0
  33. package/test/route-hot-reload.test.ts +40 -0
  34. package/test/route.test.ts +152 -0
  35. package/test/setup.ts +25 -0
  36. package/test/structural-reload.test.ts +95 -0
  37. package/CONTRACT.md +0 -151
  38. package/LICENSE +0 -21
  39. package/background.d.ts +0 -54
  40. package/background.js +0 -169
  41. package/element.d.ts +0 -34
  42. package/element.js +0 -83
  43. package/gesture.d.ts +0 -40
  44. package/gesture.js +0 -117
  45. package/internal/constants.js +0 -26
  46. package/internal/virtual-node.js +0 -388
  47. package/list.d.ts +0 -31
  48. package/list.js +0 -185
  49. package/main-thread.d.ts +0 -43
  50. package/main-thread.js +0 -165
  51. package/navigation.d.ts +0 -35
  52. package/navigation.js +0 -76
  53. package/renderer/background.d.ts +0 -21
  54. package/renderer/background.js +0 -84
  55. package/renderer/main-thread.d.ts +0 -12
  56. package/renderer/main-thread.js +0 -175
  57. package/src/lynx-mithril-shim.d.ts +0 -16
  58. package/src/lynx-mithril-shim.js +0 -1505
  59. package/src/worklet-runtime.js +0 -82
  60. package/testing.d.ts +0 -10
  61. package/testing.js +0 -91
@@ -1,388 +0,0 @@
1
- // internal/virtual-node.js
2
- //
3
- // Background-thread-only "virtual" mirror of LynxNodeWrapper/LynxStyleProxy
4
- // (src/lynx-mithril-shim.js). Implements the EXACT SAME duck-typed DOM
5
- // surface Mithril's render.js needs (per ../CONTRACT.md) — createElement,
6
- // appendChild, style, addEventListener, etc — but backed by a plain-JS
7
- // shadow tree instead of real Element PAPI calls, since the background
8
- // thread has no PAPI access at all.
9
- //
10
- // Every node gets a unique `vid` (vid 0 is reserved for the page root, by
11
- // convention shared with renderer/main-thread.js — no handshake needed).
12
- // Every WRITE emits a serializable op addressed by vid; ops describe the
13
- // SAME method calls a real LynxNodeWrapper would receive (createElement,
14
- // appendChild, setProp, setAttribute, setStyleProps, ...), so
15
- // renderer/main-thread.js's applyPatch() can replay them by calling those
16
- // exact methods on real LynxNodeWrapper instances — no PAPI-mapping logic
17
- // is duplicated here; the real wrapper's own (already-tested) setters do
18
- // that mapping on replay.
19
- //
20
- // Because the render algorithm (factory() in the shim) is UNMODIFIED and
21
- // only ever talks to `dom.*` methods generically, this file is a complete,
22
- // independent implementation of the wrapper contract — it does not import
23
- // or extend the real LynxNodeWrapper.
24
-
25
- "use strict"
26
-
27
- var DIRECT_PROPS = ["value", "checked", "selectedIndex", "className", "id", "type"]
28
-
29
- function camelize(str) {
30
- return str.replace(/-([a-z])/g, function (m, c) { return c.toUpperCase() })
31
- }
32
-
33
- // Mirrors the real LynxStyleProxy's registry exactly: render.js sometimes
34
- // writes a style property via plain camelCase assignment
35
- // (`element.style.fontSize = "20px"`) with no prior setProperty() call, so
36
- // no accessor exists yet to trigger a flush on write. Flushing every
37
- // registered proxy at the end of a render/redraw pass (mirroring the real
38
- // shim's flushTree(), see createVirtualDocument's returned flushStyleProxies)
39
- // is the safety net that catches those. The registry is scoped to one
40
- // createVirtualDocument() call (passed in as `register`), not module-level —
41
- // a real app only ever creates one document, but this keeps independent
42
- // virtual trees (as in tests) from leaking proxies into each other.
43
- function VirtualStyleProxy(wrapper, register) {
44
- this._wrapper = wrapper
45
- this._props = {}
46
- register(this)
47
- }
48
-
49
- VirtualStyleProxy.prototype._flush = function () {
50
- var styles = {}
51
- var keys = Object.keys(this)
52
- for (var i = 0; i < keys.length; i++) {
53
- var key = keys[i]
54
- if (key === "_wrapper" || key === "_props") continue
55
- var value = this[key]
56
- if (value == null) continue
57
- styles[camelize(key)] = String(value)
58
- }
59
- this._wrapper._emit({ op: "setStyleProps", vid: this._wrapper._vid, styles: styles })
60
- }
61
-
62
- VirtualStyleProxy.prototype.setProperty = function (name, value) {
63
- var key = camelize(name)
64
- if (value == null || value === "") {
65
- this.removeProperty(name)
66
- return
67
- }
68
- var self = this
69
- if (!Object.prototype.hasOwnProperty.call(this, key)) {
70
- Object.defineProperty(this, key, {
71
- configurable: true,
72
- enumerable: true,
73
- get: function () { return self._props[key] },
74
- set: function (v) {
75
- if (v == null || v === "") delete self._props[key]
76
- else self._props[key] = String(v)
77
- self._flush()
78
- }
79
- })
80
- }
81
- this._props[key] = String(value)
82
- this._flush()
83
- }
84
-
85
- VirtualStyleProxy.prototype.removeProperty = function (name) {
86
- var key = camelize(name)
87
- delete this._props[key]
88
- if (Object.prototype.hasOwnProperty.call(this, key)) delete this[key]
89
- this._flush()
90
- }
91
-
92
- Object.defineProperty(VirtualStyleProxy.prototype, "cssText", {
93
- get: function () {
94
- var out = []
95
- for (var key in this._props) out.push(key + ":" + this._props[key])
96
- return out.join(";")
97
- },
98
- set: function (value) {
99
- var self = this
100
- Object.keys(this).forEach(function (key) {
101
- if (key !== "_wrapper" && key !== "_props") delete self[key]
102
- })
103
- this._props = {}
104
- if (value) {
105
- // Only used for the shim's first-render `dom.style = ""` clear path
106
- // in practice — a non-empty string assignment isn't exercised by
107
- // render.js itself (see CONTRACT.md §f), so no parsing is needed here.
108
- this._flush()
109
- }
110
- }
111
- })
112
-
113
- function VirtualNodeWrapper(vid, emit, registerStyleProxy) {
114
- this._vid = vid
115
- this._emit = emit
116
- this._style = new VirtualStyleProxy(this, registerStyleProxy)
117
- this._directProps = {}
118
- this._listeners = Object.create(null)
119
- this._isRawText = false
120
- this._text = null
121
- this._document = null
122
- this._tag = null
123
- this._parent = null
124
- this._children = []
125
- this.vnodes = null
126
- }
127
-
128
- Object.defineProperty(VirtualNodeWrapper.prototype, "nodeType", {
129
- get: function () { return this._isRawText ? 3 : 1 }
130
- })
131
-
132
- Object.defineProperty(VirtualNodeWrapper.prototype, "ownerDocument", {
133
- get: function () { return this._document }
134
- })
135
-
136
- Object.defineProperty(VirtualNodeWrapper.prototype, "namespaceURI", {
137
- get: function () { return undefined }
138
- })
139
-
140
- Object.defineProperty(VirtualNodeWrapper.prototype, "parentNode", {
141
- get: function () { return this._parent }
142
- })
143
-
144
- Object.defineProperty(VirtualNodeWrapper.prototype, "firstChild", {
145
- get: function () { return this._children.length > 0 ? this._children[0] : null }
146
- })
147
-
148
- Object.defineProperty(VirtualNodeWrapper.prototype, "nextSibling", {
149
- get: function () {
150
- if (this._parent == null) return null
151
- var siblings = this._parent._children
152
- var index = siblings.indexOf(this)
153
- return index >= 0 && index + 1 < siblings.length ? siblings[index + 1] : null
154
- }
155
- })
156
-
157
- Object.defineProperty(VirtualNodeWrapper.prototype, "textContent", {
158
- get: function () {
159
- if (this._isRawText) return this._text
160
- var out = ""
161
- for (var i = 0; i < this._children.length; i++) out += this._children[i].textContent
162
- return out
163
- },
164
- set: function (value) {
165
- if (this._isRawText) {
166
- this.nodeValue = value
167
- return
168
- }
169
- // Mirrors the real wrapper: only ever called with "" (first-render
170
- // clear). A genuinely non-empty assignment isn't exercised by
171
- // render.js (see CONTRACT.md) so it's intentionally not implemented.
172
- if (this._children.length > 0) {
173
- var removed = this._children.slice()
174
- for (var i = 0; i < removed.length; i++) this.removeChild(removed[i])
175
- }
176
- }
177
- })
178
-
179
- Object.defineProperty(VirtualNodeWrapper.prototype, "nodeValue", {
180
- get: function () { return this._isRawText ? this._text : null },
181
- set: function (value) {
182
- if (this._isRawText) {
183
- this._text = String(value)
184
- this._emit({ op: "setText", vid: this._vid, value: this._text })
185
- }
186
- }
187
- })
188
-
189
- Object.defineProperty(VirtualNodeWrapper.prototype, "innerHTML", {
190
- get: function () { return this.textContent },
191
- set: function () {
192
- throw new Error("m.trust / innerHTML is not supported by the Lynx shim (renderer mode).")
193
- }
194
- })
195
-
196
- Object.defineProperty(VirtualNodeWrapper.prototype, "style", {
197
- get: function () { return this._style },
198
- set: function (value) {
199
- if (value == null) this._style.cssText = ""
200
- else if (typeof value === "string") this._style.cssText = value
201
- }
202
- })
203
-
204
- DIRECT_PROPS.forEach(function (p) {
205
- Object.defineProperty(VirtualNodeWrapper.prototype, p, {
206
- configurable: true,
207
- enumerable: true,
208
- get: function () { return this._directProps[p] },
209
- set: function (v) {
210
- this._directProps[p] = v
211
- this._emit({ op: "setProp", vid: this._vid, key: p, value: v })
212
- }
213
- })
214
- })
215
-
216
- VirtualNodeWrapper.prototype.setAttribute = function (key, value) {
217
- this._emit({ op: "setAttribute", vid: this._vid, key: key, value: value == null ? null : String(value) })
218
- }
219
-
220
- VirtualNodeWrapper.prototype.removeAttribute = function (key) {
221
- this._emit({ op: "removeAttribute", vid: this._vid, key: key })
222
- }
223
-
224
- VirtualNodeWrapper.prototype.setAttributeNS = function (ns, key, value) {
225
- this.setAttribute(key, value)
226
- }
227
-
228
- VirtualNodeWrapper.prototype.appendChild = function (child) {
229
- if (child == null) return child
230
- if (child.nodeType === 11) return child // inert fragment
231
- if (child._parent != null) {
232
- var oldSiblings = child._parent._children
233
- var oldIndex = oldSiblings.indexOf(child)
234
- if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
235
- }
236
- this._children.push(child)
237
- child._parent = this
238
- this._emit({ op: "appendChild", parentVid: this._vid, childVid: child._vid })
239
- return child
240
- }
241
-
242
- VirtualNodeWrapper.prototype.insertBefore = function (child, ref) {
243
- if (child == null) return child
244
- if (child.nodeType === 11) return child
245
- if (ref == null) return this.appendChild(child)
246
- if (child._parent != null) {
247
- var oldSiblings = child._parent._children
248
- var oldIndex = oldSiblings.indexOf(child)
249
- if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
250
- }
251
- var refIndex = this._children.indexOf(ref)
252
- this._children.splice(refIndex >= 0 ? refIndex : this._children.length, 0, child)
253
- child._parent = this
254
- this._emit({ op: "insertBefore", parentVid: this._vid, childVid: child._vid, refVid: ref._vid })
255
- return child
256
- }
257
-
258
- VirtualNodeWrapper.prototype.removeChild = function (child) {
259
- if (child == null) return child
260
- if (child.nodeType === 11) return child
261
- var index = this._children.indexOf(child)
262
- if (index >= 0) this._children.splice(index, 1)
263
- child._parent = null
264
- this._emit({ op: "removeChild", parentVid: this._vid, childVid: child._vid })
265
- return child
266
- }
267
-
268
- VirtualNodeWrapper.prototype.contains = function (other) {
269
- if (other == null) return false
270
- var node = other
271
- while (node != null) {
272
- if (node === this) return true
273
- node = node._parent
274
- }
275
- return false
276
- }
277
-
278
- VirtualNodeWrapper.prototype.focus = function () {}
279
-
280
- // Events: unlike the real wrapper (which wraps + registers a real PAPI
281
- // listener per type), this only needs to know WHETHER a type transitions
282
- // between "has a handler" and "has none" — that's what the main thread
283
- // needs to know to attach/detach its forwarding listener. The actual
284
- // listener function/object is resolved by dispatchEvent() at CALL time, not
285
- // registration time, so Mithril swapping vnode.events' handlers across
286
- // redraws (a very common pattern) never needs a new op.
287
- VirtualNodeWrapper.prototype.addEventListener = function (type, listener) {
288
- var hadListener = this._listeners[type] != null
289
- this._listeners[type] = listener
290
- if (!hadListener) this._emit({ op: "addEvent", vid: this._vid, type: type })
291
- }
292
-
293
- VirtualNodeWrapper.prototype.removeEventListener = function (type, listener) {
294
- if (this._listeners[type] !== listener) return
295
- delete this._listeners[type]
296
- this._emit({ op: "removeEvent", vid: this._vid, type: type })
297
- }
298
-
299
- // Invoked when renderer/background.js receives a forwarded real-PAPI event.
300
- VirtualNodeWrapper.prototype.dispatchEvent = function (ev) {
301
- var listener = this._listeners[ev.type]
302
- if (listener == null) return
303
- if (typeof listener === "function") listener.call(ev.currentTarget, ev)
304
- else if (typeof listener.handleEvent === "function") listener.handleEvent(ev)
305
- }
306
-
307
- function createRawTextNode(vid, emit, registerStyleProxy, value) {
308
- var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
309
- wrapper._isRawText = true
310
- wrapper._text = String(value)
311
- emit({ op: "createText", vid: vid, value: wrapper._text })
312
- return wrapper
313
- }
314
-
315
- function createElementWrapper(vid, emit, registerStyleProxy, tag) {
316
- var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
317
- wrapper._tag = tag
318
- emit({ op: "createElement", vid: vid, tag: tag })
319
- return wrapper
320
- }
321
-
322
- // Inert fragment — same "never attached, children go straight to the real
323
- // parent" contract as the real shim's createFragment().
324
- function createFragment(document) {
325
- return {
326
- nodeType: 11,
327
- ownerDocument: document,
328
- appendChild: function () {},
329
- insertBefore: function () {},
330
- removeChild: function () {}
331
- }
332
- }
333
-
334
- // createVirtualDocument(emit, onCreateNode) — emit(op) is called
335
- // synchronously for every mutation; the caller (renderer/background.js) is
336
- // responsible for batching and flushing the accumulated ops across the
337
- // thread boundary. onCreateNode(wrapper) is called for every node created
338
- // (root included) so the caller can index wrappers by vid. Each call gets
339
- // its own independent style-proxy registry and vid counter — see
340
- // VirtualStyleProxy's comment for why this isn't module-level.
341
- function createVirtualDocument(emit, onCreateNode) {
342
- var nextVid = 1 // vid 0 is reserved for the page root.
343
- var notify = onCreateNode || function () {}
344
- var styleProxies = []
345
- var registerStyleProxy = function (proxy) { styleProxies.push(proxy) }
346
-
347
- var document = {
348
- createElement: function (tag) {
349
- var wrapper = createElementWrapper(nextVid++, emit, registerStyleProxy, tag)
350
- wrapper._document = document
351
- notify(wrapper)
352
- return wrapper
353
- },
354
- createElementNS: function (ns, tag) {
355
- return document.createElement(tag)
356
- },
357
- createTextNode: function (value) {
358
- var wrapper = createRawTextNode(nextVid++, emit, registerStyleProxy, value)
359
- wrapper._document = document
360
- notify(wrapper)
361
- return wrapper
362
- },
363
- createDocumentFragment: function () {
364
- return createFragment(document)
365
- }
366
- }
367
- Object.defineProperty(document, "activeElement", {
368
- get: function () { return null }
369
- })
370
-
371
- function createRootWrapper() {
372
- var root = new VirtualNodeWrapper(0, emit, registerStyleProxy)
373
- root._document = document
374
- root._tag = "page"
375
- notify(root)
376
- return root
377
- }
378
-
379
- function flushStyleProxies() {
380
- for (var i = 0; i < styleProxies.length; i++) {
381
- try { styleProxies[i]._flush() } catch (e) { /* ignore */ }
382
- }
383
- }
384
-
385
- return { document: document, createRootWrapper: createRootWrapper, flushStyleProxies: flushStyleProxies }
386
- }
387
-
388
- export { createVirtualDocument, VirtualNodeWrapper, VirtualStyleProxy }
package/list.d.ts DELETED
@@ -1,31 +0,0 @@
1
- // Ambient declaration for the ESM list.js (the file itself is not
2
- // type-checked; this describes its runtime export shape for TS consumers).
3
-
4
- export interface CreateListOptions {
5
- itemCount: number;
6
- /** Must return a fresh vnode for the cell's content; may be called more than once for the same index (recycling). */
7
- renderItem(index: number): unknown;
8
- /** Defaults to String(index). Set on every item's "item-key" attribute — required by native, not just an identity hint. */
9
- itemKey?(index: number): string;
10
- className?: string;
11
- /** Defaults to "vertical". */
12
- scrollOrientation?: "vertical" | "horizontal";
13
- /** Defaults to "single". */
14
- listType?: "single" | "flow";
15
- /** Defaults to 1. */
16
- spanCount?: number;
17
- }
18
-
19
- export interface ListHandle {
20
- _handle: unknown;
21
- nodeType: 1;
22
- /** Call after mutating the underlying data so a subsequently-scrolled-into-view cell reflects the new count. */
23
- setItemCount(nextCount: number): void;
24
- }
25
-
26
- /**
27
- * Creates a native-recycled `<list>` element (project plan, Phase 8, Tier
28
- * 2). Imperative escape hatch — call from oncreate(vnode) and attach the
29
- * result yourself (parentNode.appendChild(list)).
30
- */
31
- export function createList(parentNode: { ownerDocument: unknown }, options: CreateListOptions): ListHandle;
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>;