react-fine-grained-signals 0.1.0 → 0.2.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/LICENSE-ALIEN-SIGNALS.txt +21 -0
- package/README.ja.md +73 -6
- package/README.md +89 -6
- package/dist/base-WD_74mfC.js +55 -0
- package/dist/base-WD_74mfC.js.map +1 -0
- package/dist/core-runtime-2jSE9Tz5.js +1354 -0
- package/dist/core-runtime-2jSE9Tz5.js.map +1 -0
- package/dist/index.d.ts +20 -25
- package/dist/index.js +4 -4
- package/dist/{jsx-Xp5q2y97.d.ts → jsx-BQGrTir3.d.ts} +40 -5
- package/dist/jsx-P6volZdr.js +1833 -0
- package/dist/jsx-P6volZdr.js.map +1 -0
- package/dist/jsx-dev-runtime.d.ts +3 -3
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +4 -4
- package/dist/jsx-runtime.js +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +2 -2
- package/dist/use-signals-C1K8-vJX.js +169 -0
- package/dist/use-signals-C1K8-vJX.js.map +1 -0
- package/dist/use-signals-CJYcdFdL.d.ts +11 -0
- package/dist/utils.d.ts +14 -15
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +22 -18
- package/dist/base-BZTnpU8U.js +0 -286
- package/dist/base-BZTnpU8U.js.map +0 -1
- package/dist/hooks-BFIWh7AD.js +0 -914
- package/dist/hooks-BFIWh7AD.js.map +0 -1
- package/dist/jsx-D9U6dSwL.js +0 -730
- package/dist/jsx-D9U6dSwL.js.map +0 -1
- package/dist/render-tracking-DCzw57BW.js +0 -96
- package/dist/render-tracking-DCzw57BW.js.map +0 -1
- package/dist/use-signals-8i2B-94o.js +0 -173
- package/dist/use-signals-8i2B-94o.js.map +0 -1
- package/dist/use-signals-CBDbbAbV.d.ts +0 -24
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-signals-C1K8-vJX.js","names":[],"sources":["../src/react/react-adapter.mts","../src/react/use-signals.ts"],"sourcesContent":["/* React hooks backed by attempt-local render tracking. */\nimport { useEffect, useInsertionEffect, useLayoutEffect, useMemo, useSyncExternalStore } from \"react\";\nimport type { AlienDerivedRenderAdapter, RenderAttempt, RuntimeReadable } from \"../core/alien-derived-types.js\";\nimport type { ReadableProtocolV1 } from \"../core/execution-owner.js\";\n\ntype ScopePolicy = \"managed\" | \"bare\";\ntype SubscriptionMode = \"combined\" | \"per-readable\";\ninterface SnapshotEntry { readonly dependencies: Map<object, number>; readonly value: unknown }\ninterface SnapshotCache { readonly readable: object; current: SnapshotEntry | undefined }\ninterface ManagedScope { finish(): void; getSnapshot(): number }\n\nexport function createAlienDerivedReactAdapter(_core: unknown, render: AlienDerivedRenderAdapter) {\nfunction isInteropProtocol(dependency: object): dependency is ReadableProtocolV1 {\n return Reflect.get(dependency, \"version\") === 1\n && typeof Reflect.get(dependency, \"getRevision\") === \"function\"\n && typeof Reflect.get(dependency, \"subscribe\") === \"function\";\n}\nfunction dependencyVersion(dependency: object): number {\n return isInteropProtocol(dependency)\n ? dependency.getRevision()\n : render.getRenderVersion(dependency);\n}\nfunction subscribeDependency(dependency: object, notify: () => void): () => void {\n if (isInteropProtocol(dependency)) return dependency.subscribe(() => notify()).unsubscribe;\n const subscribeRender = Reflect.get(dependency, \"subscribeRender\");\n if (typeof subscribeRender === \"function\") return subscribeRender.call(dependency, notify) as () => void;\n return render.subscribeReadables([dependency], notify);\n}\n\nfunction snapshot<T>(readable: RuntimeReadable<T>, cache: SnapshotCache): T {\n const captured = render.captureRenderSnapshot(readable);\n const current = cache.current;\n const same = current !== undefined\n && current.dependencies.size === captured.dependencies.size\n && [...captured.dependencies].every(([dependency, revision]) =>\n current.dependencies.get(dependency) === revision\n && dependencyVersion(dependency) === revision);\n if (same) return current.value as T;\n cache.current = captured;\n return captured.value as T;\n}\n\nclass RenderStore {\n #mode: SubscriptionMode;\n #subscriptions = new Map<object, () => void>();\n #combinedDependencies = new Set<object>();\n #listeners = new Set<() => void>();\n #version = 0;\n // Advanced only by lifecycle events that prove the component is mounted and\n // visible again: a layout commit and a passive (re)activation. A render on\n // its own, such as a hidden Activity prerender, must not cancel a scheduled\n // dispose: a hidden tree never commits or activates, and one deleted while\n // hidden gets no second passive cleanup.\n #lifecycle = 0;\n\n constructor(mode: SubscriptionMode) { this.#mode = mode; }\n getSnapshot = () => this.#version;\n subscribe = (listener: () => void) => {\n this.#listeners.add(listener);\n return () => this.#listeners.delete(listener);\n };\n begin(scopePolicy: ScopePolicy): RenderAttempt {\n const attempt = render.createRenderAttempt();\n attempt.restoreScope = render.pushRenderScope(attempt, scopePolicy);\n queueMicrotask(() => this.finish(attempt));\n return attempt;\n }\n finish(attempt: RenderAttempt): void {\n attempt?.restoreScope?.();\n attempt.restoreScope = undefined;\n }\n commit(attempt: RenderAttempt): void {\n this.#lifecycle += 1;\n this.finish(attempt);\n let changedDuringRender = !render.promoteRenderAttempt(attempt);\n const desired = attempt.dependencies;\n if (this.#mode === \"combined\") {\n const local = new Set([...desired.keys()].filter((dependency) => !isInteropProtocol(dependency)));\n const same = local.size === this.#combinedDependencies.size\n && [...local].every((dependency) => this.#combinedDependencies.has(dependency));\n if (!same) {\n this.#subscriptions.get(this)?.();\n this.#subscriptions.delete(this);\n this.#combinedDependencies = local;\n if (local.size > 0) this.#subscriptions.set(this, render.subscribeReadables([...local], this.#notify));\n }\n for (const [dependency, unsubscribe] of this.#subscriptions) {\n if (dependency !== this && (!isInteropProtocol(dependency) || !desired.has(dependency))) {\n unsubscribe();\n this.#subscriptions.delete(dependency);\n }\n }\n for (const dependency of desired.keys()) {\n if (isInteropProtocol(dependency) && !this.#subscriptions.has(dependency)) {\n this.#subscriptions.set(dependency, subscribeDependency(dependency, this.#notify));\n }\n }\n } else {\n for (const [dependency, unsubscribe] of this.#subscriptions) {\n if (!desired.has(dependency)) {\n unsubscribe();\n this.#subscriptions.delete(dependency);\n }\n }\n for (const dependency of desired.keys()) {\n if (!this.#subscriptions.has(dependency)) {\n this.#subscriptions.set(dependency, subscribeDependency(dependency, this.#notify));\n }\n }\n }\n if (!render.settleRenderAttempt(attempt)) changedDuringRender = true;\n if (changedDuringRender) this.#notify();\n }\n #notify = (): void => {\n this.#version = (this.#version + 1) | 0;\n for (const listener of this.#listeners) listener();\n };\n dispose(): void {\n for (const unsubscribe of this.#subscriptions.values()) unsubscribe();\n this.#subscriptions.clear();\n this.#combinedDependencies.clear();\n }\n scheduleDispose(): void {\n const lifecycle = ++this.#lifecycle;\n queueMicrotask(() => { if (this.#lifecycle === lifecycle) this.dispose(); });\n }\n activate(): void { this.#lifecycle += 1; }\n}\n\nfunction createReactAdapter(\n runtimeOrOptions: { readonly mode?: SubscriptionMode } = {},\n maybeOptions: { readonly mode?: SubscriptionMode } = {},\n) {\n const mode = maybeOptions.mode ?? runtimeOrOptions.mode ?? \"combined\";\n if (mode !== \"combined\" && mode !== \"per-readable\") throw new Error(`Unknown render subscription mode: ${mode}`);\n function useSignalValue<T>(readable: RuntimeReadable<T>): T {\n const subscribe = useMemo(() => (notify: () => void) => subscribeDependency(readable, notify), [readable]);\n const cache = useMemo<SnapshotCache>(() => ({ readable, current: undefined }), [readable]);\n const getSnapshot = useMemo(() => () => snapshot(readable, cache), [readable, cache]);\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n }\n function useTrackingStore(scopePolicy: ScopePolicy): { store: RenderStore; attempt: RenderAttempt } {\n const store = useMemo(() => new RenderStore(mode), []);\n useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n const attempt = store.begin(scopePolicy);\n // A bare scope deliberately stays open after this component returns, so\n // descendants rendered without their own scope are still attributed to it.\n // Rendering is over once React starts committing, though, and every\n // insertion effect in the tree runs before any layout effect or ref\n // callback. Closing here keeps descendants' layout effects and refs (which\n // run before this component's own layout effect) out of the render attempt.\n useInsertionEffect(() => { store.finish(attempt); }, [store, attempt]);\n useLayoutEffect(() => { store.commit(attempt); }, [store, attempt]);\n useEffect(() => {\n store.activate();\n return () => store.scheduleDispose();\n }, [store]);\n return { store, attempt };\n }\n function useSignalTracking(): void { useTrackingStore(\"bare\"); }\n function useManagedSignals(): ManagedScope {\n const { store, attempt } = useTrackingStore(\"managed\");\n return { finish: () => store.finish(attempt), getSnapshot: store.getSnapshot };\n }\n return { useSignalValue, useSignalTracking, useManagedSignals };\n}\n\nfunction managed<T>(scope: ManagedScope, callback: () => T): T {\n try { return callback(); }\n finally { scope.finish(); }\n}\n\n return { createReactAdapter, managed };\n}\n","import { coreRuntime } from \"../core/core-runtime.js\";\nimport { createAlienDerivedReactAdapter } from \"./react-adapter.mjs\";\n\nconst reactAdapter = createAlienDerivedReactAdapter(coreRuntime, coreRuntime.renderAdapter).createReactAdapter();\n\n/** The render-scope handle consumed by the source transform runtime. */\nexport interface ManagedSignalsStore {\n finish(): void;\n}\n\n/** Best-effort render tracking for untransformed components. */\nexport function useSignalTracking(): void {\n reactAdapter.useSignalTracking();\n}\n\n/** Exact render scope used by the transform-managed boundary. */\nexport function useManagedSignals(): ManagedSignalsStore {\n return reactAdapter.useManagedSignals();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/react/use-signals.d.ts
|
|
2
|
+
/** The render-scope handle consumed by the source transform runtime. */
|
|
3
|
+
interface ManagedSignalsStore {
|
|
4
|
+
finish(): void;
|
|
5
|
+
}
|
|
6
|
+
/** Best-effort render tracking for untransformed components. */
|
|
7
|
+
declare function useSignalTracking(): void;
|
|
8
|
+
/** Exact render scope used by the transform-managed boundary. */
|
|
9
|
+
declare function useManagedSignals(): ManagedSignalsStore;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { useManagedSignals as n, useSignalTracking as r, ManagedSignalsStore as t };
|
package/dist/utils.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { t as ReadonlySignal } from "./base-Bx5ahfW2.js";
|
|
|
2
2
|
import { Key, ReactNode } from "react";
|
|
3
3
|
//#region src/utils.d.ts
|
|
4
4
|
/** A plain value or a value created by `signal`, `computed`, or `deepSignal`. */
|
|
5
|
-
type SignalInput<T> = T | ReadonlySignal<T>;
|
|
5
|
+
export type SignalInput<T> = T | ReadonlySignal<T>;
|
|
6
6
|
/** Renders its children only while `when` is truthy. */
|
|
7
|
-
interface ShowProps<T> {
|
|
7
|
+
export interface ShowProps<T> {
|
|
8
8
|
when: SignalInput<T>;
|
|
9
9
|
fallback?: ReactNode;
|
|
10
10
|
children: ReactNode | ((value: NonNullable<T>) => ReactNode);
|
|
@@ -15,29 +15,29 @@ interface ShowProps<T> {
|
|
|
15
15
|
* Unlike Solid's compiler-aware control flow, this is a normal React component:
|
|
16
16
|
* only this component rerenders when a signal read from `when` changes.
|
|
17
17
|
*/
|
|
18
|
-
declare function Show<T>({ when, fallback, children }: ShowProps<T>): ReactNode;
|
|
18
|
+
export declare function Show<T>({ when, fallback, children }: ShowProps<T>): ReactNode;
|
|
19
19
|
/** A branch declaration consumed by the nearest `Switch`. */
|
|
20
|
-
interface MatchProps<T> {
|
|
20
|
+
export interface MatchProps<T> {
|
|
21
21
|
when: SignalInput<T>;
|
|
22
22
|
children: ReactNode | ((value: NonNullable<T>) => ReactNode);
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Declares a `Switch` branch. `Match` only has meaning as a child of `Switch`.
|
|
26
26
|
*/
|
|
27
|
-
declare function Match<T>(_props: MatchProps<T>): null;
|
|
27
|
+
export declare function Match<T>(_props: MatchProps<T>): null;
|
|
28
28
|
/** Chooses and renders the first truthy child `Match`. */
|
|
29
|
-
interface SwitchProps {
|
|
29
|
+
export interface SwitchProps {
|
|
30
30
|
fallback?: ReactNode;
|
|
31
31
|
children?: ReactNode;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* A small reactive multi-branch boundary inspired by Solid's `Switch`/`Match`.
|
|
35
35
|
*/
|
|
36
|
-
declare function Switch({ fallback, children }: SwitchProps): ReactNode;
|
|
36
|
+
export declare function Switch({ fallback, children }: SwitchProps): ReactNode;
|
|
37
37
|
/** A collection supported by `For` when each item has its own identity. */
|
|
38
|
-
type ForCollection<T> = readonly T[] | ReadonlySet<T>;
|
|
38
|
+
export type ForCollection<T> = readonly T[] | ReadonlySet<T>;
|
|
39
39
|
/** Renders an array or set whose items have stable identities. */
|
|
40
|
-
interface ForProps<T> {
|
|
40
|
+
export interface ForProps<T> {
|
|
41
41
|
each: SignalInput<ForCollection<T> | null | undefined>;
|
|
42
42
|
fallback?: ReactNode;
|
|
43
43
|
/** Returns the stable React key for an item. It must be pure and data-derived. */
|
|
@@ -45,7 +45,7 @@ interface ForProps<T> {
|
|
|
45
45
|
children: (item: T, index: number) => ReactNode;
|
|
46
46
|
}
|
|
47
47
|
/** Renders a map whose entries have stable identities. */
|
|
48
|
-
interface ForMapProps<K, V> {
|
|
48
|
+
export interface ForMapProps<K, V> {
|
|
49
49
|
each: SignalInput<ReadonlyMap<K, V> | null | undefined>;
|
|
50
50
|
fallback?: ReactNode;
|
|
51
51
|
/** Returns the stable React key for an entry. It must be pure and data-derived. */
|
|
@@ -58,10 +58,10 @@ interface ForMapProps<K, V> {
|
|
|
58
58
|
* React still owns reconciliation. `by` supplies stable keys, so use `Index`
|
|
59
59
|
* instead when a list's identity is intentionally positional.
|
|
60
60
|
*/
|
|
61
|
-
declare function For<T>(props: ForProps<T>): ReactNode;
|
|
62
|
-
declare function For<K, V>(props: ForMapProps<K, V>): ReactNode;
|
|
61
|
+
export declare function For<T>(props: ForProps<T>): ReactNode;
|
|
62
|
+
export declare function For<K, V>(props: ForMapProps<K, V>): ReactNode;
|
|
63
63
|
/** Renders an array whose row identity is intentionally its position. */
|
|
64
|
-
interface IndexProps<T> {
|
|
64
|
+
export interface IndexProps<T> {
|
|
65
65
|
each: SignalInput<readonly T[] | null | undefined>;
|
|
66
66
|
fallback?: ReactNode;
|
|
67
67
|
/** Receives a render-time accessor for the current value at this position. */
|
|
@@ -73,6 +73,5 @@ interface IndexProps<T> {
|
|
|
73
73
|
* The accessor should be read during render. For identity-keyed lists, use
|
|
74
74
|
* `For` instead and provide `by`.
|
|
75
75
|
*/
|
|
76
|
-
declare function Index<T>({ each, fallback, children }: IndexProps<T>): ReactNode;
|
|
76
|
+
export declare function Index<T>({ each, fallback, children }: IndexProps<T>): ReactNode;
|
|
77
77
|
//#endregion
|
|
78
|
-
export { For, ForCollection, ForMapProps, ForProps, Index, IndexProps, Match, MatchProps, Show, ShowProps, SignalInput, Switch, SwitchProps };
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as useManagedSignals } from "./use-signals-
|
|
1
|
+
import { a as isSignal } from "./base-WD_74mfC.js";
|
|
2
|
+
import { t as useManagedSignals } from "./use-signals-C1K8-vJX.js";
|
|
3
3
|
import { Children, Fragment, isValidElement } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/utils.tsx
|
|
@@ -16,7 +16,7 @@ function Show({ when, fallback = null, children }) {
|
|
|
16
16
|
if (!value) return fallback;
|
|
17
17
|
return typeof children === "function" ? children(value) : children;
|
|
18
18
|
} finally {
|
|
19
|
-
store.
|
|
19
|
+
store.finish();
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
@@ -39,7 +39,7 @@ function Switch({ fallback = null, children }) {
|
|
|
39
39
|
}
|
|
40
40
|
return fallback;
|
|
41
41
|
} finally {
|
|
42
|
-
store.
|
|
42
|
+
store.finish();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
function For({ each, fallback = null, by, children }) {
|
|
@@ -51,7 +51,7 @@ function For({ each, fallback = null, by, children }) {
|
|
|
51
51
|
if (items.length === 0) return fallback;
|
|
52
52
|
return items.map((item, index) => /* @__PURE__ */ jsx(Fragment, { children: children(item, index) }, by(item, index)));
|
|
53
53
|
} finally {
|
|
54
|
-
store.
|
|
54
|
+
store.finish();
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
@@ -68,7 +68,7 @@ function Index({ each, fallback = null, children }) {
|
|
|
68
68
|
const readAt = (index) => (readSignalInput(each) ?? items)[index];
|
|
69
69
|
return items.map((_item, index) => /* @__PURE__ */ jsx(Fragment, { children: children(() => readAt(index), index) }, index));
|
|
70
70
|
} finally {
|
|
71
|
-
store.
|
|
71
|
+
store.finish();
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
function readSignalInput(value) {
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.tsx"],"sourcesContent":["import {\n Children,\n Fragment,\n isValidElement,\n type Key,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { isSignal, type ReadonlySignal } from \"./core/index.js\";\nimport { useManagedSignals } from \"./react/use-signals.js\";\n\n/*\n * These four components use the *managed* render scope rather than the\n * best-effort `
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.tsx"],"sourcesContent":["import {\n Children,\n Fragment,\n isValidElement,\n type Key,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { isSignal, type ReadonlySignal } from \"./core/index.js\";\nimport { useManagedSignals } from \"./react/use-signals.js\";\n\n/*\n * These four components use the *managed* render scope rather than the\n * best-effort `useSignalTracking()`. The library owns their entire function body, so\n * an exact boundary — open at the top, closed in a `finally` before the\n * component returns — costs nothing here, while the `useSignalTracking()` scope can stay\n * open past the return (until the next `useSignalTracking()` call, a layout effect, or\n * a microtask) and misattribute a *sibling's* render-time signal read to this\n * component's store, which then silently stops updating the sibling. Every\n * branch and early return must stay inside the `try`. See\n * development/design/use-signals-boundary-design.md.\n */\n\n/** A plain value or a value created by `signal`, `computed`, or `deepSignal`. */\nexport type SignalInput<T> = T | ReadonlySignal<T>;\n\n/** Renders its children only while `when` is truthy. */\nexport interface ShowProps<T> {\n when: SignalInput<T>;\n fallback?: ReactNode;\n children: ReactNode | ((value: NonNullable<T>) => ReactNode);\n}\n\n/**\n * A small reactive conditional-rendering boundary.\n *\n * Unlike Solid's compiler-aware control flow, this is a normal React component:\n * only this component rerenders when a signal read from `when` changes.\n */\nexport function Show<T>({ when, fallback = null, children }: ShowProps<T>): ReactNode {\n const store = useManagedSignals();\n try {\n const value = readSignalInput(when);\n\n if (!value) return fallback;\n return typeof children === \"function\"\n ? children(value as NonNullable<T>)\n : children;\n } finally {\n store.finish();\n }\n}\n\n/** A branch declaration consumed by the nearest `Switch`. */\nexport interface MatchProps<T> {\n when: SignalInput<T>;\n children: ReactNode | ((value: NonNullable<T>) => ReactNode);\n}\n\n/**\n * Declares a `Switch` branch. `Match` only has meaning as a child of `Switch`.\n */\nexport function Match<T>(_props: MatchProps<T>): null {\n return null;\n}\n\n/** Chooses and renders the first truthy child `Match`. */\nexport interface SwitchProps {\n fallback?: ReactNode;\n children?: ReactNode;\n}\n\n/**\n * A small reactive multi-branch boundary inspired by Solid's `Switch`/`Match`.\n */\nexport function Switch({ fallback = null, children }: SwitchProps): ReactNode {\n const store = useManagedSignals();\n try {\n for (const match of collectMatches(children)) {\n const value = readSignalInput(match.props.when);\n if (!value) continue;\n\n const branch = match.props.children;\n return typeof branch === \"function\"\n ? branch(value as NonNullable<unknown>)\n : branch;\n }\n\n return fallback;\n } finally {\n store.finish();\n }\n}\n\n/** A collection supported by `For` when each item has its own identity. */\nexport type ForCollection<T> = readonly T[] | ReadonlySet<T>;\n\n/** Renders an array or set whose items have stable identities. */\nexport interface ForProps<T> {\n each: SignalInput<ForCollection<T> | null | undefined>;\n fallback?: ReactNode;\n /** Returns the stable React key for an item. It must be pure and data-derived. */\n by: (item: T, index: number) => Key;\n children: (item: T, index: number) => ReactNode;\n}\n\n/** Renders a map whose entries have stable identities. */\nexport interface ForMapProps<K, V> {\n each: SignalInput<ReadonlyMap<K, V> | null | undefined>;\n fallback?: ReactNode;\n /** Returns the stable React key for an entry. It must be pure and data-derived. */\n by: (entry: readonly [K, V], index: number) => Key;\n children: (entry: readonly [K, V], index: number) => ReactNode;\n}\n\n/**\n * A React list boundary inspired by Solid's `For`.\n *\n * React still owns reconciliation. `by` supplies stable keys, so use `Index`\n * instead when a list's identity is intentionally positional.\n */\nexport function For<T>(props: ForProps<T>): ReactNode;\nexport function For<K, V>(props: ForMapProps<K, V>): ReactNode;\nexport function For(\n { each, fallback = null, by, children }: ForProps<unknown> | ForMapProps<unknown, unknown>,\n): ReactNode {\n const store = useManagedSignals();\n try {\n const collection = readSignalInput(each);\n\n if (collection === null || collection === undefined) {\n return fallback;\n }\n\n const items = Array.isArray(collection) ? collection : Array.from(collection);\n if (items.length === 0) return fallback;\n\n return items.map((item, index) => (\n <Fragment key={by(item, index)}>{children(item, index)}</Fragment>\n ));\n } finally {\n store.finish();\n }\n}\n\n/** Renders an array whose row identity is intentionally its position. */\nexport interface IndexProps<T> {\n each: SignalInput<readonly T[] | null | undefined>;\n fallback?: ReactNode;\n /** Receives a render-time accessor for the current value at this position. */\n children: (item: () => T, index: number) => ReactNode;\n}\n\n/**\n * A position-keyed React list boundary inspired by Solid's `Index`.\n *\n * The accessor should be read during render. For identity-keyed lists, use\n * `For` instead and provide `by`.\n */\nexport function Index<T>({ each, fallback = null, children }: IndexProps<T>): ReactNode {\n const store = useManagedSignals();\n try {\n const items = readSignalInput(each);\n\n if (items === null || items === undefined || items.length === 0) {\n return fallback;\n }\n\n // Reads back through `each` on every call rather than closing over the\n // render-time `items` snapshot, so an accessor a child stores and calls\n // later (from an event handler, say) still sees the current value instead\n // of a stale row. The snapshot is only the fallback for a collection that\n // has since become null/undefined. `as T` stands in for the old `!`: an\n // out-of-range position reads as `undefined`, exactly as before, and a\n // sparse hole is no longer disguised as a present value.\n const readAt = (index: number): T =>\n (readSignalInput(each) ?? items)[index] as T;\n\n return items.map((_item, index) => (\n <Fragment key={index}>{children(() => readAt(index), index)}</Fragment>\n ));\n } finally {\n store.finish();\n }\n}\n\nfunction readSignalInput<T>(value: SignalInput<T>): T {\n return isSignal(value) ? (value.value as T) : value;\n}\n\nfunction collectMatches(children: ReactNode): Array<ReactElement<MatchProps<unknown>>> {\n const matches: Array<ReactElement<MatchProps<unknown>>> = [];\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return;\n if (child.type === Fragment) {\n const fragment = child as ReactElement<{ children?: ReactNode }>;\n matches.push(...collectMatches(fragment.props.children));\n } else if (child.type === Match) {\n matches.push(child as ReactElement<MatchProps<unknown>>);\n }\n });\n\n return matches;\n}\n"],"mappings":";;;;;;;;;;;AAuCA,SAAgB,KAAQ,EAAE,MAAM,WAAW,MAAM,YAAqC;CACpF,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,QAAQ,gBAAgB,IAAI;EAElC,IAAI,CAAC,OAAO,OAAO;EACnB,OAAO,OAAO,aAAa,aACvB,SAAS,KAAuB,IAChC;CACN,UAAU;EACR,MAAM,OAAO;CACf;AACF;;;;AAWA,SAAgB,MAAS,QAA6B;CACpD,OAAO;AACT;;;;AAWA,SAAgB,OAAO,EAAE,WAAW,MAAM,YAAoC;CAC5E,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,KAAK,MAAM,SAAS,eAAe,QAAQ,GAAG;GAC5C,MAAM,QAAQ,gBAAgB,MAAM,MAAM,IAAI;GAC9C,IAAI,CAAC,OAAO;GAEZ,MAAM,SAAS,MAAM,MAAM;GAC3B,OAAO,OAAO,WAAW,aACrB,OAAO,KAA6B,IACpC;EACN;EAEA,OAAO;CACT,UAAU;EACR,MAAM,OAAO;CACf;AACF;AA+BA,SAAgB,IACd,EAAE,MAAM,WAAW,MAAM,IAAI,YAClB;CACX,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,aAAa,gBAAgB,IAAI;EAEvC,IAAI,eAAe,QAAQ,eAAe,KAAA,GACxC,OAAO;EAGT,MAAM,QAAQ,MAAM,QAAQ,UAAU,IAAI,aAAa,MAAM,KAAK,UAAU;EAC5E,IAAI,MAAM,WAAW,GAAG,OAAO;EAE/B,OAAO,MAAM,KAAK,MAAM,UACtB,oBAAC,UAAD,EAAA,UAAiC,SAAS,MAAM,KAAK,EAAY,GAAlD,GAAG,MAAM,KAAK,CAAoC,CAClE;CACH,UAAU;EACR,MAAM,OAAO;CACf;AACF;;;;;;;AAgBA,SAAgB,MAAS,EAAE,MAAM,WAAW,MAAM,YAAsC;CACtF,MAAM,QAAQ,kBAAkB;CAChC,IAAI;EACF,MAAM,QAAQ,gBAAgB,IAAI;EAElC,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,MAAM,WAAW,GAC5D,OAAO;EAUT,MAAM,UAAU,WACb,gBAAgB,IAAI,KAAK,MAAA,CAAO;EAEnC,OAAO,MAAM,KAAK,OAAO,UACvB,oBAAC,UAAD,EAAA,UAAuB,eAAe,OAAO,KAAK,GAAG,KAAK,EAAY,GAAvD,KAAuD,CACvE;CACH,UAAU;EACR,MAAM,OAAO;CACf;AACF;AAEA,SAAS,gBAAmB,OAA0B;CACpD,OAAO,SAAS,KAAK,IAAK,MAAM,QAAc;AAChD;AAEA,SAAS,eAAe,UAA+D;CACrF,MAAM,UAAoD,CAAC;CAE3D,SAAS,QAAQ,WAAW,UAAU;EACpC,IAAI,CAAC,eAAe,KAAK,GAAG;EAC5B,IAAI,MAAM,SAAS,UAAU;GAC3B,MAAM,WAAW;GACjB,QAAQ,KAAK,GAAG,eAAe,SAAS,MAAM,QAAQ,CAAC;EACzD,OAAO,IAAI,MAAM,SAAS,OACxB,QAAQ,KAAK,KAA0C;CAE3D,CAAC;CAED,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-fine-grained-signals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fine-grained signal rendering for React 19, built on alien-signals.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "akazawa",
|
|
@@ -57,31 +57,33 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
|
-
"dist"
|
|
60
|
+
"dist",
|
|
61
|
+
"LICENSE-ALIEN-SIGNALS.txt"
|
|
61
62
|
],
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"alien-signals": "3.2.1"
|
|
65
|
+
},
|
|
62
66
|
"peerDependencies": {
|
|
63
|
-
"alien-signals": "^3.2.1",
|
|
64
67
|
"react": ">=19.0.0"
|
|
65
68
|
},
|
|
66
69
|
"devDependencies": {
|
|
67
|
-
"@playwright/test": "^1.
|
|
70
|
+
"@playwright/test": "^1.63.0",
|
|
68
71
|
"@preact/signals-core": "1.14.4",
|
|
69
72
|
"@testing-library/react": "^16.3.3",
|
|
70
|
-
"@types/node": "^24.
|
|
71
|
-
"@types/react": "^19.
|
|
72
|
-
"@types/react-dom": "^19.
|
|
73
|
-
"@vitest/coverage-v8": "
|
|
74
|
-
"alien-signals": "^3.2.1",
|
|
73
|
+
"@types/node": "^24.19.0",
|
|
74
|
+
"@types/react": "^19.3.0",
|
|
75
|
+
"@types/react-dom": "^19.3.0",
|
|
76
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
75
77
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
76
|
-
"jsdom": "^30.
|
|
77
|
-
"oxlint": "^1.
|
|
78
|
-
"react": "^19.
|
|
79
|
-
"react-dom": "^19.
|
|
80
|
-
"
|
|
78
|
+
"jsdom": "^30.1.1",
|
|
79
|
+
"oxlint": "^1.86.0",
|
|
80
|
+
"react": "^19.3.0",
|
|
81
|
+
"react-dom": "^19.3.0",
|
|
82
|
+
"react-fine-grained-signals-v0.1": "npm:react-fine-grained-signals@0.1.1",
|
|
83
|
+
"tsdown": "0.23.0",
|
|
81
84
|
"typescript": "^7.0.2",
|
|
82
|
-
"vite": "^8.
|
|
83
|
-
"vitest": "^
|
|
84
|
-
"node": "runtime:24.20.0"
|
|
85
|
+
"vite": "^8.3.1",
|
|
86
|
+
"vitest": "^5.0.2"
|
|
85
87
|
},
|
|
86
88
|
"scripts": {
|
|
87
89
|
"bench": "pnpm build:runtime && node --expose-gc benchmarks/core.mjs",
|
|
@@ -102,10 +104,12 @@
|
|
|
102
104
|
"test": "pnpm test:runtime && pnpm test:transform",
|
|
103
105
|
"test:runtime": "vitest run",
|
|
104
106
|
"test:transform": "pnpm --filter unplugin-react-fine-grained-signals test",
|
|
107
|
+
"test:phase4-duplicate": "node tests/phase4-duplicate-smoke.mjs",
|
|
108
|
+
"test:mixed-version": "pnpm build:runtime && node tests/mixed-version-smoke.mjs",
|
|
105
109
|
"test:coverage": "pnpm test:coverage:runtime && pnpm test:coverage:transform",
|
|
106
110
|
"test:coverage:runtime": "vitest run --coverage",
|
|
107
111
|
"test:coverage:transform": "pnpm --filter unplugin-react-fine-grained-signals test:coverage",
|
|
108
|
-
"test:consumer": "
|
|
112
|
+
"test:consumer": "node tests/consumer-smoke.mjs",
|
|
109
113
|
"prepare:e2e": "pnpm --dir examples/react-router install --frozen-lockfile",
|
|
110
114
|
"test:browser": "playwright test",
|
|
111
115
|
"size": "pnpm build:runtime && node scripts/check-size.mjs",
|
package/dist/base-BZTnpU8U.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { a as untrackedRender, n as hasActiveRenderCollector, t as RenderSubscription } from "./render-tracking-DCzw57BW.js";
|
|
2
|
-
import { computed, effect, endBatch, getActiveSub, setActiveSub, signal, startBatch, trigger } from "alien-signals";
|
|
3
|
-
//#region src/core/base.ts
|
|
4
|
-
const signalInstances = /* @__PURE__ */ new WeakSet();
|
|
5
|
-
/**
|
|
6
|
-
* Cross-instance signal brand. `Symbol.for` resolves through the registry that
|
|
7
|
-
* is shared by every realm, so a signal made by a duplicate copy of this
|
|
8
|
-
* package (pnpm hoisting, a monorepo consumer, an ESM/CJS split) or in another
|
|
9
|
-
* realm (iframe, worker) still answers `isSignal`. Shared with the deep-signal
|
|
10
|
-
* proxy only; the package's public API stays `isSignal`.
|
|
11
|
-
*/
|
|
12
|
-
const SIGNAL_BRAND = Symbol.for("react-fine-grained-signals.signal");
|
|
13
|
-
const SIGNAL_BRAND_VERSION = 1;
|
|
14
|
-
const SIGNAL_BRAND_MIN_VERSION = 1;
|
|
15
|
-
/** Marks an internal signal implementation for the public identity guard. */
|
|
16
|
-
function registerSignal(value) {
|
|
17
|
-
signalInstances.add(value);
|
|
18
|
-
Object.defineProperty(value, SIGNAL_BRAND, {
|
|
19
|
-
value: SIGNAL_BRAND_VERSION,
|
|
20
|
-
enumerable: false,
|
|
21
|
-
writable: false,
|
|
22
|
-
configurable: false
|
|
23
|
-
});
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
/** Returns whether a value came from this package's signal APIs, any copy. */
|
|
27
|
-
function isSignal(value) {
|
|
28
|
-
if (typeof value !== "object" || value === null) return false;
|
|
29
|
-
if (signalInstances.has(value)) return true;
|
|
30
|
-
const brand = value[SIGNAL_BRAND];
|
|
31
|
-
if (typeof brand !== "number" || brand < SIGNAL_BRAND_MIN_VERSION) return false;
|
|
32
|
-
return typeof value.peek === "function";
|
|
33
|
-
}
|
|
34
|
-
/** Internal writable signal implementation shared with deep signals. */
|
|
35
|
-
var SignalImpl = class {
|
|
36
|
-
#source;
|
|
37
|
-
#renderSubscription = new RenderSubscription();
|
|
38
|
-
#currentValue;
|
|
39
|
-
#watchedSinceWrite = false;
|
|
40
|
-
constructor(initialValue) {
|
|
41
|
-
this.#source = signal(initialValue);
|
|
42
|
-
this.#currentValue = initialValue;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Records that this signal was read while some subscriber (an alien-signals
|
|
46
|
-
* effect/computed, or a React render collector) was active. Cleared by the
|
|
47
|
-
* next write, so after that write's flush has drained, a still-false flag
|
|
48
|
-
* means every reactive subscriber has re-run without re-reading this signal
|
|
49
|
-
* and has therefore been unlinked from it.
|
|
50
|
-
*/
|
|
51
|
-
markWatched() {
|
|
52
|
-
this.#watchedSinceWrite = true;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Conservative "somebody still depends on me" test used to decide whether a
|
|
56
|
-
* `deepSignal` per-key version signal is safe to drop. Never reports `false`
|
|
57
|
-
* for a signal that still has a live dependent: the React side is exact
|
|
58
|
-
* (`hasListeners`), and the alien-signals side is covered by `markWatched`,
|
|
59
|
-
* whose flag can only be `false` once a write has notified every subscriber
|
|
60
|
-
* and none of them read this signal again.
|
|
61
|
-
*/
|
|
62
|
-
hasSubscribers() {
|
|
63
|
-
return this.#watchedSinceWrite || this.#renderSubscription.hasListeners();
|
|
64
|
-
}
|
|
65
|
-
get value() {
|
|
66
|
-
if (this.#renderSubscription.track()) return this.#currentValue;
|
|
67
|
-
this.#source();
|
|
68
|
-
return this.#currentValue;
|
|
69
|
-
}
|
|
70
|
-
set value(nextValue) {
|
|
71
|
-
if (Object.is(this.#currentValue, nextValue)) return;
|
|
72
|
-
const requiresManualTrigger = this.#currentValue === nextValue;
|
|
73
|
-
this.#currentValue = nextValue;
|
|
74
|
-
this.#watchedSinceWrite = false;
|
|
75
|
-
try {
|
|
76
|
-
if (requiresManualTrigger) trigger(this.#source);
|
|
77
|
-
else this.#source(nextValue);
|
|
78
|
-
} finally {
|
|
79
|
-
this.#renderSubscription.notify();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
peek() {
|
|
83
|
-
return this.#currentValue;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
/** Creates a writable reactive value. */
|
|
87
|
-
function signal$1(initialValue) {
|
|
88
|
-
return registerSignal(new SignalImpl(initialValue));
|
|
89
|
-
}
|
|
90
|
-
/** Unwraps a computed's box, rethrowing a cached getter error on read. */
|
|
91
|
-
function unbox(box) {
|
|
92
|
-
if ("error" in box) throw box.error;
|
|
93
|
-
return box.value;
|
|
94
|
-
}
|
|
95
|
-
/** Creates a lazily evaluated reactive value. */
|
|
96
|
-
function computed$1(getter) {
|
|
97
|
-
const source = computed((previous) => {
|
|
98
|
-
try {
|
|
99
|
-
const value = getter();
|
|
100
|
-
return previous !== void 0 && "value" in previous && Object.is(previous.value, value) ? previous : { value };
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return { error };
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
let disposeRenderBridge;
|
|
106
|
-
let isInitialBridgeRun = true;
|
|
107
|
-
let lastRenderValue;
|
|
108
|
-
const renderSubscription = new RenderSubscription(() => {
|
|
109
|
-
isInitialBridgeRun = true;
|
|
110
|
-
disposeRenderBridge = effect(() => {
|
|
111
|
-
const nextValue = untrackedRender(source);
|
|
112
|
-
const changedSinceRender = lastRenderValue !== void 0 && nextValue !== lastRenderValue;
|
|
113
|
-
lastRenderValue = nextValue;
|
|
114
|
-
if (isInitialBridgeRun) {
|
|
115
|
-
isInitialBridgeRun = false;
|
|
116
|
-
if (changedSinceRender) renderSubscription.bumpVersion();
|
|
117
|
-
} else if (changedSinceRender) renderSubscription.notify();
|
|
118
|
-
});
|
|
119
|
-
}, () => {
|
|
120
|
-
disposeRenderBridge?.();
|
|
121
|
-
disposeRenderBridge = void 0;
|
|
122
|
-
});
|
|
123
|
-
return registerSignal({
|
|
124
|
-
get value() {
|
|
125
|
-
if (hasActiveRenderCollector()) {
|
|
126
|
-
const nextValue = untracked(() => untrackedRender(source));
|
|
127
|
-
if (lastRenderValue !== void 0 && nextValue !== lastRenderValue) renderSubscription.bumpVersion();
|
|
128
|
-
lastRenderValue = nextValue;
|
|
129
|
-
renderSubscription.track();
|
|
130
|
-
return unbox(nextValue);
|
|
131
|
-
}
|
|
132
|
-
return unbox(source());
|
|
133
|
-
},
|
|
134
|
-
peek() {
|
|
135
|
-
return unbox(untracked(() => source()));
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Reports an error thrown by an `effect()` callback — its body or its cleanup —
|
|
141
|
-
* without letting it escape into the flush that ran it.
|
|
142
|
-
*
|
|
143
|
-
* alien-signals' `flush()` drains the rest of its effect queue in a `finally`
|
|
144
|
-
* *without running those effects*, so one throwing effect silently cancels
|
|
145
|
-
* every effect still queued behind it in that cycle — an unrelated binding, or
|
|
146
|
-
* a `useSignals()`-tracked component's commit, quietly missing an update — and
|
|
147
|
-
* then propagates out of whatever write triggered the flush (an event handler,
|
|
148
|
-
* anywhere). Catching here keeps the failure local to this one effect.
|
|
149
|
-
*
|
|
150
|
-
* The policy is *report, never re-raise*, matching the two other reporters in
|
|
151
|
-
* this codebase (`readBoundSignal` in src/runtime/jsx.ts and `notifyListener`
|
|
152
|
-
* in src/core/render-tracking.ts):
|
|
153
|
-
*
|
|
154
|
-
* 1. Always `console.error(message, { cause })` — the package-wide reporting
|
|
155
|
-
* shape; assert against `mock.calls[i][1].cause` in tests.
|
|
156
|
-
* 2. Then hand the error to `reportError()` when the host defines it, so
|
|
157
|
-
* `window.onerror` / `addEventListener("error")` / a telemetry SDK still see
|
|
158
|
-
* the failure. `reportError` *dispatches an error event* rather than
|
|
159
|
-
* throwing, so it reports exactly like an uncaught error while remaining
|
|
160
|
-
* non-fatal by construction.
|
|
161
|
-
*
|
|
162
|
-
* Both hooks are host-controlled, so **everything in this function runs inside a
|
|
163
|
-
* `try`/`catch`, property lookups included**. A host can legitimately make
|
|
164
|
-
* `console.error` throw (some React test setups install a throwing one to make
|
|
165
|
-
* warnings fatal) or expose `reportError` as a throwing getter; either would
|
|
166
|
-
* otherwise re-raise out of the reporter itself and cancel the rest of the
|
|
167
|
-
* flush — precisely the failure this function exists to prevent. The two steps
|
|
168
|
-
* get separate guards rather than one shared one, so a broken `console.error`
|
|
169
|
-
* still leaves the `reportError` channel to surface the error, and vice versa.
|
|
170
|
-
*
|
|
171
|
-
* What this deliberately no longer does is `queueMicrotask(() => { throw })`.
|
|
172
|
-
* That reads like a browser-only "let it reach `window.onerror`" trick, but a
|
|
173
|
-
* throw out of a microtask is not a recoverable event outside a browser: in
|
|
174
|
-
* Node it raises `uncaughtException`, which by default **terminates the
|
|
175
|
-
* process** (verified on Node 24.19: exit code 1). Every server-side consumer —
|
|
176
|
-
* SSR data plumbing, a Node script, a test harness — got an unrecoverable crash
|
|
177
|
-
* from an effect body that a plain `try`/`catch` would have handled, and
|
|
178
|
-
* because the throw was deferred out of the write, nothing could catch it
|
|
179
|
-
* anywhere. `reportError` is not a fallback for that on Node: it is the WHATWG
|
|
180
|
-
* reporting API, implemented by browsers, Web Workers, Deno, and Bun, but not
|
|
181
|
-
* defined by Node at any version this package supports (absent on Node 24 LTS,
|
|
182
|
-
* still not listed in the Node 26 globals docs). The feature check below is
|
|
183
|
-
* therefore the whole story — where it fails, step 1 is the report.
|
|
184
|
-
*
|
|
185
|
-
* The failure therefore never propagates to the write that triggered it, and
|
|
186
|
-
* that containment covers *synchronous* throws from the body and the cleanup.
|
|
187
|
-
* An `async` effect body that rejects is a different channel this cannot reach
|
|
188
|
-
* (an unhandled rejection, still fatal by default on Node), so `await`ed work
|
|
189
|
-
* needs its own `try`/`catch`. Either way, code that wants to handle its own
|
|
190
|
-
* failure should do so at the real failure site — inside the effect body or
|
|
191
|
-
* cleanup — which is always possible and is where the `try`/`catch` belongs.
|
|
192
|
-
*/
|
|
193
|
-
function reportEffectError(error) {
|
|
194
|
-
try {
|
|
195
|
-
console.error("react-fine-grained-signals: an effect() callback threw; the error is contained and reported here so this flush can finish.", { cause: error });
|
|
196
|
-
} catch {}
|
|
197
|
-
try {
|
|
198
|
-
const report = globalThis.reportError;
|
|
199
|
-
if (typeof report === "function") report.call(globalThis, error);
|
|
200
|
-
} catch {}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Wraps the cleanup an effect body returned so a throw out of it is contained
|
|
204
|
-
* exactly like a throw out of the body.
|
|
205
|
-
*
|
|
206
|
-
* alien-signals stores the returned cleanup on the effect node and calls it
|
|
207
|
-
* through its own private `runCleanup`, from two places this package cannot
|
|
208
|
-
* intercept (verified against alien-signals@3.2.1):
|
|
209
|
-
*
|
|
210
|
-
* - `run()` calls it *before* re-running the body, i.e. inside `flush()`'s
|
|
211
|
-
* `try`, so a throw there hits the very same queue-cancelling failure mode
|
|
212
|
-
* `reportEffectError` exists for — worse, in fact, because it aborts the
|
|
213
|
-
* flush before this effect's own body has even re-run.
|
|
214
|
-
* - `effectOper()` calls it on disposal — both the disposer returned below and
|
|
215
|
-
* the `unwatched` hook alien-signals fires when a nested effect loses its
|
|
216
|
-
* last subscriber, which happens during `unlink`/`purgeDeps` and so can also
|
|
217
|
-
* land in the middle of a flush.
|
|
218
|
-
*
|
|
219
|
-
* Neither call site is reachable from outside the module, so the guard goes on
|
|
220
|
-
* the cleanup itself, on its way back to alien-signals: one wrapper covers
|
|
221
|
-
* every path that can invoke it, now and later. The wrapper never rethrows, so
|
|
222
|
-
* alien-signals always sees the cleanup return normally and its own
|
|
223
|
-
* bookkeeping (`e.cleanup = undefined`, `activeSub` restore, the flags check
|
|
224
|
-
* after `runCleanup`) stays on the success path.
|
|
225
|
-
*/
|
|
226
|
-
function guardCleanup(cleanup) {
|
|
227
|
-
return () => {
|
|
228
|
-
try {
|
|
229
|
-
cleanup();
|
|
230
|
-
} catch (error) {
|
|
231
|
-
reportEffectError(error);
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
/** Runs a reactive side effect and returns a disposer. */
|
|
236
|
-
function effect$1(fn) {
|
|
237
|
-
let disposed = false;
|
|
238
|
-
let disposeEffect;
|
|
239
|
-
disposeEffect = effect(() => {
|
|
240
|
-
if (disposed) return;
|
|
241
|
-
let cleanup;
|
|
242
|
-
try {
|
|
243
|
-
cleanup = untrackedRender(fn);
|
|
244
|
-
} catch (error) {
|
|
245
|
-
reportEffectError(error);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (cleanup === void 0) return;
|
|
249
|
-
const guardedCleanup = guardCleanup(cleanup);
|
|
250
|
-
if (disposed) {
|
|
251
|
-
untracked(guardedCleanup);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
return guardedCleanup;
|
|
255
|
-
});
|
|
256
|
-
return () => {
|
|
257
|
-
if (disposed) return;
|
|
258
|
-
disposed = true;
|
|
259
|
-
disposeEffect?.();
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
/** Groups writes, deferring effect notifications until the callback completes. */
|
|
263
|
-
function batch(fn) {
|
|
264
|
-
startBatch();
|
|
265
|
-
try {
|
|
266
|
-
return fn();
|
|
267
|
-
} finally {
|
|
268
|
-
endBatch();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/** Runs a callback without collecting reactive dependencies. */
|
|
272
|
-
function untracked(fn) {
|
|
273
|
-
return untrackedRender(() => {
|
|
274
|
-
const activeSub = getActiveSub();
|
|
275
|
-
setActiveSub();
|
|
276
|
-
try {
|
|
277
|
-
return fn();
|
|
278
|
-
} finally {
|
|
279
|
-
setActiveSub(activeSub);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
//#endregion
|
|
284
|
-
export { effect$1 as a, signal$1 as c, computed$1 as i, untracked as l, SignalImpl as n, isSignal as o, batch as r, registerSignal as s, SIGNAL_BRAND as t };
|
|
285
|
-
|
|
286
|
-
//# sourceMappingURL=base-BZTnpU8U.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"base-BZTnpU8U.js","names":["createSignal","signal","computed","createComputed","createEffect","effect"],"sources":["../src/core/base.ts"],"sourcesContent":["import {\n computed as createComputed,\n effect as createEffect,\n endBatch,\n getActiveSub,\n setActiveSub,\n signal as createSignal,\n startBatch,\n trigger,\n} from \"alien-signals\";\nimport {\n hasActiveRenderCollector,\n RenderSubscription,\n untrackedRender,\n} from \"./render-tracking.js\";\n\n/** A readable reactive value. */\nexport interface ReadonlySignal<T> {\n readonly value: T;\n peek(): T;\n}\n\n/** A readable and writable reactive value. */\nexport interface Signal<T> extends ReadonlySignal<T> {\n value: T;\n}\n\nconst signalInstances = new WeakSet<object>();\n\n/**\n * Cross-instance signal brand. `Symbol.for` resolves through the registry that\n * is shared by every realm, so a signal made by a duplicate copy of this\n * package (pnpm hoisting, a monorepo consumer, an ESM/CJS split) or in another\n * realm (iframe, worker) still answers `isSignal`. Shared with the deep-signal\n * proxy only; the package's public API stays `isSignal`.\n */\nexport const SIGNAL_BRAND: unique symbol = Symbol.for(\"react-fine-grained-signals.signal\");\n// The brand carries a protocol version instead of `true` so a future instance\n// can tell which contract a foreign signal claims. A version only ever widens\n// the `{ value, peek() }` contract; a breaking change must take a new symbol\n// key, which is why anything from the minimum upwards is trusted here.\nconst SIGNAL_BRAND_VERSION = 1;\nconst SIGNAL_BRAND_MIN_VERSION = 1;\n\n/** Marks an internal signal implementation for the public identity guard. */\nexport function registerSignal<T extends object>(value: T): T {\n signalInstances.add(value);\n // Non-enumerable keeps the brand out of JSON, `Object.keys`, spread, and\n // React's prop diffing; non-writable and non-configurable keep it from being\n // retargeted or stripped once the value is public. The hardening is free:\n // measured against a writable/configurable descriptor the cost is identical,\n // because it is the `defineProperty` call itself, and V8 keeps the instance\n // in fast properties either way (checked with `%HasFastProperties`). Stamping\n // the prototype instead would be worse — that pushes the prototype into\n // dictionary mode, which every lookup through it then pays for.\n Object.defineProperty(value, SIGNAL_BRAND, {\n value: SIGNAL_BRAND_VERSION,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n return value;\n}\n\n/** Returns whether a value came from this package's signal APIs, any copy. */\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n // The WeakSet is both the cheaper lookup and the authority for values created\n // by this instance; the brand is the fallback for every other instance.\n if (signalInstances.has(value)) return true;\n const brand = (value as { [SIGNAL_BRAND]?: unknown })[SIGNAL_BRAND];\n if (typeof brand !== \"number\" || brand < SIGNAL_BRAND_MIN_VERSION) return false;\n // A foreign brand is a claim, not a proof. Checking the cheap half of the\n // contract turns a malformed brand into `false` instead of a crash inside a\n // render, and costs nothing on the local-instance path above.\n return typeof (value as ReadonlySignal<unknown>).peek === \"function\";\n}\n\n/** Internal writable signal implementation shared with deep signals. */\nexport class SignalImpl<T> implements Signal<T> {\n readonly #source: ReturnType<typeof createSignal<T>>;\n readonly #renderSubscription = new RenderSubscription();\n #currentValue: T;\n // Liveness bookkeeping for `deepSignal`'s per-key metadata pruning; see\n // `markWatched`/`hasSubscribers` below. Not maintained on the read path\n // itself — `deepSignal`'s `track()` already knows whether a subscriber is\n // active and calls `markWatched()` there, so ordinary reads pay nothing.\n #watchedSinceWrite = false;\n\n constructor(initialValue: T) {\n this.#source = createSignal(initialValue);\n this.#currentValue = initialValue;\n }\n\n /**\n * Records that this signal was read while some subscriber (an alien-signals\n * effect/computed, or a React render collector) was active. Cleared by the\n * next write, so after that write's flush has drained, a still-false flag\n * means every reactive subscriber has re-run without re-reading this signal\n * and has therefore been unlinked from it.\n */\n markWatched(): void {\n this.#watchedSinceWrite = true;\n }\n\n /**\n * Conservative \"somebody still depends on me\" test used to decide whether a\n * `deepSignal` per-key version signal is safe to drop. Never reports `false`\n * for a signal that still has a live dependent: the React side is exact\n * (`hasListeners`), and the alien-signals side is covered by `markWatched`,\n * whose flag can only be `false` once a write has notified every subscriber\n * and none of them read this signal again.\n */\n hasSubscribers(): boolean {\n return this.#watchedSinceWrite || this.#renderSubscription.hasListeners();\n }\n\n get value(): T {\n if (this.#renderSubscription.track()) return this.#currentValue;\n // The call records the dependency; `#currentValue` preserves Object.is\n // semantics where the underlying callable API treats 0 and -0 alike.\n this.#source();\n return this.#currentValue;\n }\n\n set value(nextValue: T) {\n if (Object.is(this.#currentValue, nextValue)) return;\n const requiresManualTrigger = this.#currentValue === nextValue;\n this.#currentValue = nextValue;\n // Every subscriber is about to be notified, so each one either re-reads\n // this signal (setting the flag again through `markWatched`) or drops its\n // link to it. See `hasSubscribers`.\n this.#watchedSinceWrite = false;\n // The alien-signals write below flushes effects synchronously, so it can\n // still throw even though `effect()` now contains both its body and its\n // cleanup: anything else sharing this graph (an alien-signals effect\n // created directly, a computed's render bridge) is outside that guard.\n // `#currentValue` is already committed at that point, so skipping the\n // React-side notification would\n // leave every subscribed `RenderStore` believing the old value is still\n // current — a permanently stale UI that no later write can repair,\n // because a subsequent write compares against the already-updated\n // `#currentValue`. `finally` keeps React in sync no matter how the flush\n // exits, and still lets the original error propagate to the writer.\n try {\n if (requiresManualTrigger) {\n trigger(this.#source);\n } else {\n this.#source(nextValue);\n }\n } finally {\n this.#renderSubscription.notify();\n }\n }\n\n peek(): T {\n return this.#currentValue;\n }\n}\n\n/** Creates a writable reactive value. */\nexport function signal<T>(initialValue: T): Signal<T> {\n return registerSignal(new SignalImpl(initialValue));\n}\n\n/** A computed's cached result, boxed so a thrown error can be cached too. */\ntype ComputedBox<T> = { value: T } | { error: unknown };\n\n/** Unwraps a computed's box, rethrowing a cached getter error on read. */\nfunction unbox<T>(box: ComputedBox<T>): T {\n if (\"error\" in box) throw box.error;\n return box.value;\n}\n\n/** Creates a lazily evaluated reactive value. */\nexport function computed<T>(getter: () => T): ReadonlySignal<T> {\n // alien-signals' `updateComputed` resets its dirty/pending flags in a\n // `finally` no matter how the callback exits, so a thrown getter leaves the\n // node looking clean with its stale cached value, and can permanently\n // unwatch an effect whose run was interrupted mid-flush (verified against\n // alien-signals@3.2.1: neither the computed nor the effect ever recovers on\n // a later write). Catching here keeps the callback always returning\n // normally, so alien-signals' own bookkeeping stays on the success path;\n // the error is boxed and only rethrown when something reads `.value` or\n // `.peek()`, which for React happens during render where an Error Boundary\n // can catch it (mirrors `createDeepSelectorStore` in `src/react/hooks.ts`).\n const source = createComputed<ComputedBox<T>>((previous) => {\n try {\n const value = getter();\n // Guard `\"value\" in previous`: if `previous` is an error box, its\n // `.value` is `undefined`, so without this a legitimate recovery to\n // `value === undefined` would `Object.is` that against `undefined` and\n // wrongly keep returning the stale error box.\n return previous !== undefined && \"value\" in previous && Object.is(previous.value, value)\n ? previous\n : { value };\n } catch (error) {\n return { error };\n }\n });\n let disposeRenderBridge: (() => void) | undefined;\n let isInitialBridgeRun = true;\n let lastRenderValue: ComputedBox<T> | undefined;\n const renderSubscription = new RenderSubscription(\n () => {\n isInitialBridgeRun = true;\n disposeRenderBridge = createEffect(() => {\n const nextValue = untrackedRender(source);\n const changedSinceRender =\n lastRenderValue !== undefined && nextValue !== lastRenderValue;\n lastRenderValue = nextValue;\n if (isInitialBridgeRun) {\n isInitialBridgeRun = false;\n if (changedSinceRender) renderSubscription.bumpVersion();\n } else if (changedSinceRender) {\n renderSubscription.notify();\n }\n });\n },\n () => {\n disposeRenderBridge?.();\n disposeRenderBridge = undefined;\n },\n );\n\n const result: ReadonlySignal<T> = {\n get value(): T {\n if (hasActiveRenderCollector()) {\n const nextValue = untracked(() => untrackedRender(source));\n if (lastRenderValue !== undefined && nextValue !== lastRenderValue) {\n renderSubscription.bumpVersion();\n }\n lastRenderValue = nextValue;\n // Track after evaluating so this render records the post-evaluation\n // version without masking an older concurrent render.\n renderSubscription.track();\n return unbox(nextValue);\n }\n return unbox(source());\n },\n // `peek()` is documented as an untracked read, not an error-suppressing\n // one, so it rethrows the same as `.value` — consistent with\n // `useDeepSignalValue`, whose selector errors always surface on read.\n peek(): T {\n return unbox(untracked(() => source()));\n },\n };\n return registerSignal(result);\n}\n\n/**\n * Reports an error thrown by an `effect()` callback — its body or its cleanup —\n * without letting it escape into the flush that ran it.\n *\n * alien-signals' `flush()` drains the rest of its effect queue in a `finally`\n * *without running those effects*, so one throwing effect silently cancels\n * every effect still queued behind it in that cycle — an unrelated binding, or\n * a `useSignals()`-tracked component's commit, quietly missing an update — and\n * then propagates out of whatever write triggered the flush (an event handler,\n * anywhere). Catching here keeps the failure local to this one effect.\n *\n * The policy is *report, never re-raise*, matching the two other reporters in\n * this codebase (`readBoundSignal` in src/runtime/jsx.ts and `notifyListener`\n * in src/core/render-tracking.ts):\n *\n * 1. Always `console.error(message, { cause })` — the package-wide reporting\n * shape; assert against `mock.calls[i][1].cause` in tests.\n * 2. Then hand the error to `reportError()` when the host defines it, so\n * `window.onerror` / `addEventListener(\"error\")` / a telemetry SDK still see\n * the failure. `reportError` *dispatches an error event* rather than\n * throwing, so it reports exactly like an uncaught error while remaining\n * non-fatal by construction.\n *\n * Both hooks are host-controlled, so **everything in this function runs inside a\n * `try`/`catch`, property lookups included**. A host can legitimately make\n * `console.error` throw (some React test setups install a throwing one to make\n * warnings fatal) or expose `reportError` as a throwing getter; either would\n * otherwise re-raise out of the reporter itself and cancel the rest of the\n * flush — precisely the failure this function exists to prevent. The two steps\n * get separate guards rather than one shared one, so a broken `console.error`\n * still leaves the `reportError` channel to surface the error, and vice versa.\n *\n * What this deliberately no longer does is `queueMicrotask(() => { throw })`.\n * That reads like a browser-only \"let it reach `window.onerror`\" trick, but a\n * throw out of a microtask is not a recoverable event outside a browser: in\n * Node it raises `uncaughtException`, which by default **terminates the\n * process** (verified on Node 24.19: exit code 1). Every server-side consumer —\n * SSR data plumbing, a Node script, a test harness — got an unrecoverable crash\n * from an effect body that a plain `try`/`catch` would have handled, and\n * because the throw was deferred out of the write, nothing could catch it\n * anywhere. `reportError` is not a fallback for that on Node: it is the WHATWG\n * reporting API, implemented by browsers, Web Workers, Deno, and Bun, but not\n * defined by Node at any version this package supports (absent on Node 24 LTS,\n * still not listed in the Node 26 globals docs). The feature check below is\n * therefore the whole story — where it fails, step 1 is the report.\n *\n * The failure therefore never propagates to the write that triggered it, and\n * that containment covers *synchronous* throws from the body and the cleanup.\n * An `async` effect body that rejects is a different channel this cannot reach\n * (an unhandled rejection, still fatal by default on Node), so `await`ed work\n * needs its own `try`/`catch`. Either way, code that wants to handle its own\n * failure should do so at the real failure site — inside the effect body or\n * cleanup — which is always possible and is where the `try`/`catch` belongs.\n */\nfunction reportEffectError(error: unknown): void {\n try {\n console.error(\n \"react-fine-grained-signals: an effect() callback threw; the error is contained and reported here so this flush can finish.\",\n { cause: error },\n );\n } catch {\n // A host that made `console.error` itself throw does not get to turn a\n // contained effect failure back into one that escapes and kills the flush.\n }\n try {\n // Looked up per call, not captured at module load, so a host (or a test)\n // that installs its own `reportError` later is still honored — and read\n // inside the guard, because the property itself can be a throwing getter.\n const report = (globalThis as { reportError?: (error: unknown) => void }).reportError;\n if (typeof report === \"function\") report.call(globalThis, error);\n } catch {\n // Same rule for the second hook: a host whose `reportError` throws must not\n // break containment either. The error already went to the console above.\n }\n}\n\n/**\n * Wraps the cleanup an effect body returned so a throw out of it is contained\n * exactly like a throw out of the body.\n *\n * alien-signals stores the returned cleanup on the effect node and calls it\n * through its own private `runCleanup`, from two places this package cannot\n * intercept (verified against alien-signals@3.2.1):\n *\n * - `run()` calls it *before* re-running the body, i.e. inside `flush()`'s\n * `try`, so a throw there hits the very same queue-cancelling failure mode\n * `reportEffectError` exists for — worse, in fact, because it aborts the\n * flush before this effect's own body has even re-run.\n * - `effectOper()` calls it on disposal — both the disposer returned below and\n * the `unwatched` hook alien-signals fires when a nested effect loses its\n * last subscriber, which happens during `unlink`/`purgeDeps` and so can also\n * land in the middle of a flush.\n *\n * Neither call site is reachable from outside the module, so the guard goes on\n * the cleanup itself, on its way back to alien-signals: one wrapper covers\n * every path that can invoke it, now and later. The wrapper never rethrows, so\n * alien-signals always sees the cleanup return normally and its own\n * bookkeeping (`e.cleanup = undefined`, `activeSub` restore, the flags check\n * after `runCleanup`) stays on the success path.\n */\nfunction guardCleanup(cleanup: () => void): () => void {\n return () => {\n try {\n cleanup();\n } catch (error) {\n reportEffectError(error);\n }\n };\n}\n\n/** Runs a reactive side effect and returns a disposer. */\nexport function effect(fn: () => void | (() => void)): () => void {\n let disposed = false;\n let disposeEffect: (() => void) | undefined;\n disposeEffect = createEffect(() => {\n if (disposed) return;\n let cleanup: void | (() => void);\n try {\n cleanup = untrackedRender(fn);\n } catch (error) {\n reportEffectError(error);\n return;\n }\n if (cleanup === undefined) return;\n const guardedCleanup = guardCleanup(cleanup);\n if (disposed) {\n untracked(guardedCleanup);\n return;\n }\n return guardedCleanup;\n });\n return () => {\n if (disposed) return;\n disposed = true;\n disposeEffect?.();\n };\n}\n\n/** Groups writes, deferring effect notifications until the callback completes. */\nexport function batch<T>(fn: () => T): T {\n startBatch();\n try {\n return fn();\n } finally {\n endBatch();\n }\n}\n\n/** Runs a callback without collecting reactive dependencies. */\nexport function untracked<T>(fn: () => T): T {\n return untrackedRender(() => {\n const activeSub = getActiveSub();\n setActiveSub();\n try {\n return fn();\n } finally {\n setActiveSub(activeSub);\n }\n });\n}\n"],"mappings":"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"}
|