@chrrrs/signals 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/README.md +25 -0
- package/dist/async.d.ts +6 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +29 -0
- package/dist/computed.d.ts +7 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.js +80 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/internal.d.ts +10 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +14 -0
- package/dist/signal.d.ts +8 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +27 -0
- package/dist/useSignal.d.ts +6 -0
- package/dist/useSignal.d.ts.map +1 -0
- package/dist/useSignal.js +18 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @chrrrs/signals
|
|
2
|
+
|
|
3
|
+
Minimal React 19 signals for local state and derived values.
|
|
4
|
+
|
|
5
|
+
## SSR note
|
|
6
|
+
|
|
7
|
+
Signals created at module scope are shared across SSR requests.
|
|
8
|
+
|
|
9
|
+
Avoid:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export const count = createSignal(0);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createSignal } from "@chrrrs/signals";
|
|
19
|
+
|
|
20
|
+
export function createState() {
|
|
21
|
+
return {
|
|
22
|
+
count: createSignal(0),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
```
|
package/dist/async.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../src/async.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAkChF"}
|
package/dist/async.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createSignal } from "./signal.js";
|
|
2
|
+
export function createAsyncSignal(loadValue) {
|
|
3
|
+
const valueSignal = createSignal(undefined);
|
|
4
|
+
let pendingPromise = null;
|
|
5
|
+
let hasResolved = false;
|
|
6
|
+
return {
|
|
7
|
+
get() {
|
|
8
|
+
return valueSignal.get();
|
|
9
|
+
},
|
|
10
|
+
load() {
|
|
11
|
+
if (hasResolved) {
|
|
12
|
+
return Promise.resolve(valueSignal.get());
|
|
13
|
+
}
|
|
14
|
+
if (pendingPromise) {
|
|
15
|
+
return pendingPromise;
|
|
16
|
+
}
|
|
17
|
+
pendingPromise = loadValue().then((value) => {
|
|
18
|
+
valueSignal.set(value);
|
|
19
|
+
hasResolved = true;
|
|
20
|
+
pendingPromise = null;
|
|
21
|
+
return value;
|
|
22
|
+
}, (error) => {
|
|
23
|
+
pendingPromise = null;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
return pendingPromise;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../src/computed.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IACxB,GAAG,IAAI,CAAC,CAAC;IACT,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,IAAI,CAAC;CACvC,CAAC;AA4FF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAO9D"}
|
package/dist/computed.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { registerDependency, withDependencyCollector, } from "./internal.js";
|
|
2
|
+
class ComputedSignal {
|
|
3
|
+
computeValue;
|
|
4
|
+
subscribers = new Set();
|
|
5
|
+
dependencies = new Map();
|
|
6
|
+
cachedValue;
|
|
7
|
+
hasCachedValue = false;
|
|
8
|
+
isDirty = true;
|
|
9
|
+
isComputing = false;
|
|
10
|
+
constructor(computeValue) {
|
|
11
|
+
this.computeValue = computeValue;
|
|
12
|
+
}
|
|
13
|
+
get() {
|
|
14
|
+
registerDependency(this);
|
|
15
|
+
if (this.isDirty) {
|
|
16
|
+
this.recompute();
|
|
17
|
+
}
|
|
18
|
+
return this.cachedValue;
|
|
19
|
+
}
|
|
20
|
+
subscribe(fn) {
|
|
21
|
+
this.subscribers.add(fn);
|
|
22
|
+
return () => {
|
|
23
|
+
this.subscribers.delete(fn);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
addDependency(dependency) {
|
|
27
|
+
if (dependency === this) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (this.dependencies.has(dependency)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const unsubscribe = dependency.subscribe(() => {
|
|
34
|
+
this.markDirty();
|
|
35
|
+
});
|
|
36
|
+
this.dependencies.set(dependency, unsubscribe);
|
|
37
|
+
}
|
|
38
|
+
markDirty() {
|
|
39
|
+
if (this.isDirty) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.isDirty = true;
|
|
43
|
+
for (const subscriber of [...this.subscribers]) {
|
|
44
|
+
subscriber();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
recompute() {
|
|
48
|
+
if (this.isComputing) {
|
|
49
|
+
throw new Error("Circular computed dependency detected");
|
|
50
|
+
}
|
|
51
|
+
this.cleanupDependencies();
|
|
52
|
+
this.isComputing = true;
|
|
53
|
+
try {
|
|
54
|
+
const nextValue = withDependencyCollector(this, this.computeValue);
|
|
55
|
+
this.cachedValue = nextValue;
|
|
56
|
+
this.hasCachedValue = true;
|
|
57
|
+
this.isDirty = false;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.cleanupDependencies();
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.isComputing = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
cleanupDependencies() {
|
|
68
|
+
for (const unsubscribe of this.dependencies.values()) {
|
|
69
|
+
unsubscribe();
|
|
70
|
+
}
|
|
71
|
+
this.dependencies.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function computed(computeValue) {
|
|
75
|
+
const computation = new ComputedSignal(computeValue);
|
|
76
|
+
return {
|
|
77
|
+
get: () => computation.get(),
|
|
78
|
+
subscribe: (fn) => computation.subscribe(fn),
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Subscriber = () => void;
|
|
2
|
+
export interface Subscribable {
|
|
3
|
+
subscribe(fn: Subscriber): () => void;
|
|
4
|
+
}
|
|
5
|
+
export interface DependencyCollector {
|
|
6
|
+
addDependency(dependency: Subscribable): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function withDependencyCollector<T>(collector: DependencyCollector, fn: () => T): T;
|
|
9
|
+
export declare function registerDependency(dependency: Subscribable): void;
|
|
10
|
+
//# sourceMappingURL=internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,UAAU,EAAE,YAAY,GAAG,IAAI,CAAC;CAC/C;AAID,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CASzF;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,YAAY,GAAG,IAAI,CAEjE"}
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let activeCollector = null;
|
|
2
|
+
export function withDependencyCollector(collector, fn) {
|
|
3
|
+
const previousCollector = activeCollector;
|
|
4
|
+
activeCollector = collector;
|
|
5
|
+
try {
|
|
6
|
+
return fn();
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
activeCollector = previousCollector;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function registerDependency(dependency) {
|
|
13
|
+
activeCollector?.addDependency(dependency);
|
|
14
|
+
}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Subscriber } from "./internal.js";
|
|
2
|
+
export type Signal<T> = {
|
|
3
|
+
get(): T;
|
|
4
|
+
set(value: T): void;
|
|
5
|
+
subscribe(fn: Subscriber): () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createSignal<T>(initialValue: T): Signal<T>;
|
|
8
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IACtB,GAAG,IAAI,CAAC,CAAC;IACT,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,MAAM,IAAI,CAAC;CACvC,CAAC;AAEF,wBAAgB,YAAY,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CA6B1D"}
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerDependency } from "./internal.js";
|
|
2
|
+
export function createSignal(initialValue) {
|
|
3
|
+
let value = initialValue;
|
|
4
|
+
const subscribers = new Set();
|
|
5
|
+
const signal = {
|
|
6
|
+
get() {
|
|
7
|
+
registerDependency(signal);
|
|
8
|
+
return value;
|
|
9
|
+
},
|
|
10
|
+
set(nextValue) {
|
|
11
|
+
if (Object.is(value, nextValue)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
value = nextValue;
|
|
15
|
+
for (const subscriber of [...subscribers]) {
|
|
16
|
+
subscriber();
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
subscribe(fn) {
|
|
20
|
+
subscribers.add(fn);
|
|
21
|
+
return () => {
|
|
22
|
+
subscribers.delete(fn);
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return signal;
|
|
27
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Signal } from "./signal.js";
|
|
2
|
+
type ReadableSignal<T> = Pick<Signal<T>, "get" | "subscribe">;
|
|
3
|
+
export declare function useSignal<T>(signal: ReadableSignal<T>): T;
|
|
4
|
+
export declare function useSignalSelector<T, U>(signal: ReadableSignal<T>, selector: (value: T) => U): U;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=useSignal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignal.d.ts","sourceRoot":"","sources":["../src/useSignal.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,cAAc,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC;AAM9D,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAEzD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EACpC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,EACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GACxB,CAAC,CAiBH"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
export function useSignal(signal) {
|
|
3
|
+
return useSyncExternalStore(signal.subscribe, signal.get, signal.get);
|
|
4
|
+
}
|
|
5
|
+
export function useSignalSelector(signal, selector) {
|
|
6
|
+
const snapshotRef = useRef(null);
|
|
7
|
+
const getSnapshot = () => {
|
|
8
|
+
const selectedValue = selector(signal.get());
|
|
9
|
+
const currentSnapshot = snapshotRef.current;
|
|
10
|
+
if (currentSnapshot !== null && Object.is(currentSnapshot.value, selectedValue)) {
|
|
11
|
+
return currentSnapshot;
|
|
12
|
+
}
|
|
13
|
+
const nextSnapshot = { value: selectedValue };
|
|
14
|
+
snapshotRef.current = nextSnapshot;
|
|
15
|
+
return nextSnapshot;
|
|
16
|
+
};
|
|
17
|
+
return useSyncExternalStore(signal.subscribe, getSnapshot, getSnapshot).value;
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chrrrs/signals",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal React 19 signals for local state and derived values.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"signals",
|
|
8
|
+
"state",
|
|
9
|
+
"typescript",
|
|
10
|
+
"suspense"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/chrrrs/signals.git",
|
|
16
|
+
"directory": "packages/signals"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/chrrrs/signals/tree/main/packages/signals",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/chrrrs/signals/issues"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
36
|
+
"build": "npm run clean && tsc -b",
|
|
37
|
+
"check-types": "tsc --noEmit",
|
|
38
|
+
"test": "vitest",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^19.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@repo/typescript-config": "workspace:*",
|
|
49
|
+
"@types/node": "^22.15.3",
|
|
50
|
+
"@types/react": "19.2.2",
|
|
51
|
+
"@types/react-dom": "19.2.2",
|
|
52
|
+
"jsdom": "^26.1.0",
|
|
53
|
+
"react": "^19.2.0",
|
|
54
|
+
"react-dom": "^19.2.0",
|
|
55
|
+
"typescript": "5.9.2",
|
|
56
|
+
"vitest": "^3.2.4"
|
|
57
|
+
}
|
|
58
|
+
}
|