pawajs 2.4.0-beta.6 → 2.4.0-beta.8

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/AGENT.md ADDED
@@ -0,0 +1,163 @@
1
+ # AGENT.md — PawaJS
2
+
3
+ Context for any AI agent (or new contributor) working in this codebase. Read this before making changes — several of the rules below exist because of real bugs found and fixed during development, not theoretical concerns.
4
+
5
+ ---
6
+
7
+ ## What PawaJS is
8
+
9
+ PawaJS is a **reactive web runtime** — not a virtual-DOM framework. Its identity, unchanged across the whole rewrite:
10
+
11
+ - **Extends the browser rather than replacing it.** HTML with directive attributes (`if`, `for-each`, `state-*`, `@{ }`) is the interface. No JSX, no compiler, no build step required to author with it.
12
+ - **No VDOM, no diffing pass.** Reactive effects (`createEffect`) wire directly to the specific DOM node/attribute that needs to change. An update touches only what changed.
13
+ - **Fine-grained reactivity**, structurally close to Vue 3's own core (`track`/`trigger`/`activeEffect`, a `WeakMap` of target → key → effect `Set`) — but the rendering strategy on top of it is closer to Solid's (no VDOM, direct DOM wiring).
14
+ - **Components are synchronous by default.** There is no async-component mode (`useAsync`/`$async` are removed). All async lives in the `await` / `as-fallback` / `as-catch` directives.
15
+
16
+ ## The one organizing question
17
+
18
+ Nearly everything non-trivial in this codebase — rendering, SSR pruning, `.client` detection, what goes in the continuity payload — reduces to the same question, asked at different points in the pipeline: **did this specific evaluation touch reactive state (a Proxy read)?**
19
+
20
+ - At render time: decides whether something gets wrapped in `createEffect` at all.
21
+ - At SSR time (`isProxy`): decides whether a component becomes `.client`, and whether a given prop/expression needs to be sent to the client at all.
22
+ - At payload-build time: decides whether a construct earns a JSON entry — gated down to individual props and hook calls, not just per-component.
23
+
24
+ When adding a new construct or directive, ask this same question before inventing new detection logic. It is very likely the answer is "reuse `isProxy`," not "write a new check."
25
+
26
+ ---
27
+
28
+ ## Package layout
29
+
30
+ - **`pawajs` (core)** — the reactive engine, Graph/render system, control-flow directives, component handling, built-ins (`Active`, `Transition`). No SSR code, no `linkedom`. Ships to the client. Current size: **6.6kB gzipped** (down from 17.7kB pre-rewrite, with more capability — see `bundlephobia.com/package/pawajs`).
31
+ - **`pawa-ssr`** — server-side render pass. Produces HTML + a continuity JSON payload (SCP). Depends on `linkedom` for server-side DOM. Never ships to the client.
32
+ - **`pawajs-continue`** — client-side resumer (`PawaContinue`). Consumes the HTML + JSON pair `pawa-ssr` produces and attaches live reactivity without re-rendering.
33
+ - **`pawajs-dom-router`** — pure client-side router. No server involvement past initial load.
34
+ - **`supapawajs`** — full-stack meta-framework on Express. Owns delivery strategy: normal SSR, streaming, or SSG. Calls `pawa-ssr`/`pawajs-continue` but doesn't duplicate their logic. Route navigation fetches data only — it does **not** re-invoke `pawaServer`.
35
+ - **`pawajs-vite-plugin`** — build tooling, HMR, state preservation across reloads.
36
+
37
+ `pawa-ssr` and `pawajs-continue` don't know about delivery mode (streamed vs. cached vs. per-request) — that's entirely `supapawajs`'s job. Keep that boundary intact.
38
+
39
+ ---
40
+
41
+ ## Core reactivity (`reactive.js`)
42
+
43
+ - `track`/`trigger`/`activeEffect`, `targetMap: WeakMap<target, Map<key, Set<effect>>>`.
44
+ - **Every effect re-run must clear its own prior subscriptions before re-tracking.** `cleanupEffect(effect)` runs at the top of every effect invocation, unsubscribing from every dep set the effect was in from its *previous* run, so stale dependencies never accumulate. This was a real historical bug — don't reintroduce a version of `createEffect` that skips this.
45
+ - **Dead effects are tracked via a flag (`effect._killed`), not a global `Set`.** `stopEffect`/the `deletes` closure from `createEffect` set `_killed = true`; `trigger`/`runEffectNow` check the flag. There used to be a module-level `deleteEffect` Set that only grew — don't resurrect that pattern.
46
+ - **`graph.unActive`** — effects deferred while their owning `Graph` node is inactive (see `Active`, below) get pushed here by `runEffectNow` and flushed via `queueEffect`/`setTimeout` when the graph reactivates. Each `Graph` node owns its own `unActive`/timer list — never share one across nodes (see reentrancy note below).
47
+
48
+ ### Reentrancy — a recurring bug class, watch for it
49
+
50
+ Several real bugs today were the same shape: **a single module-level mutable variable standing in for what should be call-scoped or per-node state**, unsafe under nesting or reentrant calls (`inProxy`, `transfer`/`schedule`, `store`/`useStorage`, `Active`'s single shared `timer`). The fix pattern is `withSlot` (save previous value, set new, run, restore previous in `finally`) or — better — moving the state onto the specific object it belongs to (e.g., `Active`'s timers now live on `g.timers` per-graph-node, not one shared array). When adding new module-level state that gets mutated inside a callback, ask: **can this function be called while another call to the same function is still in progress (nested or nearly-concurrent)?** If yes, it needs `withSlot` or per-instance storage, not a bare `let`.
51
+
52
+ ---
53
+
54
+ ## The Graph model (`graph/graph.js`)
55
+
56
+ Every dynamic construct (`condition`, `for-each` item, `component`, `template`, `key`) gets a `Graph` node: `{ref, children, parent, move, insertAfter, getElement, remove, killDown, ...}`.
57
+
58
+ - **`ref`** can point to: a real DOM node, another `Graph` node (recursive — chase via `getElement()`), or be temporarily unset. `getElement()` walks the `ref` chain recursively until it finds a concrete node. This is the mechanism that lets deeply nested constructs collapse to a single anchor instead of needing bracket-comment pairs at every level.
59
+ - **`move(anchor)`** repositions a node's DOM before `anchor`. **`insertAfter`-style methods must mirror `move`'s recursive structure** (template-node branch vs. direct-ref branch) — don't hardcode a single anchor node inside a helper like `createItem`; always thread the current anchor through, or moves will silently land in the wrong position (see `for-each` reconciler notes below).
60
+ - **Directionality matters.** `insertBefore(el, anchor)` puts `el` before `anchor`; `anchor.after(el)` (or an `insertAfter` graph method) puts it after. Getting this backwards is a real, easy-to-make mistake — verify against a 3+ item sequence by hand or by testing, not by reading the code once.
61
+
62
+ ---
63
+
64
+ ## `for-each` reconciliation
65
+
66
+ `for.js`'s `initializer` does keyed, LIS-based reconciliation — not naive full-rebuild:
67
+
68
+ 1. On each evaluate, compare old key order to new key order (`sameArrangement` check). **If identical, do nothing — zero DOM cost.** This is the common case (an unrelated dependency triggered the effect) and must stay free.
69
+ 2. If the arrangement changed, compute the **longest increasing subsequence** of old-indices among surviving keys. LIS members are already in correct relative order and are *not* moved.
70
+ 3. Walk backward (`i = length-1` down to `0`), maintaining a **running anchor** (starts at the `comment`, updated to each processed item's own element after handling it) — never anchor every move against one fixed node. Anchoring everything against a single fixed node breaks correctness the moment some items are skipped (stable/LIS) and others aren't.
71
+ 4. `createItem` must accept and insert against the **passed-in anchor**, not a hardcoded `comment`. A hardcoded anchor in `createItem` was a real bug: it caused a second newly-created item to land behind the first instead of in its correct position.
72
+ 5. New pushes should land visually **after** the current last item (`children[length-1]`), matching `array.push()` intuition — this requires `.after()`, not `insertBefore`, when re-anchoring the marker comment (getting the direction backward here was also a real, caught bug).
73
+ 6. `enqueueUpdate` chains updates through a promise (`updatePromise`) so overlapping triggers (e.g., two removals in quick succession, each awaiting exit-animation time via `Transition`) queue correctly instead of racing. This path is less thoroughly tested than the single-update case — see Testing below.
74
+
75
+ When touching this file: **test with a running app, not just by reading the diff.** Every bug in this reconciler so far was only caught by actually running append/swap/remove/reinsert/filter sequences, never by code review alone.
76
+
77
+ ---
78
+
79
+ ## SSR / Continuity (SCP — Server Continuation Protocol)
80
+
81
+ The core idea: SSR produces (a) real HTML with `p:id` attributes marking construct boundaries, and (b) a flat, id-keyed JSON payload carrying only what's genuinely needed to resume without recomputation. The client resumer (`pawajs-continue`) walks the JSON, not the DOM — one targeted lookup per entry via `p:id`, never a DOM traversal.
82
+
83
+ ### Payload principles — do not violate these when adding a new construct
84
+
85
+ - **Flat, not tree-shaped.** No nested structural encoding in the JSON — position/ownership is discovered via `p:id` lookups and explicit `forkey` tags, not by JSON nesting. This is what keeps the format backend-portable (any language can emit a flat object; no language-specific tree-shape assumptions).
86
+ - **Gated by proxy-touch, at the finest available granularity.** A construct earns a JSON entry only if `isProxy` fired true for its evaluation — not because it's "inside a client boundary," not because its type is usually reactive. This applies down to individual props and hook calls on components, not just whole components.
87
+ - **`id` is a pure lookup handle. `key` is diff identity.** Never conflate them. A `for-key` item's `id` is how the resumer finds its DOM node; its `key` is what a future array diff uses to decide "same item, moved" vs. "new item."
88
+ - **`hydrate.ref` follows one of two patterns**, and both are legitimate — know which one you're in:
89
+ - **Sentinel (`ref: "ref"`)**: "resolve by chasing into children/self" — used by `condition`, `await`, `template`, `component` boundaries that don't correspond to a single fixed element yet.
90
+ - **Direct (`ref: "<own id>"`)**: the construct's own element is the final anchor (most components, `state`, `Active`-style self-binding via `useRef()`).
91
+ - Note: `ref="ref"` as a literal *HTML attribute* is a completely different thing — it's `useRef()`'s own binding syntax (the string is the JS variable name), unrelated to the hydrate sentinel. They share a string value coincidentally, not semantically.
92
+ - **Every hydrate node needs a `type` field.** The resumer dispatches on it. Don't add a construct type without one — relying on field-shape inference instead of an explicit `type` was a real gap that got fixed; don't reintroduce it.
93
+ - **`resolved` (on `await` nodes) means "settled and ready to attach," not "succeeded vs failed."** Success/error is carried by which branch's HTML actually rendered (`awaits` vs `awaitError`), not by this field — both branches correctly set `resolved:true`.
94
+ - **`forkey`** tags a nested construct with its owning `for-each` item's key, so a flat `children` array under a `for-each` node can still be correctly attributed per-item without nesting. Only relevant when the parent hydrate node looks like a for-each (`hydrate?.arrayName` present).
95
+ - **Region ownership, not per-element ids.** `p:id` marks the boundary of a *construct's region* — everything inside (plain, non-reactive content) shares that id. Don't expect one id per DOM node; expect one id per independently-resolvable reactive unit.
96
+
97
+ ### Per-construct resume strategy — don't unify these into one code path
98
+
99
+ - **`condition` / `for-each`** — genuinely paused: seed comparison state (`oldChain`/`current`, keyed `children`) from JSON, skip the first `evaluate()` entirely (`isHydrate?.enter` check), resume real computation only at the next genuine trigger.
100
+ - **`await`** — resolved/error value handed straight into context via the same merge `apply()` uses live. The promise function is never called. Retry (if the app needs it) goes through a `Key`-forced remount at the parent, not a bespoke retry mechanism inside `Await` itself.
101
+ - **`component`** — re-executes in full on the client (cheap; accepted cost) to rebuild `useInsert` context and effects, but the returned HTML string is discarded — the component attaches to the already-correct SSR element found via `p:id`, never rebuilding DOM.
102
+ - **`only-client`** — opts out of SSR entirely. Ships as an inert `<template on-client>` for the client to instantiate fresh. No JSON entry, since nothing was evaluated server-side.
103
+ - **`state-*` attributes** — SSR-evaluated inline; the resolved value **must** be sent (nothing re-derives it). Component-*internal* `$state` calls (inside a component's JS body) do **not** need to be sent — the component's re-execution naturally reproduces them for free. Don't send both; sending the internal ones is pure duplication.
104
+
105
+ ### Detection: `.client` / `force` / auto-inheritance
106
+
107
+ A component is treated as client if **any** of:
108
+ 1. Registration-time flag (`Component.client = true` on the function itself — e.g. `Active`, `Transition`).
109
+ 2. Internal hook usage (`$state`, `stateWatch`) — calling a client-reactive hook is itself what marks the component client, as a side effect of the call, via `AsyncLocalStorage`-scoped detection during SSR.
110
+ 3. Ancestor is client (transitive through non-client intermediaries).
111
+ 4. **`force`** — an SSR-time template directive (`el.hasAttribute('force')`), scoped per usage site, not a static component property. Exists specifically for the one case `isProxy` structurally cannot see: a function value (event handler, callback) passed as a prop or into `useInsert`'s context. Calling a function during SSR never executes its body, so state read *inside* that function is invisible to proxy-touch detection — `force` is the deliberate escape hatch, applied at the exact call site that needs it.
112
+
113
+ Once a component is client for any reason, **nested components under it do not need repeated manual `force` calls** just because they also carry function-valued context — a check inside already-confirmed client boundaries handles function-valued `useInsert`/context entries automatically. `force` is only for establishing the boundary in the first place, not for every nested case beneath it.
114
+
115
+ `$state` cannot be called in a component that isn't (or doesn't become) client — calling it is what makes a component client, there's no separate permission check.
116
+
117
+ ### Streaming
118
+
119
+ Raw chunked HTTP, not SSE. First chunk: fully resolved content + placeholders (`awaitsLoading`, `resolved:false`) for pending `await`s. Each pending item resolves independently and flushes its own chunk with an inline `<script>` on settling — never waits for siblings. Client/server rendezvous is a flat, id-keyed handshake (`window.awaits[id]` / `window.states[id]`): whichever side (server chunk arriving, or client resumer walking to that point) gets there *second* consumes what the *first* side left behind. Neither side polls or waits.
120
+
121
+ ---
122
+
123
+ ## Directive reference (current, post-rewrite)
124
+
125
+ - `if` / `else` / `else-if` — conditional rendering. (`switch`/`case`/`default` are **removed**.)
126
+ - `for-each` + `for-key` — keyed list rendering (see reconciliation notes above).
127
+ - `state-*` — inline reactive state scoped to an element and its children.
128
+ - `await` / `as-fallback` / `as-catch` — async handling. `await` must be given a **function that returns a promise**, never an already-invoked call (`await="fetchUser"`, not `await="fetchUser()"`). `as="name"` binds the resolved/caught value under a custom name (defaults: `res` / `error`).
129
+ - `key` — forces a full remount when its watched value changes. Used as the retry mechanism for failed `await`s (force a parent `Key` change rather than building retry into `Await`).
130
+ - `on-<event>` / `out-<event>` — event handling, with `.self`/`.prevent`/`.once`/`.capture` chain modifiers.
131
+ - `:prop` — reactive component prop.
132
+ - `<slot>` / `<slot name="...">` — content projection (not a `children` prop — this changed in the rewrite).
133
+ - `<template prop="name">` — pass a raw string/markup chunk as a prop value.
134
+ - `aschild` — component's root element merges into (rather than wraps) what the caller passed.
135
+ - `ref` — bind a `useRef()` object to an element. Unrelated to the SCP hydrate sentinel of the same string value — see note above.
136
+ - `force` — SSR directive forcing a component to client status at this usage site (see Detection above).
137
+ - ~~`is-exit`~~ — **removed.** Superseded by `Transition`'s own `onExit`/`onEnter` graph hooks, which wait for the exit animation before actual DOM removal automatically.
138
+ - ~~`useServer`, `useAsync`/`$async`~~ — **removed.** No async-component mode; use `await`/`as-fallback`/`as-catch` instead.
139
+
140
+ ## Built-in components
141
+
142
+ - **`Active`** — `:show="expr"` controls visibility (`display:none`) and pauses/resumes reactive effects underneath while hidden (via `graph.active` + `graph.unActive`, see reactivity notes above). Always client (`Active.client = true`).
143
+ - **`Transition`** — `:enter`/`:exit` (class-name arrays or inline style-string arrays, same shape as `el.animation()`) + `:duration` (default 300ms). Waits for the exit animation to finish before the element is actually removed. Always client.
144
+
145
+ ---
146
+
147
+ ## Testing conventions
148
+
149
+ There is no automated correctness harness yet (SSR-render → resume → compare against fresh client render is a known, not-yet-built gap — high priority if adding one). Until it exists:
150
+
151
+ - **Trust real, running output over code review for anything touching the reconciler, the Graph model, or resume seeding.** Every bug caught in these areas during development was found by actually running sequences (append/remove/reorder/filter), never by reading the diff.
152
+ - When testing `for-each` changes, exercise at minimum: append, remove-from-middle, filter-then-restore (items reappearing at original relative position), multi-item reorder (not just single swaps), and rapid back-to-back updates (overlapping `enqueueUpdate` calls, especially with `Transition`'s exit-animation delay in play).
153
+ - When testing SSR/continuity changes, check real payload output for: correct `type` per node, correct `resolved` semantics on `await`, `p:id` region boundaries matching expectations, and — critically — that pruning is actually happening (static/non-reactive content should produce **zero** hydrate entries, not just small ones).
154
+
155
+ ---
156
+
157
+ ## Things intentionally *not* built yet — don't assume otherwise
158
+
159
+ - No automated resume-correctness harness.
160
+ - No second (non-JS) backend implementation of SCP — portability is a design property, not yet a proven one.
161
+ - No dev-mode warning system beyond the initial `stripComponent` (statically-pruned-but-in-a-client-boundary) warning — a broader dev-mode/error-handling pass is planned but not started.
162
+ - No PML (partial re-render / DOM morph after initial resume) — designed in concept (compare live Graph tree against a parsed incoming tree, route first-boundary component updates through `reProps` rather than DOM diffing) but unimplemented.
163
+ - No large-list payload/perf measurement has been taken yet — treat "for-each cost at scale" as an open, unmeasured question, not a solved one.
package/README.md CHANGED
@@ -3,14 +3,14 @@ pawajs - power the web (reactivity and html runtime)
3
3
 
4
4
  # PawaJS
5
5
 
6
- **A lightweight and reactive JavaScript library for building modern web interfaces with a simple, declarative syntax.**
6
+ **A lightweight and reactive JavaScript library for building modern web interfaces with a simple, declarative syntax (15kb).**
7
7
 
8
8
  PawaJS (reactive web runtime) is a JavaScript library designed for building dynamic user interfaces. It combines a component-based architecture and progressive enhancement with a powerful reactivity system, no v-dom. Its intuitive, directive-based templating feels familiar and makes it easy to create interactive applications, from simple widgets to complex single-page apps. With built-in support for server-side rendering using pawa-ssr (server rendering runtime) and pawajs-continue (continuity runtime), PawaJS is equipped for performance and scalability.
9
9
 
10
10
  PawaJS is a **reactive web runtime** for HTML-first UIs: components, directives, and fine-grained `$state` updates with **no virtual DOM**.
11
11
 
12
12
  On the client it powers widgets through SPAs. With **pawa-ssr** and **pawajs-continue** it uses **selective continue** (SCP): the server sends HTML plus a sparse index of reactive areas; the client continues only those areas. **Static components are stripped** from that index so they are not re-executed on the client.
13
- pawajs@2.4.0-beta.4 — ~6.6 kB min+gzip (Bundlephobia)
13
+
14
14
  🌐 **Website:** [pawajs.vercel.app](https://pawajs.vercel.app)
15
15
 
16
16
 
package/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Active } from "./src/builtIn/Active.js";
2
2
  import { Transition } from "./src/builtIn/viewTransition.js";
3
3
  import { getDev, setDev } from "./src/dev/index.js";
4
- import { mountPawaDevtools, unmountPawaDevtools } from "./src/dev/tools.js";
5
4
  import { Plugin } from "./src/dev/plugin.js";
6
5
  import { Graph } from "./src/graph/graph.js";
7
6
  import { PawaRender } from "./src/graph/index.js";
@@ -70,4 +69,4 @@ export {
70
69
  setDev
71
70
  };
72
71
 
73
- export { mountPawaDevtools, unmountPawaDevtools, RootGraph };
72
+ export { RootGraph };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pawajs",
3
- "version": "2.4.0-beta.6",
3
+ "version": "2.4.0-beta.8",
4
4
  "type": "module",
5
5
  "description": "pawajs library (reactive web runtime) for easily building web ui, enhancing html element, micro frontend, spa etc",
6
6
  "main": "index.js",
@@ -1,4 +1,4 @@
1
- import { getDevelopment } from "../../index.js"
1
+ import { getDev } from "../dev/index.js"
2
2
  import { components, hmrComponentsMap, restorePawaStateFromContext, snapshotInsert } from "../global.js"
3
3
  import { PawaRender } from "../graph/index.js"
4
4
  import { stateWatch } from "../hooks/state.js"
@@ -68,7 +68,7 @@ export const createComponent=(el,context,graph,compareGraph)=>{
68
68
  }
69
69
  try {
70
70
  const temp=sanitizeTemplate(component(mainProp))
71
- if (getDevelopment() && compareGraph) {
71
+ if (getDev() && compareGraph) {
72
72
  restorePawaStateFromContext(compareGraph.context,renderGraph.context,compareGraph.initialInsert)
73
73
  }
74
74
  for (const effect of renderGraph.beforeMount) {
@@ -130,7 +130,7 @@ export const createComponent=(el,context,graph,compareGraph)=>{
130
130
  }
131
131
  }
132
132
  const componentName=splitAndAdd(el.tagName)
133
- if (getDevelopment()) {
133
+ if (getDev()) {
134
134
  const id=Date.now() + Math.random()
135
135
  const removeFromHmrMap = () => {
136
136
  const array = hmrComponentsMap.get(componentName)
@@ -164,11 +164,11 @@ export const createComponent=(el,context,graph,compareGraph)=>{
164
164
  }
165
165
 
166
166
  comment.replaceWith(element)
167
- setContext({...renderGraph.context})
167
+ setContext({...context,...renderGraph.context})
168
168
  render(element)
169
169
 
170
170
  componentGraph=former
171
- // renderGraph.context={}
171
+ renderGraph.context={}
172
172
  renderGraph.firstTime=false
173
173
  Promise.resolve().then(()=>{
174
174
  for (const effect of renderGraph.arrayEffect) {
@@ -84,72 +84,123 @@ export const initializer=(el, attr, context,comment,renderGraph,componentGraph,a
84
84
  keyCaches.set(cacheKey, finalKey)
85
85
  return finalKey
86
86
  }
87
- const createItem=(newElement,item,key,index)=>{
88
- const itemContext={
89
- ...context,
90
- [arrayItem]:item,
91
- [indexes]:index
92
- }
93
-
94
- const {render:rend,renderGraph:newGraph}=PawaRender(renderGraph,itemContext)
95
- newGraph.nodeType='for-key'
96
- newGraph.key=key
97
- newElement.removeAttribute('for-key')
98
- comment.parentElement.insertBefore(newElement, comment)
99
- keepComponentGraph(componentGraph)
100
- rend(newElement)
101
- return newGraph
87
+ const createItem = (newElement, item, key, index, anchor) => {
88
+ const itemContext = {
89
+ ...context,
90
+ [arrayItem]: item,
91
+ [indexes]: index
102
92
  }
93
+ const {render: rend, renderGraph: newGraph} = PawaRender(renderGraph, itemContext)
94
+ newGraph.nodeType = 'for-key'
95
+ newGraph.key = key
96
+ newElement.removeAttribute('for-key')
97
+ anchor.parentElement.insertBefore(newElement, anchor) // CHANGED: was `comment.parentElement.insertBefore(newElement, comment)`
98
+ keepComponentGraph(componentGraph)
99
+ rend(newElement)
100
+ return newGraph
101
+ }
102
+ let updateGen = 0
103
+ const applyUpdate = async ({array, arrayKey,gen}) => {
104
+ if (!comment.parentElement) {
105
+ const lastChild = renderGraph.children[renderGraph.children.length -1]
106
+ const ref = lastChild?.getElement()
107
+
103
108
 
104
- const applyUpdate=async({array,arrayKey})=>{
105
- if (!comment.parentElement) {
106
- const ref=renderGraph.getElement()
107
-
108
- if (ref?.parentElement) ref.parentElement.insertBefore(comment,ref)
109
+ if (ref?.parentElement) {
110
+ ref.after(comment) // AFTER, not insertBefore — comment must sit at the true tail
111
+ }
112
+ }
113
+
114
+ const oldChildren = renderGraph.children
115
+ const oldKeyToChild = new Map(oldChildren.map(c => [c.key, c]))
116
+ const nextKeys = new Set(arrayKey)
117
+
118
+ const removed = oldChildren.filter(child => !nextKeys.has(child.key))
119
+ removed.forEach(child => child.killDown())
120
+ await Promise.all(removed.map(child => child.remove()))
121
+ if (gen !== updateGen) return
122
+ const nextChildren = arrayKey.map(key => oldKeyToChild.get(key) ?? null)
123
+
124
+ const oldKeyToOldIndex = new Map(oldChildren.map((c, idx) => [c.key, idx]))
125
+ const survivingOldIndices = []
126
+ const survivingNewIndices = []
127
+ arrayKey.forEach((key, newIndex) => {
128
+ if (oldKeyToOldIndex.has(key)) {
129
+ survivingOldIndices.push(oldKeyToOldIndex.get(key))
130
+ survivingNewIndices.push(newIndex)
109
131
  }
132
+ })
133
+ const lisIndices = longestIncreasingSubsequenceIndices(survivingOldIndices)
134
+ const stableNewIndices = new Set(lisIndices.map(i => survivingNewIndices[i]))
110
135
 
111
- const children=renderGraph.children.slice()
112
- const nextKeys=new Set(arrayKey)
113
- const removed=children.filter(child=>!nextKeys.has(child.key))
114
- removed.forEach(child=>child.killDown())
115
- await Promise.all(removed.map(child=>child.remove()))
116
-
117
- const existing=new Map(children
118
- .filter(child=>nextKeys.has(child.key))
119
- .map(child=>[child.key,child]))
120
- const nextChildren=new Array(arrayKey.length)
121
-
122
- for (let i=arrayKey.length-1; i>=0; i--) {
123
- const key=arrayKey[i]
124
- const existingChild=existing.get(key)
125
- if (existingChild) {
126
- existing.delete(key)
127
- existingChild.move(comment)
128
- nextChildren[i]=existingChild
129
- }else{
130
- nextChildren[i]=createItem(el.cloneNode(true),array[i],key,i)
136
+ // Walk backward, tracking the correct DOM anchor for "the position right
137
+ // after where we currently are" — NOT a fixed node. This is what actually
138
+ // preserves the single-anchor trick's correctness while skipping no-op moves.
139
+ let anchor = comment
140
+ for (let i = arrayKey.length - 1; i >= 0; i--) {
141
+ const key = arrayKey[i]
142
+ let child = nextChildren[i]
143
+ if (child) {
144
+ if (!stableNewIndices.has(i)) {
145
+ child.move(anchor) // only real moves touch the DOM
131
146
  }
147
+ // stable or just-moved: either way, this item's element is now
148
+ // the correct thing to anchor the NEXT (earlier) item against
149
+ anchor = child.getElement() ?? anchor
150
+
151
+ } else {
152
+ nextChildren[i] = createItem(el.cloneNode(true), array[i], key, i,anchor)
153
+ anchor = nextChildren[i].getElement() ?? anchor
132
154
  }
133
- renderGraph.children=nextChildren
134
- if (arrayKey.length > 0) comment.remove()
135
155
  }
136
156
 
137
- const enqueueUpdate=(array,arrayKey)=>{
138
- pendingUpdate={array,arrayKey}
139
- if (updateScheduled)return
140
- updateScheduled=true
141
- updatePromise=updatePromise.then(async()=>{
142
- while(pendingUpdate){
143
- const update=pendingUpdate
144
- pendingUpdate=null
145
- await applyUpdate(update)
146
- }
147
- }).finally(()=>{
148
- updateScheduled=false
149
- renderGraph.entrance=true
150
- })
157
+ renderGraph.children = nextChildren
158
+ }
159
+ // Standard O(n log n) LIS-by-index helper
160
+ function longestIncreasingSubsequenceIndices(seq) {
161
+ const n = seq.length
162
+ const tails = []
163
+ const prev = new Int32Array(n).fill(-1)
164
+ const tailsAt = [] // index into seq
165
+
166
+ for (let i = 0; i < n; i++) {
167
+ let lo = 0, hi = tails.length
168
+ while (lo < hi) {
169
+ const mid = (lo + hi) >> 1
170
+ if (seq[tailsAt[mid]] < seq[i]) lo = mid + 1
171
+ else hi = mid
151
172
  }
173
+ if (lo > 0) prev[i] = tailsAt[lo - 1]
174
+ tailsAt[lo] = i
175
+ if (lo === tails.length) tails.push(seq[i])
176
+ else tails[lo] = seq[i]
177
+ }
178
+ const result = []
179
+ let idx = tailsAt[tailsAt.length - 1]
180
+ while (idx !== undefined && idx >= 0) {
181
+ result.push(idx)
182
+ idx = prev[idx] >= 0 ? prev[idx] : undefined
183
+ }
184
+ return result.reverse()
185
+ }
186
+
152
187
 
188
+ const enqueueUpdate = (array, arrayKey) => {
189
+ pendingUpdate = { array, arrayKey, gen: ++updateGen }
190
+ if (updateScheduled) return
191
+ updateScheduled = true
192
+ updatePromise = updatePromise.then(async () => {
193
+ while (pendingUpdate) {
194
+ const update = pendingUpdate
195
+ pendingUpdate = null
196
+ await applyUpdate(update)
197
+ // if a newer pendingUpdate appeared mid-await, loop continues
198
+ }
199
+ }).finally(() => {
200
+ updateScheduled = false
201
+ renderGraph.entrance=true
202
+ })
203
+ }
153
204
  const evaluate=()=>{
154
205
 
155
206
  try {
@@ -193,8 +244,6 @@ export const initializer=(el, attr, context,comment,renderGraph,componentGraph,a
193
244
  }
194
245
 
195
246
  if (new Set(arrayKey).size !== arrayKey.length) {
196
- console.log(arrayName,arrayKey);
197
-
198
247
  throw new Error(`Duplicate key in for-each: ${arrayKey}`)
199
248
  }
200
249
  enqueueUpdate(array,arrayKey)
package/src/dev/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { components } from "../global.js";
2
-
3
2
  let devTool=false
4
3
  export const setDev=(devs=false)=>{
5
4
  devTool = Boolean(devs)
@@ -64,4 +63,4 @@ const createPawaDev = () => {
64
63
  logComponent(n, t) { /* preserve */ }
65
64
  };
66
65
  return dev;
67
- };
66
+ };
@@ -1,4 +1,3 @@
1
- import { getComponentGraph } from "../component/index.js";
2
1
  import { customEventMap } from "../global.js";
3
2
  import { getContexters, safeEval } from "../utils.js";
4
3
 
@@ -1,3 +1,5 @@
1
+ import { getDev } from "../dev/index.js"
2
+
1
3
  export const Graph=(graph)=>{
2
4
  const newGraph={
3
5
  ref:null,
@@ -19,7 +21,7 @@ export const Graph=(graph)=>{
19
21
  onEnter:graph?.onEnter ?[...graph?.onEnter] : [],
20
22
  transport:{},
21
23
  former:null,
22
- onExit:graph?.onExit ?[...graph?.onExit] : [],
24
+ onExit:graph?.onEnter ?[...graph?.onExit] : [],
23
25
  unMount:[],
24
26
  effect:[],
25
27
  reProps:[],
@@ -37,6 +39,26 @@ export const Graph=(graph)=>{
37
39
  element()
38
40
  }
39
41
  },
42
+ insertAfter: (node) => {
43
+ if (newGraph.nodeType === 'template') {
44
+ for (const element of newGraph.children) {
45
+
46
+ if (element.nodeType === 8) {
47
+ comment.parentElement.after(node)
48
+ }else if (element.nodeType === 1) {
49
+ comment.parentElement.after(node)
50
+ }else{
51
+ element.insertAfter(node)
52
+ }
53
+ }
54
+ } else {
55
+ if (newGraph.ref.nodeType === 8 || newGraph.ref.nodeType === 1) {
56
+ node.after(newGraph.ref)
57
+ } else {
58
+ newGraph.ref.insertAfter(node)
59
+ }
60
+ }
61
+ },
40
62
  move:(comment)=>{
41
63
  if (newGraph.nodeType === 'template') {
42
64
  for (const element of newGraph.children) {
@@ -151,6 +173,8 @@ export const Graph=(graph)=>{
151
173
  graph.ref=newGraph
152
174
  }
153
175
  graph?.children?.push(newGraph)
154
-
176
+ if (!getDev()) {
177
+ newGraph.context={}
178
+ }
155
179
  return newGraph
156
180
  }
@@ -176,31 +176,6 @@ export const $state = (initialValue, section = null) => {
176
176
  }
177
177
  else if (isFunction && initialValue[Symbol.toStringTag] === 'AsyncFunction') {
178
178
  states.value=null
179
- let result
180
-
181
- const setup=(result)=>{
182
- if(Array.isArray(result)) {
183
- let value=result.filter(r=> r.value.name === section)
184
- value=value[0]
185
- value=value.value
186
- if (value?.success) {
187
- if (enter) {
188
- main.value=value.success
189
- main.async=false
190
- main.failed=false
191
- }else{
192
-
193
- states.value=value.success
194
- states.async=false
195
- states.failed=false
196
- }
197
- }else{
198
- if(typeof main !== 'undefined' && enter)main.failed=true
199
- else states.failed=true
200
- }
201
-
202
- }
203
- }
204
179
  const id=graph?.id
205
180
 
206
181
  if (!id) {
@@ -209,19 +184,6 @@ export const $state = (initialValue, section = null) => {
209
184
  states.async = true
210
185
  states.failed = false
211
186
 
212
- }else{
213
- if (window.awaits?.[id]) {
214
- setup(window.awaits[id])
215
- }else{
216
- states.value=result
217
- states.async = true
218
- states.failed = false
219
- if(!window.states[id])
220
- {
221
- window.states[id]=[]
222
- }
223
- window.states[id].push(setup)
224
- }
225
187
  }
226
188
 
227
189
  }
@@ -250,8 +212,46 @@ export const $state = (initialValue, section = null) => {
250
212
  console.error('state compute must be inside a component and initialValue must be a function')
251
213
  }
252
214
  }
253
- if (promise instanceof Promise) {
254
- handlePromise(promise, main)
215
+ if (isFunction && initialValue[Symbol.toStringTag] === 'AsyncFunction') {
216
+
217
+ if (!graph?.id) {
218
+ handlePromise(promise, main)
219
+ }else{
220
+
221
+ let result
222
+ const setup=(result)=>{
223
+ if(Array.isArray(result)) {
224
+ let value=result.filter(r=> r.value.name === section)
225
+ value=value[0]
226
+ value=value.value
227
+ if (value?.success) {
228
+
229
+ main.value=value.success
230
+ main.async=false
231
+ main.failed=false
232
+
233
+ }else{
234
+ main.failed=true
235
+ main.value=value.error
236
+ }
237
+
238
+ }
239
+ }
240
+ const graphId=graph?.id
241
+ if (window.awaits?.[graphId]) {
242
+ // console.log(window.awaits[id]);
243
+ setup(window.awaits[graphId])
244
+ }else{
245
+ states.value=result
246
+ states.async = true
247
+ states.failed = false
248
+ if(!window.states[graphId])
249
+ {
250
+ window.states[graphId]=[]
251
+ }
252
+ window.states[graphId].push(setup)
253
+ }
254
+ }
255
255
 
256
256
  const asyncObject = {
257
257
  retry: () => {
File without changes