@pyreon/runtime-dom 0.13.1 → 0.14.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/README.md +23 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/keep-alive-entry.js.html +5406 -0
- package/lib/analysis/transition-entry.js.html +5406 -0
- package/lib/index.js +98 -47
- package/lib/index.js.map +1 -1
- package/lib/keep-alive-entry.js +1341 -0
- package/lib/keep-alive-entry.js.map +1 -0
- package/lib/transition-entry.js +167 -0
- package/lib/transition-entry.js.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/keep-alive-entry.d.ts +41 -0
- package/lib/types/keep-alive-entry.d.ts.map +1 -0
- package/lib/types/transition-entry.d.ts +59 -0
- package/lib/types/transition-entry.d.ts.map +1 -0
- package/package.json +16 -6
- package/src/hydrate.ts +14 -12
- package/src/index.ts +19 -3
- package/src/keep-alive-entry.ts +3 -0
- package/src/mount.ts +159 -54
- package/src/nodes.ts +61 -11
- package/src/template.ts +13 -2
- package/src/tests/coverage-gaps.test.ts +709 -0
- package/src/tests/lis-prepend.browser.test.ts +99 -0
- package/src/tests/runtime-dom.browser.test.ts +63 -1
- package/src/tests/template.test.ts +64 -0
- package/src/transition-entry.ts +7 -0
package/src/hydrate.ts
CHANGED
|
@@ -384,8 +384,8 @@ function hydrateComponent(
|
|
|
384
384
|
const { vnode: output, hooks } = result
|
|
385
385
|
|
|
386
386
|
// Register onUpdate hooks with the scope
|
|
387
|
-
|
|
388
|
-
scope.addUpdateHook(fn)
|
|
387
|
+
if (hooks.update) {
|
|
388
|
+
for (const fn of hooks.update) scope.addUpdateHook(fn)
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
if (output != null) {
|
|
@@ -395,22 +395,24 @@ function hydrateComponent(
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// Fire onMount hooks; effects created inside are tracked by the scope via runInScope
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
398
|
+
if (hooks.mount) {
|
|
399
|
+
for (const fn of hooks.mount) {
|
|
400
|
+
try {
|
|
401
|
+
let c: (() => void) | undefined
|
|
402
|
+
scope.runInScope(() => {
|
|
403
|
+
c = fn() as (() => void) | undefined
|
|
404
|
+
})
|
|
405
|
+
if (c) mountCleanups.push(c)
|
|
406
|
+
} catch (err) {
|
|
407
|
+
reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
|
|
408
|
+
}
|
|
407
409
|
}
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
const cleanup: Cleanup = () => {
|
|
411
413
|
scope.stop()
|
|
412
414
|
subtreeCleanup()
|
|
413
|
-
for (const fn of hooks.unmount) fn()
|
|
415
|
+
if (hooks.unmount) for (const fn of hooks.unmount) fn()
|
|
414
416
|
for (const fn of mountCleanups) fn()
|
|
415
417
|
}
|
|
416
418
|
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,13 @@ export type { KeepAliveProps } from './keep-alive'
|
|
|
8
8
|
export { KeepAlive } from './keep-alive'
|
|
9
9
|
export { mountChild } from './mount'
|
|
10
10
|
export type { SanitizeFn } from './props'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
applyProp,
|
|
13
|
+
applyProps,
|
|
14
|
+
applyProps as _applyProps,
|
|
15
|
+
sanitizeHtml,
|
|
16
|
+
setSanitizer,
|
|
17
|
+
} from './props'
|
|
12
18
|
export { _bindDirect, _bindText, _mountSlot, _tpl, createTemplate } from './template'
|
|
13
19
|
export type { TransitionProps } from './transition'
|
|
14
20
|
export { Transition } from './transition'
|
|
@@ -25,6 +31,9 @@ import { mountChild } from './mount'
|
|
|
25
31
|
// @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time
|
|
26
32
|
const __DEV__ = import.meta.env?.DEV === true
|
|
27
33
|
|
|
34
|
+
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
35
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
36
|
+
|
|
28
37
|
/**
|
|
29
38
|
* Mount a VNode tree into a container element.
|
|
30
39
|
* Clears the container first, then mounts the given child.
|
|
@@ -39,10 +48,17 @@ export function mount(root: VNodeChild, container: Element): () => void {
|
|
|
39
48
|
'[pyreon] mount() called with a null/undefined container. Make sure the element exists in the DOM, e.g. document.getElementById("app")',
|
|
40
49
|
)
|
|
41
50
|
}
|
|
42
|
-
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
_countSink.__pyreon_count__?.('runtime.mount')
|
|
53
|
+
installDevTools()
|
|
54
|
+
}
|
|
43
55
|
setupDelegation(container)
|
|
44
56
|
container.innerHTML = ''
|
|
45
|
-
|
|
57
|
+
const unmount = mountChild(root, container, null)
|
|
58
|
+
return () => {
|
|
59
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime.unmount')
|
|
60
|
+
unmount()
|
|
61
|
+
}
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/** Alias for `mount` */
|
package/src/mount.ts
CHANGED
|
@@ -28,6 +28,9 @@ import { applyProps } from './props'
|
|
|
28
28
|
// @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time
|
|
29
29
|
const __DEV__ = import.meta.env?.DEV === true
|
|
30
30
|
|
|
31
|
+
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
32
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
33
|
+
|
|
31
34
|
type Cleanup = () => void
|
|
32
35
|
const noop: Cleanup = () => {
|
|
33
36
|
/* noop */
|
|
@@ -40,7 +43,9 @@ let _elementDepth = 0
|
|
|
40
43
|
|
|
41
44
|
// Stack tracking which component is currently being mounted (depth-first order).
|
|
42
45
|
// Used to infer parent/child relationships for DevTools.
|
|
43
|
-
|
|
46
|
+
// Only allocated in dev — production mounts skip devtools entirely.
|
|
47
|
+
let _mountingStack: string[] | undefined
|
|
48
|
+
if (__DEV__) _mountingStack = []
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
51
|
* Mount a single child into `parent`, inserting before `anchor` (null = append).
|
|
@@ -54,6 +59,7 @@ export function mountChild(
|
|
|
54
59
|
parent: Node,
|
|
55
60
|
anchor: Node | null = null,
|
|
56
61
|
): Cleanup {
|
|
62
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime.mountChild')
|
|
57
63
|
// Reactive accessor — function that reads signals
|
|
58
64
|
if (typeof child === 'function') {
|
|
59
65
|
const sample = runUntracked(() => (child as () => VNodeChild | VNodeChild[])())
|
|
@@ -72,7 +78,8 @@ export function mountChild(
|
|
|
72
78
|
parent.insertBefore(text, anchor)
|
|
73
79
|
const dispose = renderEffect(() => {
|
|
74
80
|
const v = (child as () => unknown)()
|
|
75
|
-
|
|
81
|
+
const next = v == null || v === false ? '' : String(v as string | number)
|
|
82
|
+
if (next !== text.data) text.data = next
|
|
76
83
|
})
|
|
77
84
|
if (_elementDepth > 0) return dispose
|
|
78
85
|
return () => {
|
|
@@ -125,10 +132,30 @@ export function mountChild(
|
|
|
125
132
|
if (vnode.type === Fragment) return mountChildren(vnode.children ?? [], parent, anchor)
|
|
126
133
|
|
|
127
134
|
if (vnode.type === (ForSymbol as unknown as string)) {
|
|
128
|
-
|
|
135
|
+
// Compiler wraps `<For each={signal}>` in `_rp(() => signal())` →
|
|
136
|
+
// `props.each` is a getter that returns the resolved array, not the
|
|
137
|
+
// function. Destructuring eagerly captures the array and breaks
|
|
138
|
+
// reactivity + crashes mountFor (which calls `source()`). Read each
|
|
139
|
+
// lazily so the _rp getter fires inside mountFor's effect, preserving
|
|
140
|
+
// signal tracking. User-written `each={() => fn()}` (already a
|
|
141
|
+
// function, not _rp-wrapped) still works because props.each is the
|
|
142
|
+
// function itself.
|
|
143
|
+
const props = vnode.props as unknown as ForProps<unknown>
|
|
144
|
+
const initialEach = props.each as unknown
|
|
145
|
+
const source: () => unknown[] =
|
|
146
|
+
typeof initialEach === 'function'
|
|
147
|
+
? (initialEach as () => unknown[])
|
|
148
|
+
: (() => props.each as unknown as unknown[])
|
|
129
149
|
const prevDepth = _elementDepth
|
|
130
150
|
_elementDepth = 0
|
|
131
|
-
const cleanup = mountFor(
|
|
151
|
+
const cleanup = mountFor(
|
|
152
|
+
source as () => unknown[],
|
|
153
|
+
props.by,
|
|
154
|
+
props.children,
|
|
155
|
+
parent,
|
|
156
|
+
anchor,
|
|
157
|
+
mountChild,
|
|
158
|
+
)
|
|
132
159
|
_elementDepth = prevDepth
|
|
133
160
|
return cleanup
|
|
134
161
|
}
|
|
@@ -189,21 +216,81 @@ const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
|
|
|
189
216
|
|
|
190
217
|
// Tags that require namespace-aware creation
|
|
191
218
|
const SVG_TAGS = new Set([
|
|
192
|
-
'svg',
|
|
193
|
-
'
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'
|
|
200
|
-
'
|
|
219
|
+
'svg',
|
|
220
|
+
'circle',
|
|
221
|
+
'ellipse',
|
|
222
|
+
'line',
|
|
223
|
+
'path',
|
|
224
|
+
'polygon',
|
|
225
|
+
'polyline',
|
|
226
|
+
'rect',
|
|
227
|
+
'g',
|
|
228
|
+
'defs',
|
|
229
|
+
'symbol',
|
|
230
|
+
'use',
|
|
231
|
+
'text',
|
|
232
|
+
'tspan',
|
|
233
|
+
'textPath',
|
|
234
|
+
'image',
|
|
235
|
+
'clipPath',
|
|
236
|
+
'mask',
|
|
237
|
+
'pattern',
|
|
238
|
+
'marker',
|
|
239
|
+
'linearGradient',
|
|
240
|
+
'radialGradient',
|
|
241
|
+
'stop',
|
|
242
|
+
'filter',
|
|
243
|
+
'feBlend',
|
|
244
|
+
'feColorMatrix',
|
|
245
|
+
'feComponentTransfer',
|
|
246
|
+
'feComposite',
|
|
247
|
+
'feConvolveMatrix',
|
|
248
|
+
'feDiffuseLighting',
|
|
249
|
+
'feDisplacementMap',
|
|
250
|
+
'feFlood',
|
|
251
|
+
'feGaussianBlur',
|
|
252
|
+
'feImage',
|
|
253
|
+
'feMerge',
|
|
254
|
+
'feMergeNode',
|
|
255
|
+
'feMorphology',
|
|
256
|
+
'feOffset',
|
|
257
|
+
'feSpecularLighting',
|
|
258
|
+
'feTile',
|
|
259
|
+
'feTurbulence',
|
|
260
|
+
'animate',
|
|
261
|
+
'animateMotion',
|
|
262
|
+
'animateTransform',
|
|
263
|
+
'set',
|
|
264
|
+
'desc',
|
|
265
|
+
'title',
|
|
266
|
+
'metadata',
|
|
267
|
+
'foreignObject',
|
|
201
268
|
])
|
|
202
269
|
|
|
203
270
|
const MATHML_TAGS = new Set([
|
|
204
|
-
'math',
|
|
205
|
-
'
|
|
206
|
-
'
|
|
271
|
+
'math',
|
|
272
|
+
'mi',
|
|
273
|
+
'mo',
|
|
274
|
+
'mn',
|
|
275
|
+
'ms',
|
|
276
|
+
'mtext',
|
|
277
|
+
'mspace',
|
|
278
|
+
'mrow',
|
|
279
|
+
'mfrac',
|
|
280
|
+
'msqrt',
|
|
281
|
+
'mroot',
|
|
282
|
+
'msub',
|
|
283
|
+
'msup',
|
|
284
|
+
'msubsup',
|
|
285
|
+
'munder',
|
|
286
|
+
'mover',
|
|
287
|
+
'munderover',
|
|
288
|
+
'mtable',
|
|
289
|
+
'mtr',
|
|
290
|
+
'mtd',
|
|
291
|
+
'mpadded',
|
|
292
|
+
'mphantom',
|
|
293
|
+
'menclose',
|
|
207
294
|
])
|
|
208
295
|
|
|
209
296
|
/** Track SVG context depth — children of <svg> inherit the SVG namespace. */
|
|
@@ -303,9 +390,17 @@ function mountComponent(
|
|
|
303
390
|
let output: VNodeChild
|
|
304
391
|
|
|
305
392
|
const componentName = (vnode.type.name || 'Anonymous') as string
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
393
|
+
|
|
394
|
+
// Devtools: generate ID + track parent/child hierarchy (dev only).
|
|
395
|
+
// In production, compId/devParentId are never assigned — Vite tree-shakes
|
|
396
|
+
// all __DEV__ blocks + the devtools module import to zero bytes.
|
|
397
|
+
let compId: string | undefined
|
|
398
|
+
let devParentId: string | null | undefined
|
|
399
|
+
if (__DEV__) {
|
|
400
|
+
compId = `${componentName}-${Math.random().toString(36).slice(2, 9)}`
|
|
401
|
+
devParentId = _mountingStack![_mountingStack!.length - 1] ?? null
|
|
402
|
+
_mountingStack!.push(compId)
|
|
403
|
+
}
|
|
309
404
|
|
|
310
405
|
// Merge vnode.children into props.children if not already set
|
|
311
406
|
const children = vnode.children ?? []
|
|
@@ -320,14 +415,15 @@ function mountComponent(
|
|
|
320
415
|
// Convert compiler-emitted () => expr wrappers into getter properties.
|
|
321
416
|
// This makes component props reactive — reading props.state inside an
|
|
322
417
|
// effect/computed tracks the underlying signals.
|
|
323
|
-
const mergedProps =
|
|
418
|
+
const mergedProps =
|
|
419
|
+
rawProps === EMPTY_PROPS ? rawProps : makeReactiveProps(rawProps as Record<string, unknown>)
|
|
324
420
|
|
|
325
421
|
try {
|
|
326
422
|
const result = runWithHooks(vnode.type, mergedProps)
|
|
327
423
|
hooks = result.hooks
|
|
328
424
|
output = result.vnode
|
|
329
425
|
} catch (err) {
|
|
330
|
-
_mountingStack
|
|
426
|
+
if (__DEV__) _mountingStack!.pop()
|
|
331
427
|
setCurrentScope(null)
|
|
332
428
|
scope.stop()
|
|
333
429
|
reportError({
|
|
@@ -362,7 +458,7 @@ function mountComponent(
|
|
|
362
458
|
'Components must be synchronous — use lazy() + Suspense for async loading, ' +
|
|
363
459
|
'or fetch data in onMount and store it in a signal.',
|
|
364
460
|
)
|
|
365
|
-
} else if (!('type' in output) && !Array.isArray(output) && !(
|
|
461
|
+
} else if (!('type' in output) && !Array.isArray(output) && !(output as any).__isNative) {
|
|
366
462
|
// Objects without `type` that are NOT arrays (valid VNodeChild[])
|
|
367
463
|
// and NOT NativeItems (from _tpl()) are invalid. Arrays come from
|
|
368
464
|
// Fragment returns, NativeItems come from compiled templates.
|
|
@@ -372,15 +468,15 @@ function mountComponent(
|
|
|
372
468
|
}
|
|
373
469
|
}
|
|
374
470
|
|
|
375
|
-
|
|
376
|
-
scope.addUpdateHook(fn)
|
|
471
|
+
if (hooks.update) {
|
|
472
|
+
for (const fn of hooks.update) scope.addUpdateHook(fn)
|
|
377
473
|
}
|
|
378
474
|
|
|
379
475
|
let subtreeCleanup: Cleanup = noop
|
|
380
476
|
try {
|
|
381
477
|
subtreeCleanup = output != null ? mountChild(output, parent, anchor) : noop
|
|
382
478
|
} catch (err) {
|
|
383
|
-
_mountingStack
|
|
479
|
+
if (__DEV__) _mountingStack!.pop()
|
|
384
480
|
scope.stop()
|
|
385
481
|
const handled = propagateError(err, hooks) || dispatchToErrorBoundary(err)
|
|
386
482
|
if (!handled) {
|
|
@@ -396,44 +492,53 @@ function mountComponent(
|
|
|
396
492
|
return noop
|
|
397
493
|
}
|
|
398
494
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
495
|
+
if (__DEV__) {
|
|
496
|
+
_mountingStack!.pop()
|
|
497
|
+
const firstEl = parent instanceof Element ? parent.firstElementChild : null
|
|
498
|
+
registerComponent(compId!, componentName, firstEl, devParentId!)
|
|
499
|
+
}
|
|
403
500
|
|
|
404
|
-
// Fire onMount hooks inline — effects created inside are tracked by the scope
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
cleanup
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
501
|
+
// Fire onMount hooks inline — effects created inside are tracked by the scope.
|
|
502
|
+
// Lazy-allocate mountCleanups only when an onMount callback returns a cleanup fn.
|
|
503
|
+
let mountCleanups: Cleanup[] | null = null
|
|
504
|
+
if (hooks.mount) {
|
|
505
|
+
for (const fn of hooks.mount) {
|
|
506
|
+
try {
|
|
507
|
+
let cleanup: (() => void) | undefined
|
|
508
|
+
scope.runInScope(() => {
|
|
509
|
+
cleanup = fn() as (() => void) | undefined
|
|
510
|
+
})
|
|
511
|
+
if (cleanup) {
|
|
512
|
+
if (mountCleanups === null) mountCleanups = []
|
|
513
|
+
mountCleanups.push(cleanup)
|
|
514
|
+
}
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error(`[Pyreon] Error in onMount hook of <${componentName}>:`, err)
|
|
517
|
+
reportError({ component: componentName, phase: 'mount', error: err, timestamp: Date.now() })
|
|
518
|
+
}
|
|
416
519
|
}
|
|
417
520
|
}
|
|
418
521
|
|
|
419
522
|
return () => {
|
|
420
|
-
unregisterComponent(compId)
|
|
523
|
+
if (__DEV__) unregisterComponent(compId!)
|
|
421
524
|
scope.stop()
|
|
422
525
|
subtreeCleanup()
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
526
|
+
if (hooks.unmount) {
|
|
527
|
+
for (const fn of hooks.unmount) {
|
|
528
|
+
try {
|
|
529
|
+
fn()
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.error(`[Pyreon] Error in onUnmount hook of <${componentName}>:`, err)
|
|
532
|
+
reportError({
|
|
533
|
+
component: componentName,
|
|
534
|
+
phase: 'unmount',
|
|
535
|
+
error: err,
|
|
536
|
+
timestamp: Date.now(),
|
|
537
|
+
})
|
|
538
|
+
}
|
|
434
539
|
}
|
|
435
540
|
}
|
|
436
|
-
for (const fn of mountCleanups) fn()
|
|
541
|
+
if (mountCleanups) for (const fn of mountCleanups) fn()
|
|
437
542
|
}
|
|
438
543
|
}
|
|
439
544
|
|
package/src/nodes.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { effect, runUntracked } from '@pyreon/reactivity'
|
|
|
10
10
|
// @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time
|
|
11
11
|
const __DEV__ = import.meta.env?.DEV === true
|
|
12
12
|
|
|
13
|
+
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
14
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
15
|
+
|
|
13
16
|
type Cleanup = () => void
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -88,9 +91,7 @@ export function mountReactive(
|
|
|
88
91
|
// (e.g. DynamicStyled's class swap effect). Those effects track
|
|
89
92
|
// their own dependencies independently.
|
|
90
93
|
const cleanup = runUntracked(() =>
|
|
91
|
-
restoreContextStack(contextSnapshot, () =>
|
|
92
|
-
mount(value, parent, marker),
|
|
93
|
-
),
|
|
94
|
+
restoreContextStack(contextSnapshot, () => mount(value, parent, marker)),
|
|
94
95
|
)
|
|
95
96
|
// Guard: a re-entrant signal update (e.g. ErrorBoundary catching a child
|
|
96
97
|
// throw) may have already re-run this effect and updated currentCleanup.
|
|
@@ -157,6 +158,7 @@ function computeKeyedLis(
|
|
|
157
158
|
): number {
|
|
158
159
|
const { tails, tailIdx, pred } = lis
|
|
159
160
|
let lisLen = 0
|
|
161
|
+
let ops = 0
|
|
160
162
|
for (let i = 0; i < n; i++) {
|
|
161
163
|
const key = newKeyOrder[i]
|
|
162
164
|
if (key === undefined) continue
|
|
@@ -167,6 +169,7 @@ function computeKeyedLis(
|
|
|
167
169
|
let hi = lisLen
|
|
168
170
|
while (lo < hi) {
|
|
169
171
|
const mid = (lo + hi) >> 1
|
|
172
|
+
ops++
|
|
170
173
|
if ((tails[mid] as number) < v) lo = mid + 1
|
|
171
174
|
else hi = mid
|
|
172
175
|
}
|
|
@@ -175,6 +178,7 @@ function computeKeyedLis(
|
|
|
175
178
|
if (lo > 0) pred[i] = tailIdx[lo - 1] as number
|
|
176
179
|
if (lo === lisLen) lisLen++
|
|
177
180
|
}
|
|
181
|
+
if (__DEV__ && ops > 0) _countSink.__pyreon_count__?.('runtime.mountFor.lisOps', ops)
|
|
178
182
|
return lisLen
|
|
179
183
|
}
|
|
180
184
|
|
|
@@ -379,21 +383,62 @@ function computeForLis(
|
|
|
379
383
|
): number {
|
|
380
384
|
const { tails, tailIdx, pred } = lis
|
|
381
385
|
let lisLen = 0
|
|
386
|
+
let ops = 0
|
|
387
|
+
// Two-tier fast path.
|
|
388
|
+
//
|
|
389
|
+
// Tier 1 — "extend LIS": if v > the current tail-of-tails, v becomes the
|
|
390
|
+
// new tail. O(1). Covers APPEND: positions [0..N-1] are strictly
|
|
391
|
+
// increasing → the whole sequence is the LIS, 0 probes.
|
|
392
|
+
//
|
|
393
|
+
// Tier 2 — "known slot": if v ≤ lastV but tails[v] === v already, the
|
|
394
|
+
// binary-search answer is provably lo = v (strict-increase invariant
|
|
395
|
+
// guarantees tails[v-1] < v, so v slots exactly at index v). O(1) too.
|
|
396
|
+
// Covers PREPEND: [new N rows, old M rows] produces positions [0..N-1,
|
|
397
|
+
// 0..M-1] — the second monotonic run replaces tails[0..M-1] at indices
|
|
398
|
+
// N..N+M-1 with zero probes each. Before this tier, 1k prepend was ~10k
|
|
399
|
+
// probes; with it, 0.
|
|
400
|
+
//
|
|
401
|
+
// Tier 3 — binary search fallback. Random shuffles and other mixed
|
|
402
|
+
// reorders pay the standard log₂(lisLen) per index.
|
|
403
|
+
//
|
|
404
|
+
// Safety: the `v < lisLen && tails[v] === v` check is a strict subset of
|
|
405
|
+
// "binary-search would return v", so it never produces a wrong answer on
|
|
406
|
+
// shufles — it just opportunistically avoids probing when the answer
|
|
407
|
+
// happens to be the index itself. No behaviour change, only fewer probes.
|
|
408
|
+
let lastV = -1
|
|
382
409
|
for (let i = 0; i < n; i++) {
|
|
383
410
|
const key = newKeys[i] as string | number
|
|
384
411
|
const v = cache.get(key)?.pos ?? 0
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
412
|
+
// Tier 1: extend LIS.
|
|
413
|
+
if (v > lastV) {
|
|
414
|
+
tails[lisLen] = v
|
|
415
|
+
tailIdx[lisLen] = i
|
|
416
|
+
if (lisLen > 0) pred[i] = tailIdx[lisLen - 1] as number
|
|
417
|
+
lisLen++
|
|
418
|
+
lastV = v
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
// Tier 2: known slot for piecewise-monotonic patterns (prepend, etc.).
|
|
422
|
+
let lo: number
|
|
423
|
+
if (v < lisLen && (tails[v] as number) === v) {
|
|
424
|
+
lo = v
|
|
425
|
+
} else {
|
|
426
|
+
// Tier 3: binary search.
|
|
427
|
+
lo = 0
|
|
428
|
+
let hi = lisLen
|
|
429
|
+
while (lo < hi) {
|
|
430
|
+
const mid = (lo + hi) >> 1
|
|
431
|
+
ops++
|
|
432
|
+
if ((tails[mid] as number) < v) lo = mid + 1
|
|
433
|
+
else hi = mid
|
|
434
|
+
}
|
|
391
435
|
}
|
|
392
436
|
tails[lo] = v
|
|
393
437
|
tailIdx[lo] = i
|
|
394
438
|
if (lo > 0) pred[i] = tailIdx[lo - 1] as number
|
|
395
|
-
|
|
439
|
+
// v ≤ lastV here, so tails can't be extended: lo < lisLen always.
|
|
396
440
|
}
|
|
441
|
+
if (__DEV__ && ops > 0) _countSink.__pyreon_count__?.('runtime.mountFor.lisOps', ops)
|
|
397
442
|
return lisLen
|
|
398
443
|
}
|
|
399
444
|
|
|
@@ -463,6 +508,7 @@ export function mountFor<T>(
|
|
|
463
508
|
|
|
464
509
|
let cache = new Map<string | number, ForEntry>()
|
|
465
510
|
let currentKeys: (string | number)[] = []
|
|
511
|
+
const _reusableKeySet = new Set<string | number>()
|
|
466
512
|
let cleanupCount = 0
|
|
467
513
|
let anchorsRegistered = false
|
|
468
514
|
|
|
@@ -644,7 +690,11 @@ export function mountFor<T>(
|
|
|
644
690
|
newKeys: (string | number)[],
|
|
645
691
|
liveParent: Node,
|
|
646
692
|
) => {
|
|
647
|
-
|
|
693
|
+
// Reuse a persistent Set to avoid allocating a new one per update.
|
|
694
|
+
// Cleared + repopulated instead of constructing new Set(newKeys).
|
|
695
|
+
_reusableKeySet.clear()
|
|
696
|
+
for (let i = 0; i < newKeys.length; i++) _reusableKeySet.add(newKeys[i] as string | number)
|
|
697
|
+
removeStaleForEntries(_reusableKeySet)
|
|
648
698
|
mountNewForEntries(items, n, newKeys, liveParent)
|
|
649
699
|
|
|
650
700
|
if (!anchorsRegistered) {
|
package/src/template.ts
CHANGED
|
@@ -2,6 +2,14 @@ import type { NativeItem, VNodeChild } from '@pyreon/core'
|
|
|
2
2
|
import { renderEffect } from '@pyreon/reactivity'
|
|
3
3
|
import { mountChild } from './mount'
|
|
4
4
|
|
|
5
|
+
// Dev-mode gate: see `pyreon/no-process-dev-gate` lint rule for why this
|
|
6
|
+
// uses `import.meta.env.DEV` instead of `typeof process !== 'undefined'`.
|
|
7
|
+
// @ts-ignore — `import.meta.env.DEV` is provided by Vite/Rolldown at build time
|
|
8
|
+
const __DEV__ = import.meta.env?.DEV === true
|
|
9
|
+
|
|
10
|
+
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
11
|
+
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
12
|
+
|
|
5
13
|
/**
|
|
6
14
|
* Creates a row/item factory backed by HTML template cloning.
|
|
7
15
|
*
|
|
@@ -68,7 +76,8 @@ export function _bindText(
|
|
|
68
76
|
if (source.direct) {
|
|
69
77
|
const textUpdate = () => {
|
|
70
78
|
const v = source._v
|
|
71
|
-
|
|
79
|
+
const next = v == null || v === false ? '' : String(v as string | number)
|
|
80
|
+
if (next !== node.data) node.data = next
|
|
72
81
|
}
|
|
73
82
|
textUpdate()
|
|
74
83
|
return source.direct(textUpdate)
|
|
@@ -77,7 +86,8 @@ export function _bindText(
|
|
|
77
86
|
const fn = source as unknown as () => unknown
|
|
78
87
|
return renderEffect(() => {
|
|
79
88
|
const v = fn()
|
|
80
|
-
|
|
89
|
+
const next = v == null || v === false ? '' : String(v as string | number)
|
|
90
|
+
if (next !== node.data) node.data = next
|
|
81
91
|
})
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -142,6 +152,7 @@ const _tplCache = new Map<string, HTMLTemplateElement>()
|
|
|
142
152
|
* })
|
|
143
153
|
*/
|
|
144
154
|
export function _tpl(html: string, bind: (el: HTMLElement) => (() => void) | null): NativeItem {
|
|
155
|
+
if (__DEV__) _countSink.__pyreon_count__?.('runtime.tpl')
|
|
145
156
|
let tpl = _tplCache.get(html)
|
|
146
157
|
if (!tpl) {
|
|
147
158
|
tpl = document.createElement('template')
|