@alixex/ranview 0.1.0-alpha.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","names":[],"sources":["../src/signal.ts"],"sourcesContent":["/**\n * Reactive primitives — signal / computed / createEffect / batch, plus an\n * ownership layer (createRoot / onCleanup / getOwner / runWithOwner) and untrack.\n *\n * Design reference: SwiftUI @Observable + Solid.js fine-grained reactivity.\n *\n * View = f(State)\n * signal — reactive source (@State)\n * computed — derived, LAZY + memoized (SwiftUI computed property / Solid memo)\n * createEffect— side effect that re-runs when its reads change (≈ body)\n * ownership — effects/memos form an owner tree; disposing a scope disposes\n * everything created under it (critical for MPA: tear down a page\n * and every effect it spawned is cleaned up in one call).\n *\n * Model (push-pull scheduling + lazy memos):\n * - Reading a source inside a computation links them both ways.\n * - Writing a signal marks direct observers DIRTY and their transitive memo\n * observers CHECK (\"maybe dirty\"), scheduling any dependent effects. Effects\n * flush once (deduped — diamond dependencies run an effect a single time).\n * - Memos recompute lazily on read, and only re-notify their observers when\n * their derived value actually changes (via `equals`). A CHECK computation\n * first pulls its sources up to date; if none changed value, it is skipped —\n * so a memo whose output is stable never wakes the effects behind it.\n * - Every computation is owned; re-running or disposing it first disposes its\n * owned children and runs its cleanups (no leaked nested effects).\n */\n\n// ── Types ───────────────────────────────────────────────────────────────────\n\nexport type Getter<T> = () => T;\nexport type Setter<T> = (value: T | ((prev: T) => T)) => void;\n\nexport interface SignalOptions<T> {\n /** Custom equality. Defaults to Object.is. Return true to skip the update. */\n equals?: (prev: T, next: T) => boolean;\n}\n\n/** Anything a computation can subscribe to (a signal, or a memo). */\ninterface SourceLike {\n observers: Set<Computation>;\n}\n\n/** Staleness of a computation. CLEAN → up to date; CHECK → a memo source may\n * have changed (verify before recomputing); DIRTY → definitely recompute. */\nconst CLEAN = 0;\nconst CHECK = 1;\nconst DIRTY = 2;\ntype State = typeof CLEAN | typeof CHECK | typeof DIRTY;\n\n/** An effect or a memo — a node that runs a function and tracks its reads. */\ninterface Computation extends Owner {\n fn: () => unknown;\n /** Sources (signals / memos) read during the last run. */\n sources: Set<SourceLike>;\n isEffect: boolean;\n running: boolean;\n disposed: boolean;\n state: State;\n // Memo-only fields:\n observers: Set<Computation>; // downstream that read this memo\n value: unknown;\n equals: (a: unknown, b: unknown) => boolean; // gates observer notification\n}\n\n/** A node that can own child computations + cleanups (effect, memo, or root). */\nexport interface Owner {\n owned: Computation[];\n cleanups: Array<() => void>;\n owner: Owner | null;\n}\n\n// ── Global tracking state ────────────────────────────────────────────────────\n\n/** The computation currently running — reads link to it. Null = untracked. */\nlet currentObserver: Computation | null = null;\n/** The owner scope currently active — new computations/cleanups attach here. */\nlet currentOwner: Owner | null = null;\n/** >0 while inside batch(): effect flushing is deferred. */\nlet batchDepth = 0;\n/** Effects queued for the next flush (a Set → each runs once per flush). */\nconst scheduled = new Set<Computation>();\n/** Guard against re-entrant flushes (an effect writing a signal mid-flush). */\nlet flushing = false;\n\n// ── signal ───────────────────────────────────────────────────────────────────\n\n/**\n * Create a reactive value. Returns a `[getter, setter]` tuple.\n *\n * const [count, setCount] = signal(0);\n * count(); // read — auto-tracked inside computed / createEffect\n * setCount(1); // write — notifies dependents; skipped if unchanged\n * setCount((n) => n + 1) // updater form\n */\nexport function signal<T>(initial: T, options?: SignalOptions<T>): [Getter<T>, Setter<T>] {\n const equals = options?.equals ?? Object.is;\n const node: SourceLike & { value: T } = { value: initial, observers: new Set() };\n\n const get: Getter<T> = () => {\n track(node);\n return node.value;\n };\n\n const set: Setter<T> = (next) => {\n const value = typeof next === 'function' ? (next as (prev: T) => T)(node.value) : next;\n if (equals(node.value, value)) return;\n node.value = value;\n // markStale only flags/schedules — it never mutates node.observers mid-loop.\n for (const observer of node.observers) markStale(observer, DIRTY);\n if (batchDepth === 0) flush();\n };\n\n return [get, set];\n}\n\n// ── computed (lazy memo) ─────────────────────────────────────────────────────\n\n/**\n * Derived, read-only, LAZY + memoized value. Recomputes only when read after a\n * dependency changed; unread memos never recompute. When a recompute produces an\n * equal value (default `Object.is`, override via `options.equals`) the memo does\n * NOT notify its observers — so effects behind a value-stable memo stay asleep.\n * Owned by the current scope, so it is disposed when that scope is.\n *\n * const fullName = computed(() => `${first()} ${last()}`);\n * fullName(); // computes on first read, caches until a dep's value changes\n */\nexport function computed<T>(fn: () => T, options?: SignalOptions<T>): Getter<T> {\n const node = makeComputation(fn, false, options?.equals as ((a: unknown, b: unknown) => boolean) | undefined);\n adopt(node);\n\n return () => {\n if (node.disposed) return node.value as T;\n updateIfNecessary(node); // recompute only if actually stale\n track(node); // link the reader to this memo\n return node.value as T;\n };\n}\n\n// ── createEffect ──────────────────────────────────────────────────────────────\n\n/**\n * Run `fn` immediately, then re-run it whenever a signal/memo it read changes.\n * Owned by the current scope. `fn` may return a cleanup, called before each\n * re-run and on dispose. Returns a dispose function.\n *\n * const dispose = createEffect(() => {\n * el.textContent = `Count: ${count()}`;\n * return () => {}; // optional cleanup\n * });\n */\nexport function createEffect(fn: () => void | (() => void)): () => void {\n const node = makeComputation(fn, true);\n adopt(node);\n runComputation(node);\n return () => disposeComputation(node);\n}\n\n// ── batch ─────────────────────────────────────────────────────────────────────\n\n/**\n * Coalesce multiple writes into one flush. Effects run once after `fn` returns,\n * deduplicated. Nested batches are absorbed by the outermost one.\n */\nexport function batch(fn: () => void): void {\n batchDepth++;\n try {\n fn();\n } finally {\n batchDepth--;\n if (batchDepth === 0) flush();\n }\n}\n\n// ── untrack ────────────────────────────────────────────────────────────────────\n\n/** Read signals without subscribing the current computation to them. */\nexport function untrack<T>(fn: () => T): T {\n const prev = currentObserver;\n currentObserver = null;\n try {\n return fn();\n } finally {\n currentObserver = prev;\n }\n}\n\n// ── Ownership ───────────────────────────────────────────────────────────────\n\n/**\n * Create a disposable reactive scope. Effects/memos created inside are owned by\n * it; calling the passed `dispose` tears them all down. Use one per page/route\n * in an MPA/SPA so navigating away cleans up every effect that page spawned.\n *\n * const dispose = createRoot((dispose) => { renderPage(); return dispose; });\n * // ...later, on navigation:\n * dispose();\n */\nexport function createRoot<T>(fn: (dispose: () => void) => T): T {\n const root: Owner = { owned: [], cleanups: [], owner: currentOwner };\n const prevOwner = currentOwner;\n const prevObserver = currentObserver;\n currentOwner = root;\n currentObserver = null; // roots do not track\n try {\n return fn(() => disposeOwner(root));\n } finally {\n currentOwner = prevOwner;\n currentObserver = prevObserver;\n }\n}\n\n/** Register a cleanup on the current scope, run when it re-runs or is disposed. */\nexport function onCleanup(fn: () => void): void {\n if (currentOwner) currentOwner.cleanups.push(fn);\n}\n\n/** The current owner scope (for advanced integrations, e.g. a router). */\nexport function getOwner(): Owner | null {\n return currentOwner;\n}\n\n/** Run `fn` under a specific owner scope (pairs with getOwner). */\nexport function runWithOwner<T>(owner: Owner | null, fn: () => T): T {\n const prevOwner = currentOwner;\n const prevObserver = currentObserver;\n currentOwner = owner;\n currentObserver = null;\n try {\n return fn();\n } finally {\n currentOwner = prevOwner;\n currentObserver = prevObserver;\n }\n}\n\n// ── Internals ────────────────────────────────────────────────────────────────\n\nfunction makeComputation(\n fn: () => unknown,\n isEffect: boolean,\n equals?: (a: unknown, b: unknown) => boolean,\n): Computation {\n return {\n fn,\n sources: new Set(),\n isEffect,\n running: false,\n disposed: false,\n state: DIRTY, // never run yet\n observers: new Set(),\n value: undefined,\n equals: equals ?? Object.is,\n owned: [],\n cleanups: [],\n owner: currentOwner,\n };\n}\n\n/** Attach a computation to the current owner so it is disposed with the scope. */\nfunction adopt(node: Computation): void {\n if (currentOwner) currentOwner.owned.push(node);\n}\n\nfunction track(source: SourceLike): void {\n if (currentObserver) {\n currentObserver.sources.add(source);\n source.observers.add(currentObserver);\n }\n}\n\n/**\n * Propagate a change. `state` is DIRTY when `node` directly read the changed\n * source, or CHECK when a memo upstream of it may have changed. Effects are\n * scheduled; a memo, the first time it leaves CLEAN, marks its own observers\n * CHECK so the staleness reaches the effects behind it. The `state >= incoming`\n * guard makes this idempotent (a DIRTY effect is not re-scheduled by a later\n * CHECK) and terminates diamonds.\n */\nfunction markStale(node: Computation, state: State): void {\n if (node.disposed || node.state >= state) return;\n const wasClean = node.state === CLEAN;\n node.state = state;\n if (node.isEffect) {\n scheduled.add(node);\n } else if (wasClean) {\n // A clean memo becoming stale: its observers are now \"maybe dirty\".\n // Recursion touches each observer's own observers, never this node's.\n for (const observer of node.observers) markStale(observer, CHECK);\n }\n}\n\nfunction flush(): void {\n if (batchDepth > 0 || flushing) return; // re-entrant writes drain via the loop below\n flushing = true;\n try {\n // Loop: an effect may schedule more effects as it runs.\n while (scheduled.size > 0) {\n const batchOfEffects = [...scheduled];\n scheduled.clear();\n for (const effect of batchOfEffects) {\n if (!effect.disposed) updateIfNecessary(effect);\n }\n }\n } finally {\n flushing = false;\n }\n}\n\n/**\n * Bring `node` up to date, recomputing only if it is genuinely dirty. A CHECK\n * node first pulls every memo source current: a source whose value changed marks\n * `node` DIRTY (via runComputation → markStale). If none did, `node` stays CHECK\n * and is skipped — this is what keeps a value-stable memo from waking effects.\n * All sources are refreshed (no early break) so none recompute mid-run and\n * spuriously re-schedule the node.\n */\nfunction updateIfNecessary(node: Computation): void {\n if (node.disposed || node.state === CLEAN) return;\n if (node.state === CHECK) {\n // Refreshing a source recomputes only that memo; it never mutates node.sources.\n for (const source of node.sources) {\n if ('fn' in source) updateIfNecessary(source as Computation); // only memos have `fn`\n }\n }\n if (node.state === DIRTY) runComputation(node);\n node.state = CLEAN;\n}\n\nfunction runComputation(node: Computation): void {\n if (node.running) {\n throw new Error('ranview reactivity: cyclic dependency detected (a computation triggered itself).');\n }\n cleanNode(node);\n\n const prevObserver = currentObserver;\n const prevOwner = currentOwner;\n currentObserver = node;\n currentOwner = node;\n node.running = true;\n // Mark clean up front: a genuine re-entrant write inside fn (a cycle) re-marks\n // and re-enters, tripping the `running` guard above. Memo sources are already\n // current here (updateIfNecessary refreshed them), so no benign re-mark occurs.\n node.state = CLEAN;\n try {\n const result = node.fn();\n if (node.isEffect) {\n if (typeof result === 'function') node.cleanups.push(result as () => void);\n } else {\n // memo: store value; notify observers only when the value actually changed.\n const prev = node.value;\n node.value = result;\n // Notify only when there are observers AND the value changed. Skipping the\n // empty-observer case also means `equals` never sees the initial `undefined`\n // sentinel (the first run always precedes any observer). markStale only\n // flags/schedules — it never mutates node.observers here.\n if (node.observers.size > 0 && !node.equals(prev, result)) {\n for (const observer of node.observers) markStale(observer, DIRTY);\n }\n }\n } finally {\n node.running = false;\n currentObserver = prevObserver;\n currentOwner = prevOwner;\n }\n}\n\n/** Dispose owned children, run cleanups, and unlink from all sources. */\nfunction cleanNode(node: Computation): void {\n for (const child of node.owned) disposeComputation(child);\n node.owned.length = 0;\n\n for (const cleanup of node.cleanups) cleanup();\n node.cleanups.length = 0;\n\n for (const source of node.sources) source.observers.delete(node);\n node.sources.clear();\n}\n\nfunction disposeComputation(node: Computation): void {\n if (node.disposed) return;\n node.disposed = true;\n cleanNode(node);\n node.observers.clear();\n}\n\nfunction disposeOwner(owner: Owner): void {\n for (const child of owner.owned) disposeComputation(child);\n owner.owned.length = 0;\n for (const cleanup of owner.cleanups) cleanup();\n owner.cleanups.length = 0;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@alixex/ranview",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Framework-free DOM construction with fine-grained reactivity — build once, update in place, in a browser or to a string",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.cjs"
11
+ },
12
+ "./signal": {
13
+ "types": "./dist/signal.d.ts",
14
+ "import": "./dist/signal.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./mocks": {
18
+ "types": "./dist/mocks.d.ts",
19
+ "import": "./dist/mocks.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/chaxus/ran",
26
+ "directory": "packages/ranview"
27
+ },
28
+ "engines": {
29
+ "node": ">=24.0.0"
30
+ },
31
+ "author": "chaxus",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "ranuts": "0.4.1-alpha.2"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "26.1.2",
38
+ "jsdom": "30.0.1",
39
+ "typescript": "7.0.2",
40
+ "vite": "8.2.1",
41
+ "vitest": "4.1.11"
42
+ },
43
+ "keywords": [
44
+ "dom",
45
+ "reactive",
46
+ "signals",
47
+ "fine-grained",
48
+ "ssr",
49
+ "no-vdom",
50
+ "web-components"
51
+ ],
52
+ "sideEffects": false,
53
+ "files": [
54
+ "dist",
55
+ "README.md"
56
+ ],
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "scripts": {
61
+ "build": "vite build && tsc -p tsconfig.build.json",
62
+ "tsc": "tsc --noEmit",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest"
65
+ },
66
+ "main": "./dist/index.cjs",
67
+ "module": "./dist/index.js",
68
+ "types": "./dist/index.d.ts"
69
+ }