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.
- 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 -302
- package/REQUEST.md +71 -0
- package/ROUTE.md +71 -0
- package/package.json +24 -80
- package/plugin.d.ts +4 -33
- package/plugin.js +108 -438
- 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 +171 -187
- 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/internal/virtual-node.js
DELETED
|
@@ -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>;
|