@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.
- package/README.md +109 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +9 -0
- package/dist/react/bindToReact.cjs +10 -0
- package/dist/react/bindToReact.mjs +10 -0
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.d.mts +2 -0
- package/dist/react/index.mjs +4 -0
- package/dist/react/useComputed.cjs +14 -0
- package/dist/react/useComputed.d.cts +6 -0
- package/dist/react/useComputed.d.mts +6 -0
- package/dist/react/useComputed.mjs +14 -0
- package/dist/react/useSignal.cjs +14 -0
- package/dist/react/useSignal.d.cts +6 -0
- package/dist/react/useSignal.d.mts +7 -0
- package/dist/react/useSignal.mjs +14 -0
- package/dist/signals/Base.cjs +43 -0
- package/dist/signals/Base.d.cts +23 -0
- package/dist/signals/Base.d.mts +23 -0
- package/dist/signals/Base.mjs +43 -0
- package/dist/signals/Computed.cjs +49 -0
- package/dist/signals/Computed.d.cts +18 -0
- package/dist/signals/Computed.d.mts +18 -0
- package/dist/signals/Computed.mjs +49 -0
- package/dist/signals/Graph.cjs +28 -0
- package/dist/signals/Graph.d.cts +17 -0
- package/dist/signals/Graph.d.mts +17 -0
- package/dist/signals/Graph.mjs +27 -0
- package/dist/signals/Signal.cjs +14 -0
- package/dist/signals/Signal.d.cts +9 -0
- package/dist/signals/Signal.d.mts +9 -0
- package/dist/signals/Signal.mjs +14 -0
- package/dist/signals/index.cjs +3 -0
- package/dist/signals/index.d.mts +3 -0
- package/dist/signals/index.mjs +5 -0
- package/dist/signals/types.d.cts +5 -0
- package/dist/signals/types.d.mts +5 -0
- package/package.json +51 -0
- package/src/index.ts +2 -0
- package/src/react/bindToReact.ts +15 -0
- package/src/react/index.ts +2 -0
- package/src/react/useComputed.ts +12 -0
- package/src/react/useSignal.ts +11 -0
- package/src/signals/Base.ts +50 -0
- package/src/signals/Computed.ts +59 -0
- package/src/signals/Graph.ts +26 -0
- package/src/signals/Signal.ts +11 -0
- package/src/signals/effect.ts +6 -0
- package/src/signals/index.ts +3 -0
- package/src/signals/scratch.ts +29 -0
- 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;
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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,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,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,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,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 };
|
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,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,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,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));
|