@figliolia/signals 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 (53) hide show
  1. package/README.md +109 -0
  2. package/dist/index.cjs +14 -0
  3. package/dist/index.d.cts +6 -0
  4. package/dist/index.d.mts +8 -0
  5. package/dist/index.mjs +9 -0
  6. package/dist/react/bindToReact.cjs +10 -0
  7. package/dist/react/bindToReact.mjs +10 -0
  8. package/dist/react/index.cjs +2 -0
  9. package/dist/react/index.d.mts +2 -0
  10. package/dist/react/index.mjs +4 -0
  11. package/dist/react/useComputed.cjs +14 -0
  12. package/dist/react/useComputed.d.cts +6 -0
  13. package/dist/react/useComputed.d.mts +6 -0
  14. package/dist/react/useComputed.mjs +14 -0
  15. package/dist/react/useSignal.cjs +14 -0
  16. package/dist/react/useSignal.d.cts +6 -0
  17. package/dist/react/useSignal.d.mts +7 -0
  18. package/dist/react/useSignal.mjs +14 -0
  19. package/dist/signals/Base.cjs +43 -0
  20. package/dist/signals/Base.d.cts +23 -0
  21. package/dist/signals/Base.d.mts +23 -0
  22. package/dist/signals/Base.mjs +43 -0
  23. package/dist/signals/Computed.cjs +49 -0
  24. package/dist/signals/Computed.d.cts +18 -0
  25. package/dist/signals/Computed.d.mts +18 -0
  26. package/dist/signals/Computed.mjs +49 -0
  27. package/dist/signals/Graph.cjs +28 -0
  28. package/dist/signals/Graph.d.cts +17 -0
  29. package/dist/signals/Graph.d.mts +17 -0
  30. package/dist/signals/Graph.mjs +27 -0
  31. package/dist/signals/Signal.cjs +14 -0
  32. package/dist/signals/Signal.d.cts +9 -0
  33. package/dist/signals/Signal.d.mts +9 -0
  34. package/dist/signals/Signal.mjs +14 -0
  35. package/dist/signals/index.cjs +3 -0
  36. package/dist/signals/index.d.mts +3 -0
  37. package/dist/signals/index.mjs +5 -0
  38. package/dist/signals/types.d.cts +5 -0
  39. package/dist/signals/types.d.mts +5 -0
  40. package/package.json +51 -0
  41. package/src/index.ts +2 -0
  42. package/src/react/bindToReact.ts +15 -0
  43. package/src/react/index.ts +2 -0
  44. package/src/react/useComputed.ts +12 -0
  45. package/src/react/useSignal.ts +11 -0
  46. package/src/signals/Base.ts +50 -0
  47. package/src/signals/Computed.ts +59 -0
  48. package/src/signals/Graph.ts +26 -0
  49. package/src/signals/Signal.ts +11 -0
  50. package/src/signals/effect.ts +6 -0
  51. package/src/signals/index.ts +3 -0
  52. package/src/signals/scratch.ts +29 -0
  53. package/src/signals/types.ts +3 -0
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Signals
2
+
3
+ A light-weight signals implementation that can be bound to any UI framework or library.
4
+
5
+ The implementation borrows some of the API from [Angular Signals](https://angular.dev/guide/signals) and the [TC39 Proposal](https://github.com/tc39/proposal-signals) while aiming to yield smaller bundle sizes and better portability between UI frameworks.
6
+
7
+ To accomplish the portability aspect, changed values are piped through an event emitter that can be consumed anywhere in your code without needing `effects`
8
+
9
+ ## API
10
+
11
+ ### Signals
12
+
13
+ Signals are primitive reactive values that can be used to store and derive application state from
14
+
15
+ ```typescript
16
+ import { Signal } from "@figliolia/signals";
17
+
18
+ const signal = new Signal(1);
19
+
20
+ // get the current value
21
+ signal.get();
22
+
23
+ // set new values
24
+ signal.set(2);
25
+
26
+ // transform values
27
+ signal.update(previous => previous + 1);
28
+
29
+ // subscribe to changes
30
+ const listener = signal.listen(currentValue => {
31
+ // on value change
32
+ });
33
+
34
+ // unsubscribe from changes
35
+ listener();
36
+ ```
37
+
38
+ ### Computed
39
+
40
+ Computed signals are readonly signals that derive their value based on a computation of other signals
41
+
42
+ ```typescript
43
+ import { Computed, Signal } from "@figliolia/signals";
44
+
45
+ const signal1 = new Signal(1);
46
+ const signal2 = new Signal(1);
47
+
48
+ const computed = new Computed(() => signal1.get() + signal2.get());
49
+
50
+ // subscribe to changes
51
+ const listener = computed.listen(currentValue => {
52
+ // on value change
53
+ });
54
+
55
+ // update the computed value
56
+ signal1.set(2); // computed === 3
57
+ signal2.set(2); // computed === 4
58
+
59
+ // unsubscribe from changes
60
+ listener();
61
+ ```
62
+
63
+ ### Effect
64
+
65
+ Effects are callback functions that can be executed anytime a signal changes
66
+
67
+ ```typescript
68
+ import { effect, Signal } from "@figliolia/signals";
69
+
70
+ const signal1 = new Signal(1);
71
+ const signal2 = new Signal(1);
72
+
73
+ // Your effect callback will run on initialization and anytime
74
+ // a signal inside changes values
75
+ effect(() => {
76
+ console.log(signal1.get(), signal2.get());
77
+ });
78
+ ```
79
+
80
+ ### Binding to frameworks
81
+
82
+ In this repository you'll find a basic example of how to bind your Signals to a UI framework such as react.
83
+
84
+ In essence, what you'll want to do is create subscriptions on a signal you wish your framework to be aware of - and derive some unit of state native to the framework anytime that value changes.
85
+
86
+ In react, this can be done with [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
87
+
88
+ ```typescript
89
+ import { Signal } from "@figliolia/signals";
90
+ import { useRef, useCallback, useSyncExternalStore } from "react";
91
+
92
+ export const useSignal = <T>(initialValue: T) => {
93
+ const signal = useRef(new Signal(initialValue));
94
+ const getValue = useCallback(() => signal.current.get(), []);
95
+ const subscribe = useCallback((fn: () => void) => {
96
+ return signal.current.listen(fn);
97
+ }, []);
98
+ // make react aware of the signal's value
99
+ const value = useSyncExternalStore(subscribe, getValue);
100
+ // return the classic react state tuple
101
+ return [value, signal.current] as const;
102
+ };
103
+ ```
104
+
105
+ ## The TC39 Proposal
106
+
107
+ This implemenation differs from the TC39 proposal and is likely not going to mirror the native implemenation if the proposal passes.
108
+
109
+ When designing this API, I read the proposal and aimed to build the necessities of the proposal while omitting some of the internals that developers are likely to be using less often
package/dist/index.cjs ADDED
@@ -0,0 +1,14 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_Base = require('./signals/Base.cjs');
3
+ const require_Computed = require('./signals/Computed.cjs');
4
+ const require_Signal = require('./signals/Signal.cjs');
5
+ require('./signals/index.cjs');
6
+ const require_useSignal = require('./react/useSignal.cjs');
7
+ const require_useComputed = require('./react/useComputed.cjs');
8
+ require('./react/index.cjs');
9
+
10
+ exports.Base = require_Base.Base;
11
+ exports.Computed = require_Computed.Computed;
12
+ exports.Signal = require_Signal.Signal;
13
+ exports.useComputed = require_useComputed.useComputed;
14
+ exports.useSignal = require_useSignal.useSignal;
@@ -0,0 +1,6 @@
1
+ import { Base } from "./signals/Base.cjs";
2
+ import { Computed } from "./signals/Computed.cjs";
3
+ import { Signal } from "./signals/Signal.cjs";
4
+ import { useSignal } from "./react/useSignal.cjs";
5
+ import { useComputed } from "./react/useComputed.cjs";
6
+ export { Base, Computed, Signal, useComputed, useSignal };
@@ -0,0 +1,8 @@
1
+ import { Base } from "./signals/Base.mjs";
2
+ import { Computed } from "./signals/Computed.mjs";
3
+ import { Signal } from "./signals/Signal.mjs";
4
+ import "./signals/index.mjs";
5
+ import { useSignal } from "./react/useSignal.mjs";
6
+ import { useComputed } from "./react/useComputed.mjs";
7
+ import "./react/index.mjs";
8
+ export { Base, Computed, Signal, useComputed, useSignal };
package/dist/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import { Base } from "./signals/Base.mjs";
2
+ import { Computed } from "./signals/Computed.mjs";
3
+ import { Signal } from "./signals/Signal.mjs";
4
+ import "./signals/index.mjs";
5
+ import { useSignal } from "./react/useSignal.mjs";
6
+ import { useComputed } from "./react/useComputed.mjs";
7
+ import "./react/index.mjs";
8
+
9
+ export { Base, Computed, Signal, useComputed, useSignal };
@@ -0,0 +1,10 @@
1
+ let react = require("react");
2
+
3
+ //#region src/react/bindToReact.ts
4
+ const bindToReact = (signal) => {
5
+ const getValue = (0, react.useCallback)(() => signal.get(), [signal]);
6
+ return [(0, react.useSyncExternalStore)((0, react.useCallback)((fn) => signal.listen(fn), [signal]), getValue), signal];
7
+ };
8
+
9
+ //#endregion
10
+ exports.bindToReact = bindToReact;
@@ -0,0 +1,10 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+
3
+ //#region src/react/bindToReact.ts
4
+ const bindToReact = (signal) => {
5
+ const getValue = useCallback(() => signal.get(), [signal]);
6
+ return [useSyncExternalStore(useCallback((fn) => signal.listen(fn), [signal]), getValue), signal];
7
+ };
8
+
9
+ //#endregion
10
+ export { bindToReact };
@@ -0,0 +1,2 @@
1
+ const require_useSignal = require('./useSignal.cjs');
2
+ const require_useComputed = require('./useComputed.cjs');
@@ -0,0 +1,2 @@
1
+ import { useSignal } from "./useSignal.mjs";
2
+ import { useComputed } from "./useComputed.mjs";
@@ -0,0 +1,4 @@
1
+ import { useSignal } from "./useSignal.mjs";
2
+ import { useComputed } from "./useComputed.mjs";
3
+
4
+ export { };
@@ -0,0 +1,14 @@
1
+ const require_Computed = require('../signals/Computed.cjs');
2
+ require('../signals/index.cjs');
3
+ const require_bindToReact = require('./bindToReact.cjs');
4
+ let react = require("react");
5
+ let _figliolia_react_hooks = require("@figliolia/react-hooks");
6
+
7
+ //#region src/react/useComputed.ts
8
+ const useComputed = (input) => {
9
+ const signal = (0, _figliolia_react_hooks.useController)(new require_Computed.Computed(input));
10
+ return (0, react.useMemo)(() => require_bindToReact.bindToReact(signal)[0], [signal]);
11
+ };
12
+
13
+ //#endregion
14
+ exports.useComputed = useComputed;
@@ -0,0 +1,6 @@
1
+ import { Computer } from "../signals/types.cjs";
2
+
3
+ //#region src/react/useComputed.d.ts
4
+ declare const useComputed: <T>(input: Computer<T>) => T;
5
+ //#endregion
6
+ export { useComputed };
@@ -0,0 +1,6 @@
1
+ import { Computer } from "../signals/types.mjs";
2
+
3
+ //#region src/react/useComputed.d.ts
4
+ declare const useComputed: <T>(input: Computer<T>) => T;
5
+ //#endregion
6
+ export { useComputed };
@@ -0,0 +1,14 @@
1
+ import { Computed } from "../signals/Computed.mjs";
2
+ import "../signals/index.mjs";
3
+ import { bindToReact } from "./bindToReact.mjs";
4
+ import { useMemo } from "react";
5
+ import { useController } from "@figliolia/react-hooks";
6
+
7
+ //#region src/react/useComputed.ts
8
+ const useComputed = (input) => {
9
+ const signal = useController(new Computed(input));
10
+ return useMemo(() => bindToReact(signal)[0], [signal]);
11
+ };
12
+
13
+ //#endregion
14
+ export { useComputed };
@@ -0,0 +1,14 @@
1
+ const require_Signal = require('../signals/Signal.cjs');
2
+ require('../signals/index.cjs');
3
+ const require_bindToReact = require('./bindToReact.cjs');
4
+ let react = require("react");
5
+ let _figliolia_react_hooks = require("@figliolia/react-hooks");
6
+
7
+ //#region src/react/useSignal.ts
8
+ const useSignal = (input) => {
9
+ const signal = (0, _figliolia_react_hooks.useController)(new require_Signal.Signal(input));
10
+ return (0, react.useMemo)(() => require_bindToReact.bindToReact(signal), [signal]);
11
+ };
12
+
13
+ //#endregion
14
+ exports.useSignal = useSignal;
@@ -0,0 +1,6 @@
1
+ import { Signal } from "../signals/Signal.cjs";
2
+
3
+ //#region src/react/useSignal.d.ts
4
+ declare const useSignal: <T>(input: T) => [T, Signal<T>];
5
+ //#endregion
6
+ export { useSignal };
@@ -0,0 +1,7 @@
1
+ import { Signal } from "../signals/Signal.mjs";
2
+ import "../signals/index.mjs";
3
+
4
+ //#region src/react/useSignal.d.ts
5
+ declare const useSignal: <T>(input: T) => [T, Signal<T>];
6
+ //#endregion
7
+ export { useSignal };
@@ -0,0 +1,14 @@
1
+ import { Signal } from "../signals/Signal.mjs";
2
+ import "../signals/index.mjs";
3
+ import { bindToReact } from "./bindToReact.mjs";
4
+ import { useMemo } from "react";
5
+ import { useController } from "@figliolia/react-hooks";
6
+
7
+ //#region src/react/useSignal.ts
8
+ const useSignal = (input) => {
9
+ const signal = useController(new Signal(input));
10
+ return useMemo(() => bindToReact(signal), [signal]);
11
+ };
12
+
13
+ //#endregion
14
+ export { useSignal };
@@ -0,0 +1,43 @@
1
+ const require_Graph = require('./Graph.cjs');
2
+ let _figliolia_event_emitter = require("@figliolia/event-emitter");
3
+
4
+ //#region src/signals/Base.ts
5
+ var Base = class Base {
6
+ ID;
7
+ static Graph = new require_Graph.Graph(void 0);
8
+ static IDs = new _figliolia_event_emitter.AutoIncrementingID();
9
+ Emitter = new _figliolia_event_emitter.EventEmitter();
10
+ static ACTIVE_CONSUMER = null;
11
+ static RESOLVED_DEPENDENCIES = /* @__PURE__ */ new Set();
12
+ constructor(value) {
13
+ this.value = value;
14
+ this.ID = Base.IDs.get();
15
+ Base.Graph.register(this);
16
+ }
17
+ get() {
18
+ if (Base.ACTIVE_CONSUMER !== null && !Base.RESOLVED_DEPENDENCIES.has(this.ID)) Base.RESOLVED_DEPENDENCIES.add(this.ID);
19
+ return this.value;
20
+ }
21
+ listen(notifier) {
22
+ const ID = this.Emitter.on("change", notifier);
23
+ return () => {
24
+ this.Emitter.off("change", ID);
25
+ };
26
+ }
27
+ valueOf() {
28
+ return this.value;
29
+ }
30
+ toJSON() {
31
+ return this.value;
32
+ }
33
+ withEmission(func) {
34
+ return (...args) => {
35
+ const result = func(...args);
36
+ this.Emitter.emit("change", this.value);
37
+ return result;
38
+ };
39
+ }
40
+ };
41
+
42
+ //#endregion
43
+ exports.Base = Base;
@@ -0,0 +1,23 @@
1
+ import { Graph } from "./Graph.cjs";
2
+ import { EventEmitter } from "@figliolia/event-emitter";
3
+
4
+ //#region src/signals/Base.d.ts
5
+ declare class Base<T> {
6
+ protected value: T;
7
+ ID: string;
8
+ static Graph: Graph;
9
+ private static IDs;
10
+ protected Emitter: EventEmitter<{
11
+ change: T;
12
+ }>;
13
+ protected static ACTIVE_CONSUMER: string | null;
14
+ protected static RESOLVED_DEPENDENCIES: Set<string>;
15
+ constructor(value: T);
16
+ get(): T;
17
+ listen(notifier: (value: T) => void): () => void;
18
+ valueOf(): T;
19
+ toJSON(): T;
20
+ protected withEmission<F extends (...args: any[]) => any>(func: F): (...args: Parameters<F>) => ReturnType<F>;
21
+ }
22
+ //#endregion
23
+ export { Base };
@@ -0,0 +1,23 @@
1
+ import { Graph } from "./Graph.mjs";
2
+ import { EventEmitter } from "@figliolia/event-emitter";
3
+
4
+ //#region src/signals/Base.d.ts
5
+ declare class Base<T> {
6
+ protected value: T;
7
+ ID: string;
8
+ static Graph: Graph;
9
+ private static IDs;
10
+ protected Emitter: EventEmitter<{
11
+ change: T;
12
+ }>;
13
+ protected static ACTIVE_CONSUMER: string | null;
14
+ protected static RESOLVED_DEPENDENCIES: Set<string>;
15
+ constructor(value: T);
16
+ get(): T;
17
+ listen(notifier: (value: T) => void): () => void;
18
+ valueOf(): T;
19
+ toJSON(): T;
20
+ protected withEmission<F extends (...args: any[]) => any>(func: F): (...args: Parameters<F>) => ReturnType<F>;
21
+ }
22
+ //#endregion
23
+ export { Base };
@@ -0,0 +1,43 @@
1
+ import { Graph } from "./Graph.mjs";
2
+ import { AutoIncrementingID, EventEmitter } from "@figliolia/event-emitter";
3
+
4
+ //#region src/signals/Base.ts
5
+ var Base = class Base {
6
+ ID;
7
+ static Graph = new Graph(void 0);
8
+ static IDs = new AutoIncrementingID();
9
+ Emitter = new EventEmitter();
10
+ static ACTIVE_CONSUMER = null;
11
+ static RESOLVED_DEPENDENCIES = /* @__PURE__ */ new Set();
12
+ constructor(value) {
13
+ this.value = value;
14
+ this.ID = Base.IDs.get();
15
+ Base.Graph.register(this);
16
+ }
17
+ get() {
18
+ if (Base.ACTIVE_CONSUMER !== null && !Base.RESOLVED_DEPENDENCIES.has(this.ID)) Base.RESOLVED_DEPENDENCIES.add(this.ID);
19
+ return this.value;
20
+ }
21
+ listen(notifier) {
22
+ const ID = this.Emitter.on("change", notifier);
23
+ return () => {
24
+ this.Emitter.off("change", ID);
25
+ };
26
+ }
27
+ valueOf() {
28
+ return this.value;
29
+ }
30
+ toJSON() {
31
+ return this.value;
32
+ }
33
+ withEmission(func) {
34
+ return (...args) => {
35
+ const result = func(...args);
36
+ this.Emitter.emit("change", this.value);
37
+ return result;
38
+ };
39
+ }
40
+ };
41
+
42
+ //#endregion
43
+ export { Base };
@@ -0,0 +1,49 @@
1
+ const require_Base = require('./Base.cjs');
2
+ let _figliolia_event_emitter = require("@figliolia/event-emitter");
3
+
4
+ //#region src/signals/Computed.ts
5
+ var Computed = class Computed extends require_Base.Base {
6
+ trackingID;
7
+ dependencies = [];
8
+ listeners = [];
9
+ static trackingIDs = new _figliolia_event_emitter.AutoIncrementingID();
10
+ constructor(computer) {
11
+ const trackingID = Computed.trackingIDs.get();
12
+ const [value, dependencies] = Computed.runCompute(trackingID, computer);
13
+ super(value);
14
+ this.computer = computer;
15
+ this.trackingID = trackingID;
16
+ this.setupDependencyTracking(dependencies);
17
+ }
18
+ compute = this.withEmission(() => {
19
+ const [value, dependencies] = Computed.runCompute(this.trackingID, this.computer);
20
+ this.value = value;
21
+ this.setupDependencyTracking(dependencies);
22
+ return value;
23
+ });
24
+ buildDependencyGraph(newDependencies) {
25
+ const node = require_Base.Base.Graph.get(this.ID);
26
+ if (node) {
27
+ for (const signal of this.dependencies) node.remove(signal);
28
+ for (const signal of newDependencies) node.register(signal);
29
+ }
30
+ }
31
+ setupDependencyTracking(nodeIDs) {
32
+ while (this.listeners.length) this.listeners.pop()?.();
33
+ const dependencies = nodeIDs.map((ID) => require_Base.Base.Graph.get(ID).value);
34
+ this.buildDependencyGraph(dependencies);
35
+ this.dependencies = dependencies;
36
+ this.listeners = dependencies.map((d) => d.listen(() => this.compute()));
37
+ }
38
+ static runCompute(ID, computer) {
39
+ require_Base.Base.ACTIVE_CONSUMER = ID;
40
+ const value = computer();
41
+ const dependencies = Array.from(require_Base.Base.RESOLVED_DEPENDENCIES);
42
+ require_Base.Base.ACTIVE_CONSUMER = null;
43
+ require_Base.Base.RESOLVED_DEPENDENCIES = /* @__PURE__ */ new Set();
44
+ return [value, dependencies];
45
+ }
46
+ };
47
+
48
+ //#endregion
49
+ exports.Computed = Computed;
@@ -0,0 +1,18 @@
1
+ import { Computer } from "./types.cjs";
2
+ import { Base } from "./Base.cjs";
3
+
4
+ //#region src/signals/Computed.d.ts
5
+ declare class Computed<T> extends Base<T> {
6
+ private readonly computer;
7
+ private trackingID;
8
+ private dependencies;
9
+ private listeners;
10
+ private static trackingIDs;
11
+ constructor(computer: Computer<T>);
12
+ private readonly compute;
13
+ private buildDependencyGraph;
14
+ private setupDependencyTracking;
15
+ private static runCompute;
16
+ }
17
+ //#endregion
18
+ export { Computed };
@@ -0,0 +1,18 @@
1
+ import { Computer } from "./types.mjs";
2
+ import { Base } from "./Base.mjs";
3
+
4
+ //#region src/signals/Computed.d.ts
5
+ declare class Computed<T> extends Base<T> {
6
+ private readonly computer;
7
+ private trackingID;
8
+ private dependencies;
9
+ private listeners;
10
+ private static trackingIDs;
11
+ constructor(computer: Computer<T>);
12
+ private readonly compute;
13
+ private buildDependencyGraph;
14
+ private setupDependencyTracking;
15
+ private static runCompute;
16
+ }
17
+ //#endregion
18
+ export { Computed };
@@ -0,0 +1,49 @@
1
+ import { Base } from "./Base.mjs";
2
+ import { AutoIncrementingID } from "@figliolia/event-emitter";
3
+
4
+ //#region src/signals/Computed.ts
5
+ var Computed = class Computed extends Base {
6
+ trackingID;
7
+ dependencies = [];
8
+ listeners = [];
9
+ static trackingIDs = new AutoIncrementingID();
10
+ constructor(computer) {
11
+ const trackingID = Computed.trackingIDs.get();
12
+ const [value, dependencies] = Computed.runCompute(trackingID, computer);
13
+ super(value);
14
+ this.computer = computer;
15
+ this.trackingID = trackingID;
16
+ this.setupDependencyTracking(dependencies);
17
+ }
18
+ compute = this.withEmission(() => {
19
+ const [value, dependencies] = Computed.runCompute(this.trackingID, this.computer);
20
+ this.value = value;
21
+ this.setupDependencyTracking(dependencies);
22
+ return value;
23
+ });
24
+ buildDependencyGraph(newDependencies) {
25
+ const node = Base.Graph.get(this.ID);
26
+ if (node) {
27
+ for (const signal of this.dependencies) node.remove(signal);
28
+ for (const signal of newDependencies) node.register(signal);
29
+ }
30
+ }
31
+ setupDependencyTracking(nodeIDs) {
32
+ while (this.listeners.length) this.listeners.pop()?.();
33
+ const dependencies = nodeIDs.map((ID) => Base.Graph.get(ID).value);
34
+ this.buildDependencyGraph(dependencies);
35
+ this.dependencies = dependencies;
36
+ this.listeners = dependencies.map((d) => d.listen(() => this.compute()));
37
+ }
38
+ static runCompute(ID, computer) {
39
+ Base.ACTIVE_CONSUMER = ID;
40
+ const value = computer();
41
+ const dependencies = Array.from(Base.RESOLVED_DEPENDENCIES);
42
+ Base.ACTIVE_CONSUMER = null;
43
+ Base.RESOLVED_DEPENDENCIES = /* @__PURE__ */ new Set();
44
+ return [value, dependencies];
45
+ }
46
+ };
47
+
48
+ //#endregion
49
+ export { Computed };
@@ -0,0 +1,28 @@
1
+
2
+ //#region src/signals/Graph.ts
3
+ var Graph = class Graph {
4
+ nodes = /* @__PURE__ */ new Map();
5
+ constructor(value) {
6
+ this.value = value;
7
+ }
8
+ register(node) {
9
+ this.nodes.set(node.ID, new Graph(node));
10
+ }
11
+ remove(node) {
12
+ this.nodes.delete(node.ID);
13
+ }
14
+ get(ID) {
15
+ return this.nodes.get(ID);
16
+ }
17
+ toJSON() {
18
+ const nodes = {};
19
+ for (const [id, node] of this.nodes) nodes[id] = node;
20
+ return {
21
+ nodes,
22
+ value: this.value
23
+ };
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ exports.Graph = Graph;
@@ -0,0 +1,17 @@
1
+ import { Base } from "./Base.cjs";
2
+
3
+ //#region src/signals/Graph.d.ts
4
+ declare class Graph {
5
+ value: Base<any>;
6
+ private nodes;
7
+ constructor(value: Base<any>);
8
+ register(node: Base<any>): void;
9
+ remove(node: Base<any>): void;
10
+ get(ID: string): Graph | undefined;
11
+ toJSON(): {
12
+ nodes: Record<string, Graph>;
13
+ value: Base<any>;
14
+ };
15
+ }
16
+ //#endregion
17
+ export { Graph };
@@ -0,0 +1,17 @@
1
+ import { Base } from "./Base.mjs";
2
+
3
+ //#region src/signals/Graph.d.ts
4
+ declare class Graph {
5
+ value: Base<any>;
6
+ private nodes;
7
+ constructor(value: Base<any>);
8
+ register(node: Base<any>): void;
9
+ remove(node: Base<any>): void;
10
+ get(ID: string): Graph | undefined;
11
+ toJSON(): {
12
+ nodes: Record<string, Graph>;
13
+ value: Base<any>;
14
+ };
15
+ }
16
+ //#endregion
17
+ export { Graph };
@@ -0,0 +1,27 @@
1
+ //#region src/signals/Graph.ts
2
+ var Graph = class Graph {
3
+ nodes = /* @__PURE__ */ new Map();
4
+ constructor(value) {
5
+ this.value = value;
6
+ }
7
+ register(node) {
8
+ this.nodes.set(node.ID, new Graph(node));
9
+ }
10
+ remove(node) {
11
+ this.nodes.delete(node.ID);
12
+ }
13
+ get(ID) {
14
+ return this.nodes.get(ID);
15
+ }
16
+ toJSON() {
17
+ const nodes = {};
18
+ for (const [id, node] of this.nodes) nodes[id] = node;
19
+ return {
20
+ nodes,
21
+ value: this.value
22
+ };
23
+ }
24
+ };
25
+
26
+ //#endregion
27
+ export { Graph };
@@ -0,0 +1,14 @@
1
+ const require_Base = require('./Base.cjs');
2
+
3
+ //#region src/signals/Signal.ts
4
+ var Signal = class extends require_Base.Base {
5
+ set = this.withEmission((value) => {
6
+ this.value = value;
7
+ });
8
+ update = this.withEmission((updater) => {
9
+ this.value = updater(this.value);
10
+ });
11
+ };
12
+
13
+ //#endregion
14
+ exports.Signal = Signal;
@@ -0,0 +1,9 @@
1
+ import { Base } from "./Base.cjs";
2
+
3
+ //#region src/signals/Signal.d.ts
4
+ declare class Signal<T> extends Base<T> {
5
+ readonly set: (value: T) => void;
6
+ readonly update: (updater: (value: T) => T) => void;
7
+ }
8
+ //#endregion
9
+ export { Signal };
@@ -0,0 +1,9 @@
1
+ import { Base } from "./Base.mjs";
2
+
3
+ //#region src/signals/Signal.d.ts
4
+ declare class Signal<T> extends Base<T> {
5
+ readonly set: (value: T) => void;
6
+ readonly update: (updater: (value: T) => T) => void;
7
+ }
8
+ //#endregion
9
+ export { Signal };
@@ -0,0 +1,14 @@
1
+ import { Base } from "./Base.mjs";
2
+
3
+ //#region src/signals/Signal.ts
4
+ var Signal = class extends Base {
5
+ set = this.withEmission((value) => {
6
+ this.value = value;
7
+ });
8
+ update = this.withEmission((updater) => {
9
+ this.value = updater(this.value);
10
+ });
11
+ };
12
+
13
+ //#endregion
14
+ export { Signal };
@@ -0,0 +1,3 @@
1
+ const require_Base = require('./Base.cjs');
2
+ const require_Computed = require('./Computed.cjs');
3
+ const require_Signal = require('./Signal.cjs');
@@ -0,0 +1,3 @@
1
+ import { Base } from "./Base.mjs";
2
+ import { Computed } from "./Computed.mjs";
3
+ import { Signal } from "./Signal.mjs";
@@ -0,0 +1,5 @@
1
+ import { Base } from "./Base.mjs";
2
+ import { Computed } from "./Computed.mjs";
3
+ import { Signal } from "./Signal.mjs";
4
+
5
+ export { };
@@ -0,0 +1,5 @@
1
+ //#region src/signals/types.d.ts
2
+ type Computer<T> = () => NotPromise<T>;
3
+ type NotPromise<T> = T extends Promise<any> ? never : T;
4
+ //#endregion
5
+ export { Computer };
@@ -0,0 +1,5 @@
1
+ //#region src/signals/types.d.ts
2
+ type Computer<T> = () => NotPromise<T>;
3
+ type NotPromise<T> = T extends Promise<any> ? never : T;
4
+ //#endregion
5
+ export { Computer };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@figliolia/signals",
3
+ "version": "1.0.0",
4
+ "description": "A signals implemenation loosely based on the TC39 Proposal",
5
+ "keywords": [
6
+ "javascript",
7
+ "signals",
8
+ "typescript"
9
+ ],
10
+ "license": "ISC",
11
+ "author": "Alex Figliolia",
12
+ "files": [
13
+ "dist",
14
+ "src"
15
+ ],
16
+ "main": "dist/index.cjs",
17
+ "module": "dist/index.mjs",
18
+ "types": "dist/index.d.mts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.mts",
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "tsdown",
31
+ "lint": "oxlint --type-aware --type-check --report-unused-disable-directives --fix && oxfmt"
32
+ },
33
+ "devDependencies": {
34
+ "@figliolia/event-emitter": ">=1.1.0",
35
+ "@figliolia/react-hooks": ">=1.7.0",
36
+ "@types/node": "^25.2.1",
37
+ "@types/react": "^19.2.13",
38
+ "oxfmt": "^0.28.0",
39
+ "oxlint": "^1.36.0",
40
+ "oxlint-tsgolint": "^0.11.4",
41
+ "react": ">=18.0.0",
42
+ "tsdown": "^0.20.3",
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "peerDependencies": {
47
+ "@figliolia/event-emitter": ">=1.1.0",
48
+ "@figliolia/react-hooks": ">=1.7.0",
49
+ "react": ">=18.0.0"
50
+ }
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./react";
2
+ export * from "./signals";
@@ -0,0 +1,15 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+
3
+ import { type Base } from "../signals";
4
+
5
+ export const bindToReact = <T extends Base<any>>(
6
+ signal: T,
7
+ ): [ReturnType<T["get"]>, T] => {
8
+ const getValue = useCallback(() => signal.get(), [signal]);
9
+ const subscribe = useCallback(
10
+ (fn: () => void) => signal.listen(fn),
11
+ [signal],
12
+ );
13
+ const value = useSyncExternalStore(subscribe, getValue);
14
+ return [value, signal] as const;
15
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./useSignal";
2
+ export * from "./useComputed";
@@ -0,0 +1,12 @@
1
+ import { useMemo } from "react";
2
+ import { useController } from "@figliolia/react-hooks";
3
+
4
+ import type { Computer } from "../signals/types";
5
+ import { Computed } from "../signals";
6
+
7
+ import { bindToReact } from "./bindToReact";
8
+
9
+ export const useComputed = <T>(input: Computer<T>) => {
10
+ const signal = useController(new Computed(input));
11
+ return useMemo(() => bindToReact(signal)[0], [signal]);
12
+ };
@@ -0,0 +1,11 @@
1
+ import { useMemo } from "react";
2
+ import { useController } from "@figliolia/react-hooks";
3
+
4
+ import { Signal } from "../signals";
5
+
6
+ import { bindToReact } from "./bindToReact";
7
+
8
+ export const useSignal = <T>(input: T) => {
9
+ const signal = useController(new Signal(input));
10
+ return useMemo(() => bindToReact(signal), [signal]);
11
+ };
@@ -0,0 +1,50 @@
1
+ import { AutoIncrementingID, EventEmitter } from "@figliolia/event-emitter";
2
+
3
+ import { Graph } from "./Graph";
4
+
5
+ export class Base<T> {
6
+ public ID: string;
7
+ // @ts-ignore
8
+ public static Graph = new Graph(undefined);
9
+ private static IDs = new AutoIncrementingID();
10
+ protected Emitter = new EventEmitter<{ change: T }>();
11
+ protected static ACTIVE_CONSUMER: string | null = null;
12
+ protected static RESOLVED_DEPENDENCIES = new Set<string>();
13
+ constructor(protected value: T) {
14
+ this.ID = Base.IDs.get();
15
+ Base.Graph.register(this);
16
+ }
17
+
18
+ public get() {
19
+ if (
20
+ Base.ACTIVE_CONSUMER !== null &&
21
+ !Base.RESOLVED_DEPENDENCIES.has(this.ID)
22
+ ) {
23
+ Base.RESOLVED_DEPENDENCIES.add(this.ID);
24
+ }
25
+ return this.value;
26
+ }
27
+
28
+ public listen(notifier: (value: T) => void) {
29
+ const ID = this.Emitter.on("change", notifier);
30
+ return () => {
31
+ this.Emitter.off("change", ID);
32
+ };
33
+ }
34
+
35
+ public valueOf() {
36
+ return this.value;
37
+ }
38
+
39
+ public toJSON() {
40
+ return this.value;
41
+ }
42
+
43
+ protected withEmission<F extends (...args: any[]) => any>(func: F) {
44
+ return (...args: Parameters<F>): ReturnType<F> => {
45
+ const result = func(...args);
46
+ this.Emitter.emit("change", this.value);
47
+ return result;
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,59 @@
1
+ import { AutoIncrementingID } from "@figliolia/event-emitter";
2
+
3
+ import type { Computer } from "./types";
4
+ import { Base } from "./Base";
5
+
6
+ export class Computed<T> extends Base<T> {
7
+ private trackingID: string;
8
+ private dependencies: Base<any>[] = [];
9
+ private listeners: (() => void)[] = [];
10
+ private static trackingIDs = new AutoIncrementingID();
11
+ constructor(private readonly computer: Computer<T>) {
12
+ const trackingID = Computed.trackingIDs.get();
13
+ const [value, dependencies] = Computed.runCompute(trackingID, computer);
14
+ super(value);
15
+ this.trackingID = trackingID;
16
+ this.setupDependencyTracking(dependencies);
17
+ }
18
+
19
+ private readonly compute = this.withEmission(() => {
20
+ const [value, dependencies] = Computed.runCompute(
21
+ this.trackingID,
22
+ this.computer,
23
+ );
24
+ this.value = value;
25
+ this.setupDependencyTracking(dependencies);
26
+ return value;
27
+ });
28
+
29
+ private buildDependencyGraph(newDependencies: Base<any>[]) {
30
+ const node = Base.Graph.get(this.ID);
31
+ if (node) {
32
+ for (const signal of this.dependencies) {
33
+ node.remove(signal);
34
+ }
35
+ for (const signal of newDependencies) {
36
+ node.register(signal);
37
+ }
38
+ }
39
+ }
40
+
41
+ private setupDependencyTracking(nodeIDs: string[]) {
42
+ while (this.listeners.length) {
43
+ this.listeners.pop()?.();
44
+ }
45
+ const dependencies = nodeIDs.map(ID => Base.Graph.get(ID)!.value);
46
+ this.buildDependencyGraph(dependencies);
47
+ this.dependencies = dependencies;
48
+ this.listeners = dependencies.map(d => d.listen(() => this.compute()));
49
+ }
50
+
51
+ private static runCompute<T>(ID: string, computer: () => T) {
52
+ Base.ACTIVE_CONSUMER = ID;
53
+ const value = computer();
54
+ const dependencies = Array.from(Base.RESOLVED_DEPENDENCIES);
55
+ Base.ACTIVE_CONSUMER = null;
56
+ Base.RESOLVED_DEPENDENCIES = new Set();
57
+ return [value, dependencies] as const;
58
+ }
59
+ }
@@ -0,0 +1,26 @@
1
+ import type { Base } from "./Base";
2
+
3
+ export class Graph {
4
+ private nodes = new Map<string, Graph>();
5
+ constructor(public value: Base<any>) {}
6
+
7
+ public register(node: Base<any>) {
8
+ this.nodes.set(node.ID, new Graph(node));
9
+ }
10
+
11
+ public remove(node: Base<any>) {
12
+ this.nodes.delete(node.ID);
13
+ }
14
+
15
+ public get(ID: string) {
16
+ return this.nodes.get(ID);
17
+ }
18
+
19
+ public toJSON() {
20
+ const nodes: Record<string, Graph> = {};
21
+ for (const [id, node] of this.nodes) {
22
+ nodes[id] = node;
23
+ }
24
+ return { nodes, value: this.value };
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ import { Base } from "./Base";
2
+
3
+ export class Signal<T> extends Base<T> {
4
+ public readonly set = this.withEmission((value: T) => {
5
+ this.value = value;
6
+ });
7
+
8
+ public readonly update = this.withEmission((updater: (value: T) => T) => {
9
+ this.value = updater(this.value);
10
+ });
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { Computer } from "./types";
2
+ import { Computed } from "./Computed";
3
+
4
+ export const effect = (computer: Computer<void>) => {
5
+ new Computed(computer);
6
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./Computed";
2
+ export * from "./Signal";
3
+ export * from "./Base";
@@ -0,0 +1,29 @@
1
+ import { Signal } from "./Signal";
2
+ import { effect } from "./effect";
3
+ import { Computed } from "./Computed";
4
+ import { Base } from "./Base";
5
+
6
+ const value1 = new Signal(1);
7
+ const value2 = new Signal(1);
8
+ const value3 = new Signal(1);
9
+ const value4 = new Signal(1);
10
+
11
+ const computed = new Computed(() => {
12
+ if (value1.get() < 2) {
13
+ return value1.get();
14
+ }
15
+ return value1.get() + value2.get() + value3.get() + value4.get();
16
+ });
17
+
18
+ effect(() => {
19
+ console.log(computed.get());
20
+ });
21
+
22
+ computed.listen(v => console.log(v));
23
+
24
+ value1.set(2);
25
+ value2.update(v => v + 1);
26
+ value3.set(2);
27
+ value4.update(v => v + 1);
28
+
29
+ console.log(JSON.stringify(Base.Graph, null, 2));
@@ -0,0 +1,3 @@
1
+ export type Computer<T> = () => NotPromise<T>;
2
+
3
+ export type NotPromise<T> = T extends Promise<any> ? never : T;