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.
Files changed (46) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/FETCH_INVESTIGATION.md +3 -2
  3. package/INPUT.md +115 -0
  4. package/README.md +47 -8
  5. package/ROUTE.md +111 -16
  6. package/ROUTE_CONTRACT_ANALYSIS.md +224 -0
  7. package/UPDATE_ANIMATION_GAP.md +133 -0
  8. package/package.json +1 -9
  9. package/plugin.js +33 -0
  10. package/src/apply-patch.js +369 -60
  11. package/src/backends/virtual-backend.js +120 -22
  12. package/src/background.d.ts +26 -0
  13. package/src/background.js +116 -7
  14. package/src/channel.js +22 -5
  15. package/src/commit.js +9 -0
  16. package/src/dev-reload-client.js +46 -0
  17. package/src/dev-transport-noop.js +3 -0
  18. package/src/fake-dom.js +559 -61
  19. package/src/list-attributes.js +69 -0
  20. package/src/list-runtime.js +425 -0
  21. package/src/main-thread.js +18 -1
  22. package/src/mount-redraw.js +14 -1
  23. package/src/patch-protocol.js +41 -16
  24. package/src/reload/version.js +10 -1
  25. package/src/request.js +38 -0
  26. package/src/route.d.ts +30 -2
  27. package/src/route.js +214 -56
  28. package/src/testing.d.ts +14 -1
  29. package/src/testing.js +56 -1
  30. package/test/event-render-coalescing.test.ts +117 -0
  31. package/test/gesture.test.ts +75 -0
  32. package/test/input.test.ts +388 -0
  33. package/test/list.test.ts +417 -113
  34. package/test/remove-subtree-cleanup.test.ts +75 -0
  35. package/test/route-hot-reload.test.ts +2 -1
  36. package/test/route.test.ts +201 -4
  37. package/test/style-clear.test.ts +97 -0
  38. package/.omo/plans/m-request-fetch-lynx.md +0 -306
  39. package/.omo/plans/m-route-en-memoria.md +0 -397
  40. package/.omo/plans/mithril-lynx-v2-desde-cero.md +0 -548
  41. package/.omo/run-continuation/ses_f2f02336cffeO0ssvuxqZ05iZq.json +0 -10
  42. package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.json +0 -10
  43. package/src/list-cell.d.ts +0 -20
  44. package/src/list-cell.js +0 -73
  45. package/src/list-support.d.ts +0 -13
  46. package/src/list-support.js +0 -197
@@ -1,13 +0,0 @@
1
- // Ambient declaration for the ESM src/list-support.js — the main-thread
2
- // half of native list support. Not meant to be imported directly by app
3
- // code; apply-patch.js's own Op.CreateList case is the only caller. See
4
- // list-cell.d.ts for the background-thread half an app actually uses.
5
-
6
- export function createNativeList(
7
- pageId: number,
8
- scrollOrientation: string,
9
- listType: string,
10
- spanCount: number,
11
- createPatchApplier: (pageId: number, options?: { onEvent?: Function; flush?: boolean }) => object,
12
- onEvent?: Function,
13
- ): { handle: unknown; setCells: (cells: unknown[]) => void };
@@ -1,197 +0,0 @@
1
- // src/list-support.js
2
- //
3
- // Runs ONLY on the main thread — the counterpart to list-cell.js's
4
- // background-thread half. Wires up the native list contract
5
- // (__CreateList/__UpdateListCallbacks, the synchronous componentAtIndex/
6
- // enqueueComponent pair, recycling cells by a type key) — unchanged from
7
- // mithril-lynx v1's own device-verified list.js.
8
- //
9
- // What's different from v1, and from this file's own first version: a
10
- // cell's content is never rendered here. `componentAtIndex` only replays
11
- // ops the background thread already computed (list-cell.js), the same way
12
- // apply-patch.js's own top-level applyPatch replays the app's own tree —
13
- // this file has no dependency on mithril-runtime, fake-dom.js, or a virtual
14
- // backend at all anymore, because it never runs a render pass of its own.
15
- // componentAtIndex(itemCount) cost is therefore the cost of creating/
16
- // updating real elements from an already-known op list, same as any other
17
- // patch — not a render.
18
- //
19
- // Recycling a cell for a different item clears its real children (removed
20
- // by the handle ids list-cell.js recorded — see clearWrapperChildren below)
21
- // and replays the new item's ops fresh; this is a real teardown-and-rebuild
22
- // of that cell's content, not a diff against what was there before. A real
23
- // Mithril diff on recycle would need a persistent per-slot fake-dom
24
- // document kept in sync with which native cell native actually chose to
25
- // reuse — information only native has, on the main thread, which the
26
- // background thread (where the diff would have to run) can't see without a
27
- // round trip. Not implemented.
28
-
29
- export function createNativeList(pageId, scrollOrientation, listType, spanCount, createPatchApplier, onEvent) {
30
- let cells = []; // current cells, indexed by cellIndex — see patch-protocol.js's Op.SetListItems
31
- let count = 0;
32
- const signMap = new Map(); // sign -> { wrapperHandle, applier, rootChildIds, cellIndex, typeKey }
33
- const recycleMap = new Map(); // typeKey -> Map<sign, entry> (entries currently off-screen, available to reuse)
34
-
35
- function replayCell(wrapperHandle, cell) {
36
- const applier = createPatchApplier(pageId, { onEvent, flush: false });
37
- applier.registerRoot(cell.containerId, wrapperHandle);
38
- applier.applyPatch(cell.ops);
39
- return applier;
40
- }
41
-
42
- function clearWrapperChildren(entry) {
43
- for (const id of entry.rootChildIds) {
44
- const handle = entry.applier.getHandle(id);
45
- if (handle != null) __RemoveElement(entry.wrapperHandle, handle);
46
- }
47
- }
48
-
49
- /** The wrapper of the lowest currently-attached cellIndex greater than
50
- * `cellIndex`, or null if none — the DOM position a cell at `cellIndex`
51
- * needs to be inserted before to land in the right place. Native lays
52
- * cells out in document-tree order, not by `item-key`, so a recycled
53
- * wrapper that keeps its OLD tree position (this file's own first
54
- * version never repositioned it at all) shows up out of order the
55
- * moment it's reused for a different index — confirmed on real
56
- * hardware via a scrambled child order after scrolling (item-keys
57
- * 7,8,9,3,4,5,6). Same problem `@lynx-js/react`'s own
58
- * `attachListItemAtIndex`/`findNextAttachedItem` solves for its
59
- * Element Template list. */
60
- function findNextAttachedWrapper(cellIndex) {
61
- let best = null;
62
- for (const entry of signMap.values()) {
63
- if (entry.cellIndex > cellIndex && (best == null || entry.cellIndex < best.cellIndex)) best = entry;
64
- }
65
- return best ? best.wrapperHandle : null;
66
- }
67
-
68
- function attachWrapper(listHandle, wrapperHandle, cellIndex) {
69
- const refHandle = findNextAttachedWrapper(cellIndex);
70
- if (refHandle != null) __InsertElementBefore(listHandle, wrapperHandle, refHandle);
71
- else __AppendElement(listHandle, wrapperHandle);
72
- }
73
-
74
- function bindFreshItem(listHandle, listId, cellIndex, opId, cell, flush) {
75
- const wrapperHandle = __CreateElement("list-item", pageId, {});
76
- __SetAttribute(wrapperHandle, "item-key", String(cellIndex));
77
- attachWrapper(listHandle, wrapperHandle, cellIndex);
78
-
79
- const applier = replayCell(wrapperHandle, cell);
80
- const sign = __GetElementUniqueID(wrapperHandle);
81
- signMap.set(sign, { wrapperHandle, applier, rootChildIds: cell.rootChildIds, cellIndex, typeKey: cell.typeKey });
82
- if (flush) __FlushElementTree(wrapperHandle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
83
- return sign;
84
- }
85
-
86
- function bindRecycledItem(listHandle, listId, cellIndex, opId, cell, pool, flush) {
87
- const [sign, entry] = pool.entries().next().value;
88
- pool.delete(sign);
89
- clearWrapperChildren(entry);
90
- __SetAttribute(entry.wrapperHandle, "item-key", String(cellIndex));
91
- attachWrapper(listHandle, entry.wrapperHandle, cellIndex);
92
- const applier = replayCell(entry.wrapperHandle, cell);
93
- entry.applier = applier;
94
- entry.rootChildIds = cell.rootChildIds;
95
- entry.cellIndex = cellIndex;
96
- signMap.set(sign, entry);
97
- if (flush) __FlushElementTree(entry.wrapperHandle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
98
- return sign;
99
- }
100
-
101
- function bindCell(listHandle, listId, cellIndex, opId, flush) {
102
- if (cellIndex < 0 || cellIndex >= count) {
103
- throw new Error(`[mithril-lynx] list: cellIndex ${cellIndex} out of range (itemCount=${count})`);
104
- }
105
- const cell = cells[cellIndex];
106
- const pool = recycleMap.get(cell.typeKey);
107
- if (pool && pool.size > 0) return bindRecycledItem(listHandle, listId, cellIndex, opId, cell, pool, flush);
108
- return bindFreshItem(listHandle, listId, cellIndex, opId, cell, flush);
109
- }
110
-
111
- function componentAtIndex(listHandle, listId, cellIndex, opId) {
112
- return bindCell(listHandle, listId, cellIndex, opId, true);
113
- }
114
-
115
- /**
116
- * The batched form of componentAtIndex — real native calls this (not a
117
- * fallback: confirmed against `@lynx-js/react`'s own
118
- * componentAtIndexFactory/attachListItemAtIndex, which always registers
119
- * BOTH forms) for several cells at once, expecting exactly ONE
120
- * `__FlushElementTree` covering all of them (`operationIDs`/`elementIDs`
121
- * arrays), not one per cell — the real reason a native list's initial,
122
- * simultaneously-visible cells need this at all.
123
- */
124
- function componentAtIndexes(listHandle, listId, cellIndexes, operationIDs) {
125
- const elementIDs = cellIndexes.map((cellIndex, i) => bindCell(listHandle, listId, cellIndex, operationIDs[i], false));
126
- __FlushElementTree(listHandle, { triggerLayout: true, operationIDs, elementIDs, listID: listId });
127
- }
128
-
129
- function enqueueComponent(_listHandle, _listId, sign) {
130
- const entry = signMap.get(sign);
131
- if (entry == null) return;
132
- signMap.delete(sign);
133
- if (!recycleMap.has(entry.typeKey)) recycleMap.set(entry.typeKey, new Map());
134
- recycleMap.get(entry.typeKey).set(sign, entry);
135
- }
136
-
137
- function sendListInfo(listHandle, listId, insertAction, removeAction, updateAction) {
138
- __SetAttribute(listHandle, "update-list-info", { insertAction, removeAction, updateAction });
139
- __UpdateListCallbacks(listHandle, componentAtIndex, enqueueComponent, componentAtIndexes);
140
- }
141
-
142
- const listHandle = __CreateList(pageId, componentAtIndex, enqueueComponent, {}, componentAtIndexes);
143
- const listId = __GetElementUniqueID(listHandle);
144
- __SetAttribute(listHandle, "scroll-orientation", scrollOrientation);
145
- __SetAttribute(listHandle, "list-type", listType);
146
- __SetAttribute(listHandle, "span-count", String(spanCount));
147
-
148
- // Re-flushes every currently-attached (on-screen) cell with its newest
149
- // ops — e.g. after a redraw triggered by a tap inside a cell, or any
150
- // other app state change that reaches this list's items. Unconditional,
151
- // same as List's own onupdate already unconditionally resends the whole
152
- // `items` array on every relevant redraw (see mithril-lynx-ui's
153
- // list/list.js) — a real "did this cell's content actually change"
154
- // check would need to compare `ops` arrays, not implemented yet. Cells
155
- // that aren't attached right now just pick up their new content next
156
- // time componentAtIndex asks for that index.
157
- function refreshAttachedCells(nextCells) {
158
- for (const [sign, entry] of signMap) {
159
- const nextCell = nextCells[entry.cellIndex];
160
- clearWrapperChildren(entry);
161
- entry.applier = replayCell(entry.wrapperHandle, nextCell);
162
- entry.rootChildIds = nextCell.rootChildIds;
163
- entry.typeKey = nextCell.typeKey;
164
- signMap.set(sign, entry);
165
- __FlushElementTree(entry.wrapperHandle, { triggerLayout: false });
166
- }
167
- }
168
-
169
- function setCells(nextCells) {
170
- const nextCount = nextCells.length;
171
- refreshAttachedCells(nextCells);
172
- cells = nextCells;
173
- if (nextCount === count) return;
174
- if (nextCount > count) {
175
- sendListInfo(
176
- listHandle,
177
- listId,
178
- Array.from({ length: nextCount - count }, (_, i) => ({ position: count + i, type: "cell", "item-key": String(count + i) })),
179
- [],
180
- [],
181
- );
182
- } else {
183
- sendListInfo(listHandle, listId, [], Array.from({ length: count - nextCount }, (_, i) => nextCount + i), []);
184
- }
185
- count = nextCount;
186
- }
187
-
188
- // Returned separately, not as a property stuck onto `listHandle` itself:
189
- // confirmed on real hardware (unlike @lynx-js/testing-environment's
190
- // simulated PAPI) that a native list's own returned handle does not
191
- // reliably hold a custom property across calls — assigning one there
192
- // and reading it back later from apply-patch.js's Op.SetListItems case
193
- // threw "TypeError: not a function" on the very first SetListItems, even
194
- // with zero items. apply-patch.js keeps `setCells` in its own map
195
- // instead, keyed the same way `handles` already is.
196
- return { handle: listHandle, setCells };
197
- }