@epoch32/visage 0.0.0-stage → 0.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.
- package/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- 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
|
+
}
|