@lab206/core 0.1.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 +114 -0
- package/README.md +6 -0
- package/dist/effect.d.ts +31 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +85 -0
- package/dist/effect.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/errors.js.map +1 -0
- package/dist/graph.d.ts +62 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +165 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +35 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +35 -0
- package/dist/query.js.map +1 -0
- package/dist/resource.d.ts +55 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +128 -0
- package/dist/resource.js.map +1 -0
- package/dist/scheduler.d.ts +13 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +62 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/signal.d.ts +25 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +94 -0
- package/dist/signal.js.map +1 -0
- package/dist/store.d.ts +52 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +70 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/effect.ts +101 -0
- package/src/errors.ts +84 -0
- package/src/graph.ts +218 -0
- package/src/index.ts +45 -0
- package/src/query.ts +58 -0
- package/src/resource.ts +239 -0
- package/src/scheduler.ts +63 -0
- package/src/signal.ts +115 -0
- package/src/store.ts +118 -0
- package/src/types.ts +34 -0
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Resource } from "./resource.js";
|
|
2
|
+
export type QueryKey = string | number | false | null | undefined;
|
|
3
|
+
export type CreateQueryOptions<T> = {
|
|
4
|
+
/**
|
|
5
|
+
* Reactive key. When `false` / `null` / `undefined`, the query is idle
|
|
6
|
+
* (no fetch). Change the key to re-run — same mental model as signals.
|
|
7
|
+
*/
|
|
8
|
+
queryKey: () => QueryKey;
|
|
9
|
+
/** Async work for the current key. */
|
|
10
|
+
queryFn: (key: string) => Promise<T> | T;
|
|
11
|
+
/** Seed UI before the first success. */
|
|
12
|
+
initialData?: T;
|
|
13
|
+
/** Debug name for the underlying resource. */
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Signal-native async query — thin, intentional ergonomics on `resource`.
|
|
18
|
+
*
|
|
19
|
+
* Why it feels great with Powers:
|
|
20
|
+
* - The key is a **function of signals** — no dependency arrays
|
|
21
|
+
* - `data()` / `loading()` / `error()` are fine-grained
|
|
22
|
+
* - `refetch()` re-runs without remounting the page
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const q = signal("design");
|
|
27
|
+
* const art = createQuery({
|
|
28
|
+
* queryKey: () => q(),
|
|
29
|
+
* queryFn: (key) => fetch(`/api/search?q=${key}`).then((r) => r.json()),
|
|
30
|
+
* });
|
|
31
|
+
* // art() · art.loading() · art.error() · art.refetch()
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function createQuery<T>(options: CreateQueryOptions<T>): Resource<T>;
|
|
35
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC;AAElE,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAClC;;;OAGG;IACH,QAAQ,EAAE,MAAM,QAAQ,CAAC;IACzB,sCAAsC;IACtC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzC,wCAAwC;IACxC,WAAW,CAAC,EAAE,CAAC,CAAC;IAChB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,QAAQ,CAAC,CAAC,CAAC,CAkBb"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resource } from "./resource.js";
|
|
2
|
+
/**
|
|
3
|
+
* Signal-native async query — thin, intentional ergonomics on `resource`.
|
|
4
|
+
*
|
|
5
|
+
* Why it feels great with Powers:
|
|
6
|
+
* - The key is a **function of signals** — no dependency arrays
|
|
7
|
+
* - `data()` / `loading()` / `error()` are fine-grained
|
|
8
|
+
* - `refetch()` re-runs without remounting the page
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const q = signal("design");
|
|
13
|
+
* const art = createQuery({
|
|
14
|
+
* queryKey: () => q(),
|
|
15
|
+
* queryFn: (key) => fetch(`/api/search?q=${key}`).then((r) => r.json()),
|
|
16
|
+
* });
|
|
17
|
+
* // art() · art.loading() · art.error() · art.refetch()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createQuery(options) {
|
|
21
|
+
const opts = {
|
|
22
|
+
name: options.name ?? "query",
|
|
23
|
+
};
|
|
24
|
+
if (options.initialData !== undefined) {
|
|
25
|
+
opts.initialValue = options.initialData;
|
|
26
|
+
}
|
|
27
|
+
// Source is `string | false` so resource can idle; fetcher only runs when key is string.
|
|
28
|
+
return resource(() => {
|
|
29
|
+
const k = options.queryKey();
|
|
30
|
+
if (k === false || k === null || k === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
return String(k);
|
|
33
|
+
}, (key) => options.queryFn(key), opts);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAmBzC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CACzB,OAA8B;IAE9B,MAAM,IAAI,GAAuB;QAC/B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO;KAC9B,CAAC;IACF,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAC1C,CAAC;IAED,yFAAyF;IACzF,OAAO,QAAQ,CACb,GAAmB,EAAE;QACnB,MAAM,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC/D,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC,EACD,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAa,CAAC,EACvC,IAAI,CACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReadonlySignal, SignalOptions } from "./types.js";
|
|
2
|
+
/** Lifecycle of a resource fetch. */
|
|
3
|
+
export type ResourceState = "pending" | "ready" | "errored";
|
|
4
|
+
export interface ResourceFetcherInfo<T> {
|
|
5
|
+
/** Previous successful value, if any. */
|
|
6
|
+
value: T | undefined;
|
|
7
|
+
/** True when we already had data and are fetching again. */
|
|
8
|
+
refetching: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type ResourceFetcher<T, S> = (source: S, info: ResourceFetcherInfo<T>) => T | Promise<T>;
|
|
11
|
+
export interface ResourceOptions<T> extends SignalOptions<T | undefined> {
|
|
12
|
+
/** Initial value before the first fetch settles. */
|
|
13
|
+
initialValue?: T;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Async-friendly reactive value.
|
|
17
|
+
*
|
|
18
|
+
* Learn it in 30 seconds:
|
|
19
|
+
* - `user()` → data (or `undefined` while loading the first time)
|
|
20
|
+
* - `user.loading()` → is a fetch in flight?
|
|
21
|
+
* - `user.error()` → last error, if any
|
|
22
|
+
* - `user.refetch()` → run again
|
|
23
|
+
*
|
|
24
|
+
* @example Fetcher only
|
|
25
|
+
* ```ts
|
|
26
|
+
* const users = resource(async () => {
|
|
27
|
+
* const res = await fetch("/api/users");
|
|
28
|
+
* return res.json() as Promise<User[]>;
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example Reactive source + fetcher (re-runs when source changes)
|
|
33
|
+
* ```ts
|
|
34
|
+
* const id = signal(1);
|
|
35
|
+
* const user = resource(
|
|
36
|
+
* () => id(),
|
|
37
|
+
* async (id) => (await fetch(`/api/users/${id}`)).json(),
|
|
38
|
+
* );
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface Resource<T> extends ReadonlySignal<T | undefined> {
|
|
42
|
+
/** True while a fetch is in flight. */
|
|
43
|
+
readonly loading: ReadonlySignal<boolean>;
|
|
44
|
+
/** Last error from the fetcher (cleared on success). */
|
|
45
|
+
readonly error: ReadonlySignal<unknown>;
|
|
46
|
+
/** Last successful value — kept during refetch so UI can stay stable. */
|
|
47
|
+
readonly latest: ReadonlySignal<T | undefined>;
|
|
48
|
+
/** Coarse lifecycle state. */
|
|
49
|
+
readonly state: ReadonlySignal<ResourceState>;
|
|
50
|
+
/** Re-run the fetcher with the current source. */
|
|
51
|
+
refetch(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare function resource<T>(fetcher: (source: true, info: ResourceFetcherInfo<T>) => T | Promise<T>, options?: ResourceOptions<T>): Resource<T>;
|
|
54
|
+
export declare function resource<T, S>(source: () => S, fetcher: ResourceFetcher<T, S>, options?: ResourceOptions<T>): Resource<T>;
|
|
55
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,qCAAqC;AACrC,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,yCAAyC;IACzC,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC;IACrB,4DAA4D;IAC5D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAClC,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,KACzB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEpB,MAAM,WAAW,eAAe,CAAC,CAAC,CAAE,SAAQ,aAAa,CAAC,CAAC,GAAG,SAAS,CAAC;IACtE,oDAAoD;IACpD,YAAY,CAAC,EAAE,CAAC,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,GAAG,SAAS,CAAC;IAChE,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IACxC,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/C,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IAC9C,kDAAkD;IAClD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,QAAQ,CAAC,CAAC,EACxB,OAAO,EAAE,CACP,MAAM,EAAE,IAAI,EACZ,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,KACzB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EACnB,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEf,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,EACf,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,GAC3B,QAAQ,CAAC,CAAC,CAAC,CAAC"}
|
package/dist/resource.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { untrack } from "./graph.js";
|
|
2
|
+
import { batch } from "./scheduler.js";
|
|
3
|
+
import { computed, signal } from "./signal.js";
|
|
4
|
+
import { effect } from "./effect.js";
|
|
5
|
+
export function resource(sourceOrFetcher, fetcherOrOptions, maybeOptions) {
|
|
6
|
+
const hasSource = typeof fetcherOrOptions === "function";
|
|
7
|
+
const source = (hasSource ? sourceOrFetcher : () => true);
|
|
8
|
+
const fetcher = (hasSource
|
|
9
|
+
? fetcherOrOptions
|
|
10
|
+
: sourceOrFetcher);
|
|
11
|
+
const options = (hasSource ? maybeOptions : fetcherOrOptions);
|
|
12
|
+
const initial = options?.initialValue;
|
|
13
|
+
const data = signal(initial, {
|
|
14
|
+
name: options?.name ? `${options.name}:data` : "resource:data",
|
|
15
|
+
...(options?.equals ? { equals: options.equals } : {}),
|
|
16
|
+
});
|
|
17
|
+
const loading = signal(true, { name: "resource:loading" });
|
|
18
|
+
const error = signal(undefined, { name: "resource:error" });
|
|
19
|
+
const latest = signal(initial, { name: "resource:latest" });
|
|
20
|
+
const tick = signal(0, { name: "resource:tick" });
|
|
21
|
+
const state = computed(() => {
|
|
22
|
+
if (error() !== undefined)
|
|
23
|
+
return "errored";
|
|
24
|
+
if (loading())
|
|
25
|
+
return "pending";
|
|
26
|
+
return "ready";
|
|
27
|
+
}, { name: "resource:state" });
|
|
28
|
+
let currentToken = 0;
|
|
29
|
+
let disposed = false;
|
|
30
|
+
let hasSucceeded = initial !== undefined;
|
|
31
|
+
const runFetch = () => {
|
|
32
|
+
if (disposed)
|
|
33
|
+
return;
|
|
34
|
+
const token = ++currentToken;
|
|
35
|
+
const sourceValue = untrack(() => source());
|
|
36
|
+
// Skip until the source is ready (null | undefined | false).
|
|
37
|
+
if (sourceValue === false || sourceValue === null || sourceValue === undefined) {
|
|
38
|
+
batch(() => {
|
|
39
|
+
loading.set(false);
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const refetching = hasSucceeded;
|
|
44
|
+
batch(() => {
|
|
45
|
+
loading.set(true);
|
|
46
|
+
error.set(undefined);
|
|
47
|
+
});
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = fetcher(sourceValue, {
|
|
51
|
+
value: latest.peek(),
|
|
52
|
+
refetching,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (token !== currentToken || disposed)
|
|
57
|
+
return;
|
|
58
|
+
batch(() => {
|
|
59
|
+
error.set(err);
|
|
60
|
+
loading.set(false);
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const settle = (ok, value) => {
|
|
65
|
+
if (token !== currentToken || disposed)
|
|
66
|
+
return;
|
|
67
|
+
batch(() => {
|
|
68
|
+
if (ok) {
|
|
69
|
+
data.set(value);
|
|
70
|
+
latest.set(value);
|
|
71
|
+
error.set(undefined);
|
|
72
|
+
hasSucceeded = true;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
error.set(value);
|
|
76
|
+
}
|
|
77
|
+
loading.set(false);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
if (isThenable(result)) {
|
|
81
|
+
result.then((value) => settle(true, value), (err) => settle(false, err));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
settle(true, result);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
effect(() => {
|
|
88
|
+
source();
|
|
89
|
+
tick();
|
|
90
|
+
runFetch();
|
|
91
|
+
return () => {
|
|
92
|
+
// Invalidate in-flight work when deps change or the effect disposes.
|
|
93
|
+
currentToken++;
|
|
94
|
+
};
|
|
95
|
+
}, { name: options?.name ? `${options.name}:effect` : "resource:effect" });
|
|
96
|
+
// When the owner disposes all effects, in-flight promises must no-op.
|
|
97
|
+
// The cleanup above bumps the token on each re-run; final dispose also runs it.
|
|
98
|
+
// Additionally guard with a disposed flag via a zero-dep lifetime effect:
|
|
99
|
+
effect(() => {
|
|
100
|
+
return () => {
|
|
101
|
+
disposed = true;
|
|
102
|
+
currentToken++;
|
|
103
|
+
};
|
|
104
|
+
}, { name: "resource:lifetime" });
|
|
105
|
+
const read = Object.assign((() => data()), {
|
|
106
|
+
peek: () => data.peek(),
|
|
107
|
+
loading,
|
|
108
|
+
error,
|
|
109
|
+
latest,
|
|
110
|
+
state,
|
|
111
|
+
refetch: () => {
|
|
112
|
+
if (disposed)
|
|
113
|
+
return;
|
|
114
|
+
tick.update((v) => v + 1);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (options?.name !== undefined) {
|
|
118
|
+
Object.defineProperty(read, "name", { value: options.name });
|
|
119
|
+
}
|
|
120
|
+
return read;
|
|
121
|
+
}
|
|
122
|
+
function isThenable(value) {
|
|
123
|
+
return (typeof value === "object" &&
|
|
124
|
+
value !== null &&
|
|
125
|
+
"then" in value &&
|
|
126
|
+
typeof value.then === "function");
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactiveNode } from "./graph.js";
|
|
2
|
+
/** Queue an effect node for execution. */
|
|
3
|
+
export declare function enqueue(node: ReactiveNode): void;
|
|
4
|
+
/** Run all queued effects. Nested enqueues during flush are drained too. */
|
|
5
|
+
export declare function flush(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Batch multiple writes so dependents run once after the callback.
|
|
8
|
+
* Nested batches collapse into the outermost boundary.
|
|
9
|
+
*/
|
|
10
|
+
export declare function batch<T>(fn: () => T): T;
|
|
11
|
+
/** True when currently inside `batch()`. */
|
|
12
|
+
export declare function isBatching(): boolean;
|
|
13
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAe/C,0CAA0C;AAC1C,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAKhD;AAED,4EAA4E;AAC5E,wBAAgB,KAAK,IAAI,IAAI,CAiB5B;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAUvC;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,IAAI,OAAO,CAEpC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Effects queued during the current flush cycle. */
|
|
2
|
+
const queue = [];
|
|
3
|
+
let scheduled = false;
|
|
4
|
+
let flushing = false;
|
|
5
|
+
let batchDepth = 0;
|
|
6
|
+
function scheduleFlush() {
|
|
7
|
+
// While flushing, newly enqueued work is drained by the active for-loop.
|
|
8
|
+
if (scheduled || batchDepth > 0 || flushing)
|
|
9
|
+
return;
|
|
10
|
+
scheduled = true;
|
|
11
|
+
queueMicrotask(flush);
|
|
12
|
+
}
|
|
13
|
+
/** Queue an effect node for execution. */
|
|
14
|
+
export function enqueue(node) {
|
|
15
|
+
if (node.pending)
|
|
16
|
+
return;
|
|
17
|
+
node.pending = true;
|
|
18
|
+
queue.push(node);
|
|
19
|
+
scheduleFlush();
|
|
20
|
+
}
|
|
21
|
+
/** Run all queued effects. Nested enqueues during flush are drained too. */
|
|
22
|
+
export function flush() {
|
|
23
|
+
if (flushing)
|
|
24
|
+
return;
|
|
25
|
+
flushing = true;
|
|
26
|
+
scheduled = false;
|
|
27
|
+
try {
|
|
28
|
+
// Index loop so effects enqueued during a run are drained in this pass.
|
|
29
|
+
for (let i = 0; i < queue.length; i++) {
|
|
30
|
+
const node = queue[i];
|
|
31
|
+
node.pending = false;
|
|
32
|
+
if (node.disposed || !node.run)
|
|
33
|
+
continue;
|
|
34
|
+
node.run();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
queue.length = 0;
|
|
39
|
+
flushing = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Batch multiple writes so dependents run once after the callback.
|
|
44
|
+
* Nested batches collapse into the outermost boundary.
|
|
45
|
+
*/
|
|
46
|
+
export function batch(fn) {
|
|
47
|
+
batchDepth++;
|
|
48
|
+
try {
|
|
49
|
+
return fn();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
batchDepth--;
|
|
53
|
+
if (batchDepth === 0 && queue.length > 0) {
|
|
54
|
+
flush();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** True when currently inside `batch()`. */
|
|
59
|
+
export function isBatching() {
|
|
60
|
+
return batchDepth > 0;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAEA,qDAAqD;AACrD,MAAM,KAAK,GAAmB,EAAE,CAAC;AACjC,IAAI,SAAS,GAAG,KAAK,CAAC;AACtB,IAAI,QAAQ,GAAG,KAAK,CAAC;AACrB,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB,SAAS,aAAa;IACpB,yEAAyE;IACzE,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,IAAI,QAAQ;QAAE,OAAO;IACpD,SAAS,GAAG,IAAI,CAAC;IACjB,cAAc,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,OAAO,CAAC,IAAkB;IACxC,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO;IACzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,aAAa,EAAE,CAAC;AAClB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,KAAK;IACnB,IAAI,QAAQ;QAAE,OAAO;IACrB,QAAQ,GAAG,IAAI,CAAC;IAChB,SAAS,GAAG,KAAK,CAAC;IAElB,IAAI,CAAC;QACH,wEAAwE;QACxE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YACvB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;YACrB,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,GAAG;gBAAE,SAAS;YACzC,IAAI,CAAC,GAAG,EAAE,CAAC;QACb,CAAC;IACH,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QACjB,QAAQ,GAAG,KAAK,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAI,EAAW;IAClC,UAAU,EAAE,CAAC;IACb,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,UAAU,EAAE,CAAC;QACb,IAAI,UAAU,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;AACH,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,GAAG,CAAC,CAAC;AACxB,CAAC"}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReadonlySignal, Signal, SignalOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a writable fine-grained reactive value.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const count = signal(0);
|
|
8
|
+
* count(); // 0 (tracked read)
|
|
9
|
+
* count.set(1);
|
|
10
|
+
* count.update(n => n + 1);
|
|
11
|
+
* count.peek(); // untracked read
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Create a derived readonly value that recomputes only when dependencies change.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const double = computed(() => count() * 2);
|
|
21
|
+
* double(); // tracked
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function computed<T>(fn: () => T, options?: SignalOptions<T>): ReadonlySignal<T>;
|
|
25
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIxE;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACzB,MAAM,CAAC,CAAC,CAAC,CA2BX;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,EAAE,EAAE,MAAM,CAAC,EACX,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GACzB,cAAc,CAAC,CAAC,CAAC,CAqDnB"}
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createNode, notify, setActiveNode, track } from "./graph.js";
|
|
2
|
+
const defaultEquals = Object.is;
|
|
3
|
+
/**
|
|
4
|
+
* Create a writable fine-grained reactive value.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const count = signal(0);
|
|
9
|
+
* count(); // 0 (tracked read)
|
|
10
|
+
* count.set(1);
|
|
11
|
+
* count.update(n => n + 1);
|
|
12
|
+
* count.peek(); // untracked read
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function signal(initial, options) {
|
|
16
|
+
const equals = options?.equals ?? defaultEquals;
|
|
17
|
+
const node = createNode("signal", options?.name);
|
|
18
|
+
let value = initial;
|
|
19
|
+
const read = (() => {
|
|
20
|
+
track(node);
|
|
21
|
+
return value;
|
|
22
|
+
});
|
|
23
|
+
read.peek = () => value;
|
|
24
|
+
read.set = (next) => {
|
|
25
|
+
if (equals(value, next))
|
|
26
|
+
return;
|
|
27
|
+
value = next;
|
|
28
|
+
notify(node);
|
|
29
|
+
};
|
|
30
|
+
read.update = (fn) => {
|
|
31
|
+
read.set(fn(value));
|
|
32
|
+
};
|
|
33
|
+
if (options?.name !== undefined) {
|
|
34
|
+
Object.defineProperty(read, "name", { value: options.name });
|
|
35
|
+
}
|
|
36
|
+
return read;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a derived readonly value that recomputes only when dependencies change.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const double = computed(() => count() * 2);
|
|
44
|
+
* double(); // tracked
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function computed(fn, options) {
|
|
48
|
+
const equals = options?.equals ?? defaultEquals;
|
|
49
|
+
const node = createNode("computed", options?.name);
|
|
50
|
+
let value;
|
|
51
|
+
let hasValue = false;
|
|
52
|
+
const recompute = () => {
|
|
53
|
+
// Drop previous source links before re-tracking.
|
|
54
|
+
for (const source of node.sources) {
|
|
55
|
+
source.observers.delete(node);
|
|
56
|
+
}
|
|
57
|
+
node.sources.clear();
|
|
58
|
+
const prev = setActiveNode(node);
|
|
59
|
+
try {
|
|
60
|
+
const next = fn();
|
|
61
|
+
const changed = !hasValue || !equals(value, next);
|
|
62
|
+
value = next;
|
|
63
|
+
hasValue = true;
|
|
64
|
+
node.dirty = false;
|
|
65
|
+
// If value is unchanged after a dirty cycle, suppress further
|
|
66
|
+
// effect churn by not re-notifying. Effects already enqueued will
|
|
67
|
+
// still run once and see the same value — acceptable for v0.1.
|
|
68
|
+
void changed;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setActiveNode(prev);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
node.run = recompute;
|
|
75
|
+
const ensure = () => {
|
|
76
|
+
if (node.disposed)
|
|
77
|
+
return value;
|
|
78
|
+
if (node.dirty || !hasValue) {
|
|
79
|
+
recompute();
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
const read = (() => {
|
|
84
|
+
const v = ensure();
|
|
85
|
+
track(node);
|
|
86
|
+
return v;
|
|
87
|
+
});
|
|
88
|
+
read.peek = () => ensure();
|
|
89
|
+
if (options?.name !== undefined) {
|
|
90
|
+
Object.defineProperty(read, "name", { value: options.name });
|
|
91
|
+
}
|
|
92
|
+
return read;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=signal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Signal, SignalOptions } from "./types.js";
|
|
2
|
+
type PlainObject = Record<string, unknown>;
|
|
3
|
+
/** Per-key signal accessors on a store. */
|
|
4
|
+
export type StoreFields<T extends PlainObject> = {
|
|
5
|
+
readonly [K in keyof T]: Signal<T[K]>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Shallow reactive object — each top-level key is a signal.
|
|
9
|
+
*
|
|
10
|
+
* This is the easiest multi-field state model to learn:
|
|
11
|
+
* ```ts
|
|
12
|
+
* const app = store({ count: 0, name: "Ada" });
|
|
13
|
+
* app.count(); // read
|
|
14
|
+
* app.count.set(1); // write one field
|
|
15
|
+
* app.set({ count: 2, name: "Grace" }); // write many (batched)
|
|
16
|
+
* app(); // snapshot { count, name } (tracks all keys)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type Store<T extends PlainObject> = StoreFields<T> & {
|
|
20
|
+
/** Reactive snapshot of all fields. */
|
|
21
|
+
(): {
|
|
22
|
+
[K in keyof T]: T[K];
|
|
23
|
+
};
|
|
24
|
+
/** Untracked snapshot. */
|
|
25
|
+
peek(): {
|
|
26
|
+
[K in keyof T]: T[K];
|
|
27
|
+
};
|
|
28
|
+
/** Batch-update one or more fields. */
|
|
29
|
+
set(partial: Partial<{
|
|
30
|
+
[K in keyof T]: T[K];
|
|
31
|
+
}>): void;
|
|
32
|
+
/** Replace every field from a full object. */
|
|
33
|
+
replace(next: {
|
|
34
|
+
[K in keyof T]: T[K];
|
|
35
|
+
}): void;
|
|
36
|
+
};
|
|
37
|
+
export interface StoreOptions {
|
|
38
|
+
name?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a shallow store from a plain object.
|
|
42
|
+
* Nested objects are treated as single values (replace the whole nest to update).
|
|
43
|
+
* Prefer nesting `store()` calls if you need deep fine-grained fields.
|
|
44
|
+
*/
|
|
45
|
+
export declare function store<T extends PlainObject>(initial: T, options?: StoreOptions): Store<T>;
|
|
46
|
+
/**
|
|
47
|
+
* Convenience: create a single named signal with the same options shape.
|
|
48
|
+
* Prefer `signal()` — this exists for API symmetry with `store`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function cell<T>(initial: T, options?: SignalOptions<T>): Signal<T>;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAExD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3C,2CAA2C;AAC3C,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,WAAW,IAAI;IAC/C,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;IAC1D,uCAAuC;IACvC,IAAI;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;IAC7B,0BAA0B;IAC1B,IAAI,IAAI;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;IACjC,uCAAuC;IACvC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC,GAAG,IAAI,CAAC;IACtD,8CAA8C;IAC9C,OAAO,CAAC,IAAI,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,WAAW,EACzC,OAAO,EAAE,CAAC,EACV,OAAO,CAAC,EAAE,YAAY,GACrB,KAAK,CAAC,CAAC,CAAC,CA+DV;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAEzE"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { batch } from "./scheduler.js";
|
|
2
|
+
import { signal } from "./signal.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create a shallow store from a plain object.
|
|
5
|
+
* Nested objects are treated as single values (replace the whole nest to update).
|
|
6
|
+
* Prefer nesting `store()` calls if you need deep fine-grained fields.
|
|
7
|
+
*/
|
|
8
|
+
export function store(initial, options) {
|
|
9
|
+
const keys = Object.keys(initial);
|
|
10
|
+
const fields = {};
|
|
11
|
+
for (const key of keys) {
|
|
12
|
+
fields[key] = signal(initial[key], {
|
|
13
|
+
name: options?.name ? `${options.name}.${String(key)}` : String(key),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const snapshot = () => {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
out[key] = fields[key]();
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
const peek = () => {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
out[key] = fields[key].peek();
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
const set = (partial) => {
|
|
31
|
+
batch(() => {
|
|
32
|
+
for (const key of Object.keys(partial)) {
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
34
|
+
fields[key].set(partial[key]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const replace = (next) => {
|
|
40
|
+
batch(() => {
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
fields[key].set(next[key]);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
// Callable store + field properties.
|
|
47
|
+
const api = snapshot;
|
|
48
|
+
api.peek = peek;
|
|
49
|
+
api.set = set;
|
|
50
|
+
api.replace = replace;
|
|
51
|
+
for (const key of keys) {
|
|
52
|
+
Object.defineProperty(api, key, {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: false,
|
|
55
|
+
get: () => fields[key],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (options?.name !== undefined) {
|
|
59
|
+
Object.defineProperty(api, "name", { value: options.name });
|
|
60
|
+
}
|
|
61
|
+
return api;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Convenience: create a single named signal with the same options shape.
|
|
65
|
+
* Prefer `signal()` — this exists for API symmetry with `store`.
|
|
66
|
+
*/
|
|
67
|
+
export function cell(initial, options) {
|
|
68
|
+
return signal(initial, options);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A function that reads reactive sources and may return a cleanup. */
|
|
2
|
+
export type EffectFn = () => void | (() => void);
|
|
3
|
+
/** Dispose callback that tears down an owner subtree or effect. */
|
|
4
|
+
export type Dispose = () => void;
|
|
5
|
+
/** Options for creating a writable signal. */
|
|
6
|
+
export interface SignalOptions<T> {
|
|
7
|
+
/**
|
|
8
|
+
* Equality check. Defaults to `Object.is`.
|
|
9
|
+
* Return true when the next value should be treated as unchanged.
|
|
10
|
+
*/
|
|
11
|
+
equals?: (prev: T, next: T) => boolean;
|
|
12
|
+
/** Optional debug name for DevTools / error messages. */
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Readable reactive value. */
|
|
16
|
+
export interface ReadonlySignal<T> {
|
|
17
|
+
/** Tracked read — registers the active effect/computed as a dependent. */
|
|
18
|
+
(): T;
|
|
19
|
+
/** Untracked read — does not register a dependency. */
|
|
20
|
+
peek(): T;
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Writable reactive value. */
|
|
24
|
+
export interface Signal<T> extends ReadonlySignal<T> {
|
|
25
|
+
set(value: T): void;
|
|
26
|
+
update(fn: (prev: T) => T): void;
|
|
27
|
+
}
|
|
28
|
+
/** Internal node kinds used by the graph. */
|
|
29
|
+
export type NodeKind = "signal" | "computed" | "effect";
|
|
30
|
+
//# sourceMappingURL=types.d.ts.map
|