@epoch32/visage 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/src/api/index.js +4002 -0
  4. package/dist/src/charts/index.js +2717 -0
  5. package/dist/src/devtools/index.js +4677 -0
  6. package/dist/src/dom/index.js +1550 -0
  7. package/dist/src/form/index.js +1415 -0
  8. package/dist/src/index.js +11826 -0
  9. package/dist/src/jsx-runtime/index.js +1630 -0
  10. package/dist/src/query/index.js +2422 -0
  11. package/dist/src/router/index.js +1856 -0
  12. package/dist/src/rpc/index.js +3484 -0
  13. package/dist/src/server/index.js +1173 -0
  14. package/dist/src/signals/index.js +1077 -0
  15. package/dist/src/style/index.js +2340 -0
  16. package/dist/src/template/index.js +1149 -0
  17. package/dist/src/ui/index.js +4155 -0
  18. package/dist/src/virtual/index.js +1955 -0
  19. package/dist/src/vite/index.js +984 -0
  20. package/package.json +140 -4
  21. package/src/api/client.ts +371 -0
  22. package/src/api/file-router.ts +183 -0
  23. package/src/api/handler.ts +326 -0
  24. package/src/api/index.ts +71 -0
  25. package/src/api/middleware.ts +54 -0
  26. package/src/api/pragma.ts +155 -0
  27. package/src/api/procedure.ts +172 -0
  28. package/src/api/types.ts +249 -0
  29. package/src/charts/Area.tsx +111 -0
  30. package/src/charts/Axis.tsx +126 -0
  31. package/src/charts/Bar.tsx +86 -0
  32. package/src/charts/CartesianGrid.tsx +72 -0
  33. package/src/charts/Chart.tsx +269 -0
  34. package/src/charts/Cursor.tsx +42 -0
  35. package/src/charts/Legend.tsx +56 -0
  36. package/src/charts/Line.tsx +106 -0
  37. package/src/charts/Pie.tsx +88 -0
  38. package/src/charts/Tooltip.tsx +127 -0
  39. package/src/charts/context.ts +30 -0
  40. package/src/charts/index.ts +14 -0
  41. package/src/charts/path.ts +155 -0
  42. package/src/charts/scale.ts +133 -0
  43. package/src/charts/types.ts +50 -0
  44. package/src/devtools/DomTab.tsx +134 -0
  45. package/src/devtools/GraphInspector.tsx +196 -0
  46. package/src/devtools/GraphTab.tsx +315 -0
  47. package/src/devtools/QueryTab.tsx +200 -0
  48. package/src/devtools/RpcTab.tsx +190 -0
  49. package/src/devtools/SignalsTab.tsx +129 -0
  50. package/src/devtools/component.tsx +243 -0
  51. package/src/devtools/highlighter.ts +134 -0
  52. package/src/devtools/hub.ts +175 -0
  53. package/src/devtools/index.ts +20 -0
  54. package/src/devtools/styles.ts +313 -0
  55. package/src/devtools/types.ts +45 -0
  56. package/src/dom/async.ts +232 -0
  57. package/src/dom/control-flow.ts +353 -0
  58. package/src/dom/index.ts +32 -0
  59. package/src/dom/lazy.ts +82 -0
  60. package/src/dom/render.ts +286 -0
  61. package/src/dom/resource.ts +120 -0
  62. package/src/form/form.ts +218 -0
  63. package/src/form/index.ts +17 -0
  64. package/src/form/schema.ts +299 -0
  65. package/src/index.ts +413 -0
  66. package/src/jsx-dev-runtime/index.ts +1 -0
  67. package/src/jsx-runtime/index.ts +112 -0
  68. package/src/jsx-runtime/jsx.d.ts +278 -0
  69. package/src/query/create-mutation.ts +64 -0
  70. package/src/query/create-query.ts +426 -0
  71. package/src/query/focus-manager.ts +68 -0
  72. package/src/query/index.ts +63 -0
  73. package/src/query/mutation-cache.ts +138 -0
  74. package/src/query/online-manager.ts +67 -0
  75. package/src/query/query-cache.ts +369 -0
  76. package/src/query/query-client.ts +248 -0
  77. package/src/query/types.ts +221 -0
  78. package/src/query/utils.ts +132 -0
  79. package/src/router/index.ts +19 -0
  80. package/src/router/router.tsx +352 -0
  81. package/src/rpc/client.ts +317 -0
  82. package/src/rpc/error.ts +70 -0
  83. package/src/rpc/index.ts +52 -0
  84. package/src/rpc/procedure.ts +88 -0
  85. package/src/rpc/router.ts +105 -0
  86. package/src/rpc/server.ts +511 -0
  87. package/src/rpc/types.ts +328 -0
  88. package/src/server/hydrate.ts +21 -0
  89. package/src/server/index.ts +10 -0
  90. package/src/server/render-to-stream.ts +31 -0
  91. package/src/server/render-to-string.ts +125 -0
  92. package/src/signals/computed.ts +217 -0
  93. package/src/signals/context.ts +89 -0
  94. package/src/signals/effect.ts +161 -0
  95. package/src/signals/index.ts +57 -0
  96. package/src/signals/scope.ts +132 -0
  97. package/src/signals/signal.ts +203 -0
  98. package/src/signals/store.ts +163 -0
  99. package/src/signals/system.ts +124 -0
  100. package/src/signals/types.ts +64 -0
  101. package/src/style/component.tsx +122 -0
  102. package/src/style/create-style.ts +229 -0
  103. package/src/style/cx.ts +75 -0
  104. package/src/style/hash.ts +19 -0
  105. package/src/style/index.ts +8 -0
  106. package/src/style/layout.tsx +406 -0
  107. package/src/style/manager.ts +87 -0
  108. package/src/style/parser.ts +257 -0
  109. package/src/style/scale.ts +237 -0
  110. package/src/template/index.ts +4 -0
  111. package/src/template/template.ts +126 -0
  112. package/src/ui/accordion.tsx +286 -0
  113. package/src/ui/command-dialog.tsx +49 -0
  114. package/src/ui/command.tsx +400 -0
  115. package/src/ui/data-table.tsx +322 -0
  116. package/src/ui/dialog.tsx +362 -0
  117. package/src/ui/dropdown-menu.tsx +381 -0
  118. package/src/ui/icon.tsx +69 -0
  119. package/src/ui/index.ts +138 -0
  120. package/src/ui/layout.tsx +13 -0
  121. package/src/ui/popover.tsx +249 -0
  122. package/src/ui/switch.tsx +114 -0
  123. package/src/ui/tabs.tsx +297 -0
  124. package/src/ui/toast.tsx +280 -0
  125. package/src/ui/tooltip.tsx +262 -0
  126. package/src/ui/utils.ts +110 -0
  127. package/src/virtual/index.ts +12 -0
  128. package/src/virtual/virtual-list.tsx +185 -0
  129. package/src/virtual/virtualizer.ts +274 -0
  130. package/src/vite/hmr.ts +38 -0
  131. package/src/vite/index.ts +3 -0
  132. package/src/vite/lucide.ts +124 -0
  133. package/src/vite/plugin.ts +102 -0
  134. package/src/vite/rpc-middleware.ts +82 -0
  135. package/src/vite/types.ts +65 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Visage Lazy, Glitch-Free Computed State Engine
3
+ */
4
+ import type { Scope } from "./scope";
5
+ import { currentScope, currentSubscriber, setCurrentScope, setCurrentSubscriber } from "./system";
6
+ import {
7
+ COMPUTED_BRAND,
8
+ type ComputedOptions,
9
+ type IDependency,
10
+ type ISubscriber,
11
+ type ReadonlySignal,
12
+ SubscriberState
13
+ } from "./types";
14
+
15
+ export type { ComputedOptions };
16
+
17
+ /**
18
+ * Computed / Derived reactive state (Lazy, Glitch-Free, Memoized)
19
+ */
20
+ export class Computed<T> implements ISubscriber, IDependency {
21
+ [COMPUTED_BRAND] = true as const;
22
+ private _fn: () => T;
23
+ private _value!: T;
24
+ private _subscribers: Set<ISubscriber> = new Set();
25
+ sources: Set<IDependency> = new Set();
26
+ sourceVersions: Map<IDependency, number> = new Map();
27
+ state: SubscriberState = SubscriberState.DIRTY;
28
+ scope: Scope | null;
29
+ version = 0;
30
+
31
+ constructor(fn: () => T) {
32
+ this._fn = fn;
33
+ this.scope = currentScope;
34
+ if (currentScope) {
35
+ currentScope.addSubscriber(this);
36
+ }
37
+ }
38
+
39
+ get(): T {
40
+ if (this.state === SubscriberState.DISPOSED) {
41
+ return this._fn();
42
+ }
43
+
44
+ if (currentSubscriber !== null && currentSubscriber.state !== SubscriberState.DISPOSED) {
45
+ this._subscribers.add(currentSubscriber);
46
+ currentSubscriber.sources.add(this);
47
+ currentSubscriber.sourceVersions.set(this, this.version);
48
+ }
49
+
50
+ if (this.state === SubscriberState.DIRTY || this.state === SubscriberState.CHECK_DIRTY) {
51
+ this.updateIfNecessary();
52
+ }
53
+
54
+ return this._value;
55
+ }
56
+
57
+ get value(): T {
58
+ return this.get();
59
+ }
60
+
61
+ removeSubscriber(sub: ISubscriber): void {
62
+ this._subscribers.delete(sub);
63
+ }
64
+
65
+ markDirty(targetState: SubscriberState = SubscriberState.CHECK_DIRTY): void {
66
+ if (this.state >= targetState || this.state === SubscriberState.DISPOSED) {
67
+ return;
68
+ }
69
+ this.state = targetState;
70
+
71
+ for (const sub of this._subscribers) {
72
+ sub.markDirty(SubscriberState.CHECK_DIRTY);
73
+ }
74
+ }
75
+
76
+ updateIfNecessary(): boolean {
77
+ if (this.state === SubscriberState.DIRTY) {
78
+ const prev = this._value;
79
+ this.recompute();
80
+ return !Object.is(prev, this._value);
81
+ }
82
+
83
+ if (this.state === SubscriberState.CHECK_DIRTY) {
84
+ let anyChanged = false;
85
+ for (const source of this.sources) {
86
+ if (source instanceof Computed) {
87
+ source.updateIfNecessary();
88
+ }
89
+ const lastVersion = this.sourceVersions.get(source);
90
+ if (lastVersion !== undefined && lastVersion !== source.version) {
91
+ anyChanged = true;
92
+ }
93
+ }
94
+
95
+ if (anyChanged) {
96
+ const prev = this._value;
97
+ this.recompute();
98
+ return !Object.is(prev, this._value);
99
+ }
100
+
101
+ this.state = SubscriberState.CLEAN;
102
+ return false;
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ private recompute(): void {
109
+ this.cleanupSources();
110
+
111
+ const prevSubscriber = currentSubscriber;
112
+ setCurrentSubscriber(this);
113
+ const prevScope = currentScope;
114
+ setCurrentScope(this.scope);
115
+
116
+ try {
117
+ const next = this._fn();
118
+ if (!Object.is(this._value, next)) {
119
+ this._value = next;
120
+ this.version++;
121
+ }
122
+ this.state = SubscriberState.CLEAN;
123
+ } finally {
124
+ setCurrentSubscriber(prevSubscriber);
125
+ setCurrentScope(prevScope);
126
+ }
127
+ }
128
+
129
+ private cleanupSources(): void {
130
+ for (const source of this.sources) {
131
+ source.removeSubscriber(this);
132
+ }
133
+ this.sources.clear();
134
+ this.sourceVersions.clear();
135
+ }
136
+
137
+ dispose(): void {
138
+ if (this.state === SubscriberState.DISPOSED) return;
139
+ this.state = SubscriberState.DISPOSED;
140
+ this.cleanupSources();
141
+ this._subscribers.clear();
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Factory for creating lazy, memoized, glitch-free computed signals.
147
+ *
148
+ * Computed values are only recalculated when one of their recorded reactive
149
+ * dependencies changes and the value is actually accessed.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * const count = signal(2);
154
+ * const double = computed(() => count() * 2);
155
+ *
156
+ * console.log(double()); // 4
157
+ *
158
+ * // In JSX, passes directly or as a function
159
+ * <div>Doubled value: {double}</div>
160
+ * ```
161
+ *
162
+ * @param fn - The pure reactive computation function
163
+ * @param options - Optional debug metadata and internal flags
164
+ * @returns A readonly reactive signal function accessor
165
+ */
166
+ export function computed<T>(fn: () => T, options?: ComputedOptions): ReadonlySignal<T> {
167
+ const isInternal = options?.internal ?? false;
168
+ const customName = options?.name;
169
+
170
+ const c = new Computed(fn);
171
+
172
+ const accessor = ((): T => c.get()) as ReadonlySignal<T>;
173
+
174
+ Object.defineProperties(accessor, {
175
+ [COMPUTED_BRAND]: { value: true },
176
+ get: { value: () => c.get() },
177
+ value: { get: () => c.get() }
178
+ });
179
+
180
+ if (!isInternal && typeof window !== "undefined" && (window as any).__VISAGE_DEVTOOLS__) {
181
+ const devtools = (window as any).__VISAGE_DEVTOOLS__;
182
+ const sigId = devtools.registerSignal({
183
+ name: customName,
184
+ type: "computed",
185
+ getValue: () => c.get(),
186
+ version: () => c.version,
187
+ subscribersCount: () => (c as any)._subscribers?.size ?? 0,
188
+ dependencies: () =>
189
+ Array.from(c.sources ?? [])
190
+ .map((src: any) => (src as any)._devId)
191
+ .filter(Boolean),
192
+ dependents: () =>
193
+ Array.from((c as any)._subscribers ?? [])
194
+ .map((sub: any) => sub._devId)
195
+ .filter(Boolean)
196
+ });
197
+ (c as any)._devId = sigId;
198
+ if (currentScope) {
199
+ currentScope.addCleanup(() => {
200
+ devtools.unregisterSignal(sigId);
201
+ });
202
+ }
203
+ }
204
+
205
+ return accessor;
206
+ }
207
+
208
+ /**
209
+ * Solid-compatible alias for `computed()`.
210
+ *
211
+ * @example
212
+ * ```tsx
213
+ * const count = signal(5);
214
+ * const squared = createMemo(() => count() ** 2);
215
+ * ```
216
+ */
217
+ export const createMemo = computed;
@@ -0,0 +1,89 @@
1
+ import { type Child, insertChild } from "../dom/render";
2
+ import { getActiveScope, runInScope, Scope } from "./scope";
3
+ import { untrack } from "./system";
4
+
5
+ export interface Context<T> {
6
+ id: symbol;
7
+ defaultValue: T;
8
+ Provider: (props: { value: T; children: Child }) => any;
9
+ }
10
+
11
+ /**
12
+ * Create a reactive Context for dependency injection across the component tree.
13
+ *
14
+ * Context allows passing state and utilities down the component tree without
15
+ * manually drilling props through every intermediate component.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const ThemeContext = createContext<"light" | "dark">("light");
20
+ *
21
+ * // Provide value in parent
22
+ * function App() {
23
+ * return (
24
+ * <ThemeContext.Provider value="dark">
25
+ * <Header />
26
+ * </ThemeContext.Provider>
27
+ * );
28
+ * }
29
+ *
30
+ * // Consume value in descendant
31
+ * function Header() {
32
+ * const theme = useContext(ThemeContext);
33
+ * return <div>Current theme: {theme}</div>;
34
+ * }
35
+ * ```
36
+ */
37
+ export function createContext<T>(defaultValue: T): Context<T>;
38
+ export function createContext<T>(): Context<T | undefined>;
39
+ export function createContext<T>(defaultValue?: T): Context<T | undefined> {
40
+ const id = Symbol("visage.context");
41
+
42
+ const context: Context<T | undefined> = {
43
+ id,
44
+ defaultValue,
45
+ Provider: (props: { value: T | undefined; children: Child }): any => {
46
+ const parentScope = getActiveScope();
47
+ const scope = new Scope(parentScope);
48
+
49
+ if (!scope.context) {
50
+ scope.context = new Map(parentScope?.context);
51
+ }
52
+ scope.context.set(id, props.value);
53
+
54
+ return runInScope(scope, () => {
55
+ const content =
56
+ typeof props.children === "function"
57
+ ? untrack(() => (props.children as (...args: any[]) => any)())
58
+ : props.children;
59
+ if (typeof document !== "undefined") {
60
+ const frag = document.createDocumentFragment();
61
+ insertChild(frag, content);
62
+ return frag;
63
+ }
64
+ return content;
65
+ });
66
+ }
67
+ };
68
+
69
+ return context;
70
+ }
71
+
72
+ /**
73
+ * Consume a Context value from the nearest ancestor Provider, falling back to defaultValue.
74
+ *
75
+ * @param context - The Context object returned from `createContext`
76
+ * @returns The current context value
77
+ */
78
+ export function useContext<T>(context: Context<T>): T {
79
+ let scope = getActiveScope();
80
+
81
+ while (scope) {
82
+ if (scope.context?.has(context.id)) {
83
+ return scope.context.get(context.id);
84
+ }
85
+ scope = scope.parent;
86
+ }
87
+
88
+ return context.defaultValue;
89
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Visage Reactive Side-Effect Runner
3
+ */
4
+ import { Computed } from "./computed";
5
+ import type { Scope } from "./scope";
6
+ import {
7
+ addPendingEffect,
8
+ batchDepth,
9
+ currentScope,
10
+ currentSubscriber,
11
+ flushEffects,
12
+ removePendingEffect,
13
+ setCurrentScope,
14
+ setCurrentSubscriber
15
+ } from "./system";
16
+ import { type CleanupFn, type IDependency, type ISubscriber, SubscriberState } from "./types";
17
+
18
+ /**
19
+ * Side-effect runner with automatic dependency tracking and disposal
20
+ */
21
+ export class Effect implements ISubscriber {
22
+ private _fn: () => void | CleanupFn;
23
+ sources: Set<IDependency> = new Set();
24
+ sourceVersions: Map<IDependency, number> = new Map();
25
+ cleanups: CleanupFn[] = [];
26
+ state: SubscriberState = SubscriberState.DIRTY;
27
+ scope: Scope | null;
28
+
29
+ constructor(fn: () => void | CleanupFn) {
30
+ this._fn = fn;
31
+ this.scope = currentScope;
32
+ if (currentScope) {
33
+ currentScope.addSubscriber(this);
34
+ }
35
+ this.run();
36
+ }
37
+
38
+ addCleanup(fn: CleanupFn): void {
39
+ this.cleanups.push(fn);
40
+ }
41
+
42
+ private runCleanups(): void {
43
+ for (let i = this.cleanups.length - 1; i >= 0; i--) {
44
+ try {
45
+ this.cleanups[i]!();
46
+ } catch (e) {
47
+ console.error("Error in effect cleanup:", e);
48
+ }
49
+ }
50
+ this.cleanups = [];
51
+ }
52
+
53
+ markDirty(targetState: SubscriberState = SubscriberState.DIRTY): void {
54
+ if (this.state >= targetState || this.state === SubscriberState.DISPOSED) {
55
+ return;
56
+ }
57
+ this.state = targetState;
58
+ addPendingEffect(this);
59
+
60
+ if (batchDepth === 0) {
61
+ flushEffects();
62
+ }
63
+ }
64
+
65
+ run(): void {
66
+ if (this.state === SubscriberState.DISPOSED) return;
67
+
68
+ if (this.state === SubscriberState.CHECK_DIRTY) {
69
+ let isDirty = false;
70
+ for (const source of this.sources) {
71
+ if (source instanceof Computed) {
72
+ source.updateIfNecessary();
73
+ }
74
+ const lastVersion = this.sourceVersions.get(source);
75
+ if (lastVersion !== undefined && lastVersion !== source.version) {
76
+ isDirty = true;
77
+ }
78
+ }
79
+ if (!isDirty) {
80
+ this.state = SubscriberState.CLEAN;
81
+ return;
82
+ }
83
+ }
84
+
85
+ this.runCleanups();
86
+ this.cleanupSources();
87
+
88
+ const prevSubscriber = currentSubscriber;
89
+ setCurrentSubscriber(this);
90
+ const prevScope = currentScope;
91
+ setCurrentScope(this.scope);
92
+
93
+ try {
94
+ this.state = SubscriberState.CLEAN;
95
+ const res = this._fn();
96
+ if (typeof res === "function") {
97
+ this.cleanups.push(res);
98
+ }
99
+ } finally {
100
+ setCurrentSubscriber(prevSubscriber);
101
+ setCurrentScope(prevScope);
102
+ }
103
+ }
104
+
105
+ private cleanupSources(): void {
106
+ for (const source of this.sources) {
107
+ source.removeSubscriber(this);
108
+ }
109
+ this.sources.clear();
110
+ this.sourceVersions.clear();
111
+ }
112
+
113
+ dispose(): void {
114
+ if (this.state === SubscriberState.DISPOSED) return;
115
+ this.state = SubscriberState.DISPOSED;
116
+ this.runCleanups();
117
+ this.cleanupSources();
118
+ removePendingEffect(this);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Execute side-effects reactively when dependencies change.
124
+ *
125
+ * Runs immediately upon creation and tracks any signals read within `fn`.
126
+ * When dependencies change, re-execution is scheduled automatically.
127
+ *
128
+ * An optional cleanup function can be returned from `fn`, which runs
129
+ * before the next re-execution and when the enclosing scope is disposed.
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * const count = signal(0);
134
+ *
135
+ * effect(() => {
136
+ * console.log("Count changed to:", count());
137
+ *
138
+ * const timer = setInterval(() => console.log("tick"), 1000);
139
+ * return () => clearInterval(timer);
140
+ * });
141
+ * ```
142
+ *
143
+ * @param fn - The side-effect function, optionally returning a cleanup function
144
+ * @returns A manual disposal cleanup function
145
+ */
146
+ export function effect(fn: () => void | CleanupFn): CleanupFn {
147
+ const eff = new Effect(fn);
148
+ return () => eff.dispose();
149
+ }
150
+
151
+ /**
152
+ * Solid / React-compatible alias for `effect()`.
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * createEffect(() => {
157
+ * document.title = `Total: ${count()}`;
158
+ * });
159
+ * ```
160
+ */
161
+ export const createEffect = effect;
@@ -0,0 +1,57 @@
1
+ export {
2
+ Computed,
3
+ type ComputedOptions,
4
+ computed,
5
+ createMemo
6
+ } from "./computed";
7
+
8
+ export {
9
+ type Context,
10
+ createContext,
11
+ useContext
12
+ } from "./context";
13
+
14
+ export {
15
+ createEffect,
16
+ Effect,
17
+ effect
18
+ } from "./effect";
19
+
20
+ export {
21
+ createRoot,
22
+ getActiveScope,
23
+ onCleanup,
24
+ runInScope,
25
+ Scope
26
+ } from "./scope";
27
+
28
+ export {
29
+ createSignal,
30
+ Signal,
31
+ type SignalOptions,
32
+ signal
33
+ } from "./signal";
34
+
35
+ export {
36
+ createStore,
37
+ mutateStore,
38
+ type Store,
39
+ unwrapStore
40
+ } from "./store";
41
+
42
+ export {
43
+ batch,
44
+ untrack
45
+ } from "./system";
46
+
47
+ export {
48
+ type CleanupFn,
49
+ COMPUTED_BRAND,
50
+ type Dependency,
51
+ isSignal,
52
+ type ReadonlySignal,
53
+ SIGNAL_BRAND,
54
+ type Subscriber,
55
+ SubscriberState,
56
+ type WritableSignal
57
+ } from "./types";
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Visage Reactive Scope & Lifecycle Management
3
+ */
4
+ import { currentScope, currentSubscriber, getCurrentScope, setCurrentScope } from "./system";
5
+ import type { CleanupFn, ISubscriber } from "./types";
6
+
7
+ /**
8
+ * Reactive Scope for lifecycle and automatic memory management
9
+ */
10
+ export class Scope {
11
+ subscribers = new Set<ISubscriber>();
12
+ cleanups: CleanupFn[] = [];
13
+ parent: Scope | null = null;
14
+ isDisposed = false;
15
+ context?: Map<symbol, any>;
16
+
17
+ constructor(parent: Scope | null = currentScope) {
18
+ this.parent = parent;
19
+ if (parent && !parent.isDisposed) {
20
+ parent.cleanups.push(() => this.dispose());
21
+ if (parent.context) {
22
+ this.context = new Map(parent.context);
23
+ }
24
+ }
25
+ }
26
+
27
+ addSubscriber(sub: ISubscriber): void {
28
+ if (this.isDisposed) return;
29
+ this.subscribers.add(sub);
30
+ }
31
+
32
+ addCleanup(fn: CleanupFn): void {
33
+ if (this.isDisposed) {
34
+ fn();
35
+ return;
36
+ }
37
+ this.cleanups.push(fn);
38
+ }
39
+
40
+ dispose(): void {
41
+ if (this.isDisposed) return;
42
+ this.isDisposed = true;
43
+ for (const sub of this.subscribers) {
44
+ sub.dispose();
45
+ }
46
+ this.subscribers.clear();
47
+
48
+ for (let i = this.cleanups.length - 1; i >= 0; i--) {
49
+ try {
50
+ this.cleanups[i]!();
51
+ } catch (err) {
52
+ console.error("Error during cleanup:", err);
53
+ }
54
+ }
55
+ this.cleanups = [];
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Register a cleanup callback for the current running effect or active scope.
61
+ *
62
+ * Runs automatically when the enclosing component/scope unmounts or before
63
+ * the effect re-executes.
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * function TimerComponent() {
68
+ * const id = setInterval(() => console.log("tick"), 1000);
69
+ * onCleanup(() => clearInterval(id));
70
+ * return <div>Timer running</div>;
71
+ * }
72
+ * ```
73
+ */
74
+ export function onCleanup(fn: CleanupFn): void {
75
+ if (currentSubscriber && "addCleanup" in currentSubscriber) {
76
+ (currentSubscriber as any).addCleanup(fn);
77
+ } else if (currentScope) {
78
+ currentScope.addCleanup(fn);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create an isolated reactive root scope that outlives the current execution context.
84
+ *
85
+ * Computations and signals created inside `fn` will not be tied to any parent scope
86
+ * and will persist until the provided `dispose` callback is invoked.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * const dispose = createRoot((dispose) => {
91
+ * const count = signal(0);
92
+ * effect(() => console.log(count()));
93
+ * return dispose;
94
+ * });
95
+ *
96
+ * // Later, dispose of all memory & subscribers
97
+ * dispose();
98
+ * ```
99
+ *
100
+ * @param fn - A factory function receiving the `dispose` callback
101
+ * @returns The return value of `fn`
102
+ */
103
+ export function createRoot<T>(fn: (dispose: () => void) => T): T {
104
+ const prevScope = currentScope;
105
+ const scope = new Scope(null);
106
+ setCurrentScope(scope);
107
+ try {
108
+ return fn(() => scope.dispose());
109
+ } finally {
110
+ setCurrentScope(prevScope);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Run a function with a specified reactive scope active.
116
+ */
117
+ export function runInScope<T>(scope: Scope | null, fn: () => T): T {
118
+ const prevScope = currentScope;
119
+ setCurrentScope(scope);
120
+ try {
121
+ return fn();
122
+ } finally {
123
+ setCurrentScope(prevScope);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get the currently active reactive scope.
129
+ */
130
+ export function getActiveScope(): Scope | null {
131
+ return getCurrentScope();
132
+ }