easy-signal 2.0.1 → 3.0.1
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/.prettierrc.cjs +8 -0
- package/.vscode/settings.json +18 -0
- package/README.md +99 -19
- package/eventSignal.d.ts +36 -0
- package/eventSignal.js +51 -0
- package/eventSignal.ts +74 -0
- package/index.d.ts +3 -36
- package/index.js +3 -51
- package/index.ts +3 -75
- package/package.json +1 -1
- package/reactiveSignal.d.ts +102 -0
- package/reactiveSignal.js +210 -0
- package/reactiveSignal.ts +311 -0
- package/signalStore.d.ts +11 -0
- package/signalStore.js +13 -0
- package/signalStore.ts +14 -0
package/.prettierrc.cjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"[typescript]": {
|
|
3
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
4
|
+
},
|
|
5
|
+
"[json]": {
|
|
6
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
7
|
+
},
|
|
8
|
+
"eslint.format.enable": true,
|
|
9
|
+
"editor.codeActionsOnSave": {
|
|
10
|
+
"source.organizeImports": "explicit"
|
|
11
|
+
},
|
|
12
|
+
"editor.formatOnSave": true,
|
|
13
|
+
"eslint.validate": [
|
|
14
|
+
"javascript",
|
|
15
|
+
"javascriptreact",
|
|
16
|
+
"svelte"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Easy Signal
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Two simple interfaces for creating two types of signals. The first (and original signal in this module) is a defined
|
|
4
|
+
event that can be triggered and listened to with a single function. The second is a defined data store that allows you
|
|
5
|
+
to react to changes to that data (popularized by solid-js). These two are `EventSignal` and `ReactiveSignal`.
|
|
6
6
|
|
|
7
|
-
Full type safety with TypeScript
|
|
7
|
+
Full type safety with TypeScript with both use-cases.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -12,24 +12,24 @@ Full type safety with TypeScript providing good autocomplete.
|
|
|
12
12
|
npm install easy-signal
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
## Usage
|
|
15
|
+
## EventSignal Usage
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
events as well as to trigger them.
|
|
17
|
+
An EventSignal is a function that represents a single event. The function can be used to subscribe to be notified of
|
|
18
|
+
the events as well as to trigger them.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
each event would use its own signal. This allows each signal to have a specific function signature as opposed to
|
|
22
|
-
browser's generic `event` object. This is a great system in TypeScript being able to see the exact data each event
|
|
20
|
+
EventSignals offer similar functionality as the browser's `eventDispatcher` API, but rather than a general API for any
|
|
21
|
+
event, each event would use its own signal. This allows each signal to have a specific function signature as opposed to
|
|
22
|
+
the browser's generic `event` object. This is a great system in TypeScript being able to see the exact data each event
|
|
23
23
|
produces.
|
|
24
24
|
|
|
25
|
-
### Basic Usage
|
|
25
|
+
### EventSignal Basic Usage
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
// file seconds.ts
|
|
29
|
-
import {
|
|
29
|
+
import { eventSignal } from 'easy-signal';
|
|
30
30
|
|
|
31
31
|
// Create the signal and export it for use. Optionally provide the subscriber signature
|
|
32
|
-
export const onSecond =
|
|
32
|
+
export const onSecond = eventSignal<number>();
|
|
33
33
|
|
|
34
34
|
// Passing a non-function value will dispatch the event
|
|
35
35
|
setInterval(() => {
|
|
@@ -51,9 +51,9 @@ Errors may also be listened to from the signal by passing `ForErrors` as the sec
|
|
|
51
51
|
and errors may be dispatched by passing an Error object to the signal method.
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
|
-
import {
|
|
54
|
+
import { eventSignal, ForErrors } from 'easy-signal';
|
|
55
55
|
|
|
56
|
-
const dataStream =
|
|
56
|
+
const dataStream = eventSignal();
|
|
57
57
|
|
|
58
58
|
dataStream(data => console.log('data is:' data));
|
|
59
59
|
dataStream(error => console.log('Error is:' error), ForErrors);
|
|
@@ -65,11 +65,10 @@ stream.on('error', err => dataStream(err));
|
|
|
65
65
|
To get a subscriber-only method for external use, pass in the `GetOnSignal` constant.
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
|
-
import {
|
|
69
|
-
|
|
68
|
+
import { eventSignal, GetOnSignal } from 'easy-signal';
|
|
70
69
|
|
|
71
70
|
function getMyAPI() {
|
|
72
|
-
const doSomething =
|
|
71
|
+
const doSomething = eventSignal();
|
|
73
72
|
|
|
74
73
|
// doSomething() will trigger subscribers that were added in onSomething(...). This protects the signal from being
|
|
75
74
|
// triggered/dispatched outside of `getMyAPI`. Sometimes you may want more control to prevent just anyone from
|
|
@@ -81,7 +80,6 @@ function getMyAPI() {
|
|
|
81
80
|
}
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
|
|
85
83
|
To clear the listeners from the signal, pass in the `ClearSignal` constant.
|
|
86
84
|
|
|
87
85
|
```ts
|
|
@@ -91,3 +89,85 @@ const onSomething = signal();
|
|
|
91
89
|
|
|
92
90
|
onSomething(ClearSignal); // clears signal
|
|
93
91
|
```
|
|
92
|
+
|
|
93
|
+
## ReactiveSignal Usage
|
|
94
|
+
|
|
95
|
+
A ReactiveSignal is a function that represents a single piece of data. The function can be used to get the data, set the
|
|
96
|
+
data, and update the data with an updater function. To subscribe to changes use the separate `subscribe` function. The
|
|
97
|
+
ReactiveSignal allows for Observers to be created, which are functions that will be rerun whenever any ReactiveSignals
|
|
98
|
+
they access are updated, and ComputedSignals which are read-only signals whose value is derived from other signals and
|
|
99
|
+
which will be updated whenever they are.
|
|
100
|
+
|
|
101
|
+
### ReactiveSignal Basic Usage
|
|
102
|
+
|
|
103
|
+
Here we will use an example similar to the EventSignal, but unlike the EventSignal, the current seconds since epoch will
|
|
104
|
+
be stored and can be accessed any time, whereas the EventSignal only fires an event with the data provided. This
|
|
105
|
+
particular example isn't very compelling.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// file seconds.ts
|
|
109
|
+
import { reactiveSignal } from 'easy-signal';
|
|
110
|
+
|
|
111
|
+
// Create the signal and export it for use. Optionally provide the subscriber signature
|
|
112
|
+
export const onSecond = reactiveSignal(0);
|
|
113
|
+
|
|
114
|
+
// Passing a non-function value will dispatch the event
|
|
115
|
+
setInterval(() => {
|
|
116
|
+
const currentSecond = Math.floor(Date.now() / 1000);
|
|
117
|
+
onSecond(currentSecond);
|
|
118
|
+
// or onSecond(currentValue => newValue) to update
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { onSecond, subscribe } from './seconds.ts';
|
|
124
|
+
|
|
125
|
+
// Get the value of onSecond() at any time
|
|
126
|
+
console.log(onSecond(), 'since epoc');
|
|
127
|
+
|
|
128
|
+
// Typescript knows that seconds is a number because of the concrete type definition in seconds.ts
|
|
129
|
+
const unsubscribe = subscribe(onSecond, seconds => {
|
|
130
|
+
console.log(seconds, 'since epoc');
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Observer Basic Usage
|
|
135
|
+
|
|
136
|
+
To take action whenever data changes, you can observe or more signals by accessing them in a function call. You can also
|
|
137
|
+
prevent that function from being called too often when data changes by using the Timings. Below, we update the content
|
|
138
|
+
of the DOM whenever the user or billing data is updated, but we only do it after an animation frame to prevent the DOM
|
|
139
|
+
updates from being too frequent. Providing no Timing will call the function immediately after any data is changed.
|
|
140
|
+
|
|
141
|
+
Because `user()` and `billing()` are called the first time the observe function is run, it automatically subscribes to
|
|
142
|
+
know when they are changed so that the function may be rerun.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { reactiveSignal, observe, Timing } from 'easy-signal';
|
|
146
|
+
|
|
147
|
+
const user = reactiveSignal(userData);
|
|
148
|
+
const billing = reactiveSignal(billingData);
|
|
149
|
+
|
|
150
|
+
const unobserve = observe(() => {
|
|
151
|
+
document.body.innerText = `${user().name} has the plan ${billing().plan}`;
|
|
152
|
+
}, Timing.AnimationFrame);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### ComputedSignal Basic Usage
|
|
156
|
+
|
|
157
|
+
Create read-only signals whose value is derived from other signals and which will be updated whenever they are.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { computedSignal, subscribe } from 'easy-signal';
|
|
161
|
+
import { user, billing } from 'my-other-signals';
|
|
162
|
+
|
|
163
|
+
const delinquent = computedSignal(() => {
|
|
164
|
+
if (user().subscribed) {
|
|
165
|
+
return billing().status === 'delinquent';
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
subscribe(delinquent, delinquent => {
|
|
171
|
+
console.log(`The user is${delinquent ? '' : ' not'} delinquent`);
|
|
172
|
+
});
|
|
173
|
+
```
|
package/eventSignal.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare type Args<T> = T extends (...args: infer A) => any ? A : never;
|
|
2
|
+
export declare type EventSignalSubscriber = (...args: any[]) => any;
|
|
3
|
+
export declare type ErrorSubscriber = (error: Error) => any;
|
|
4
|
+
export declare type Unsubscriber = () => void;
|
|
5
|
+
export declare type OnSignal<T extends EventSignalSubscriber = EventSignalSubscriber> = {
|
|
6
|
+
(subscriber: T): Unsubscriber;
|
|
7
|
+
(errorListener: ErrorSubscriber, what: typeof ForErrors): Unsubscriber;
|
|
8
|
+
};
|
|
9
|
+
export declare type EventSignal<T extends EventSignalSubscriber = EventSignalSubscriber> = OnSignal<T> & {
|
|
10
|
+
(...args: Args<T>): void;
|
|
11
|
+
(data: Error): void;
|
|
12
|
+
(data: typeof ClearSignal): void;
|
|
13
|
+
(data: typeof GetOnSignal): OnSignal<T>;
|
|
14
|
+
};
|
|
15
|
+
export declare const ClearSignal: unique symbol;
|
|
16
|
+
export declare const GetOnSignal: unique symbol;
|
|
17
|
+
export declare const ForErrors: unique symbol;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
20
|
+
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
21
|
+
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
22
|
+
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
23
|
+
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
24
|
+
* @example
|
|
25
|
+
* const onLoad = signal();
|
|
26
|
+
*
|
|
27
|
+
* // Subscribe to data
|
|
28
|
+
* onLoad((data) => console.log('loaded', data));
|
|
29
|
+
* onLoad((error) => console.error('error', error), true);
|
|
30
|
+
*
|
|
31
|
+
* // Dispatch data
|
|
32
|
+
* onLoad('data'); // logs 'loaded data'
|
|
33
|
+
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
34
|
+
*/
|
|
35
|
+
export declare function eventSignal<T extends EventSignalSubscriber = EventSignalSubscriber>(): EventSignal<T>;
|
|
36
|
+
export {};
|
package/eventSignal.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const ClearSignal = Symbol();
|
|
2
|
+
export const GetOnSignal = Symbol();
|
|
3
|
+
export const ForErrors = Symbol();
|
|
4
|
+
/**
|
|
5
|
+
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
6
|
+
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
7
|
+
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
8
|
+
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
9
|
+
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
10
|
+
* @example
|
|
11
|
+
* const onLoad = signal();
|
|
12
|
+
*
|
|
13
|
+
* // Subscribe to data
|
|
14
|
+
* onLoad((data) => console.log('loaded', data));
|
|
15
|
+
* onLoad((error) => console.error('error', error), true);
|
|
16
|
+
*
|
|
17
|
+
* // Dispatch data
|
|
18
|
+
* onLoad('data'); // logs 'loaded data'
|
|
19
|
+
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
20
|
+
*/
|
|
21
|
+
export function eventSignal() {
|
|
22
|
+
const subscribers = new Set();
|
|
23
|
+
const errorListeners = new Set();
|
|
24
|
+
function onSignal(subscriber, what) {
|
|
25
|
+
const listeners = what === ForErrors ? errorListeners : subscribers;
|
|
26
|
+
listeners.add(subscriber);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(subscriber);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function signal(...args) {
|
|
32
|
+
const arg = args[0];
|
|
33
|
+
if (typeof arg === 'function') {
|
|
34
|
+
return onSignal(arg);
|
|
35
|
+
}
|
|
36
|
+
else if (arg === ClearSignal) {
|
|
37
|
+
subscribers.clear();
|
|
38
|
+
errorListeners.clear();
|
|
39
|
+
}
|
|
40
|
+
else if (arg === GetOnSignal) {
|
|
41
|
+
return onSignal;
|
|
42
|
+
}
|
|
43
|
+
else if (arg instanceof Error) {
|
|
44
|
+
errorListeners.forEach(listener => listener(arg));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
subscribers.forEach(listener => listener(...args));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return signal;
|
|
51
|
+
}
|
package/eventSignal.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type Args<T> = T extends (...args: infer A) => any ? A : never;
|
|
2
|
+
export type EventSignalSubscriber = (...args: any[]) => any;
|
|
3
|
+
export type ErrorSubscriber = (error: Error) => any;
|
|
4
|
+
export type Unsubscriber = () => void;
|
|
5
|
+
|
|
6
|
+
export type OnSignal<T extends EventSignalSubscriber = EventSignalSubscriber> = {
|
|
7
|
+
(subscriber: T): Unsubscriber;
|
|
8
|
+
(errorListener: ErrorSubscriber, what: typeof ForErrors): Unsubscriber;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type EventSignal<T extends EventSignalSubscriber = EventSignalSubscriber> = OnSignal<T> & {
|
|
12
|
+
(...args: Args<T>): void;
|
|
13
|
+
(data: Error): void;
|
|
14
|
+
(data: typeof ClearSignal): void;
|
|
15
|
+
(data: typeof GetOnSignal): OnSignal<T>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const ClearSignal = Symbol();
|
|
19
|
+
export const GetOnSignal = Symbol();
|
|
20
|
+
export const ForErrors = Symbol();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
24
|
+
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
25
|
+
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
26
|
+
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
27
|
+
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
28
|
+
* @example
|
|
29
|
+
* const onLoad = signal();
|
|
30
|
+
*
|
|
31
|
+
* // Subscribe to data
|
|
32
|
+
* onLoad((data) => console.log('loaded', data));
|
|
33
|
+
* onLoad((error) => console.error('error', error), true);
|
|
34
|
+
*
|
|
35
|
+
* // Dispatch data
|
|
36
|
+
* onLoad('data'); // logs 'loaded data'
|
|
37
|
+
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
38
|
+
*/
|
|
39
|
+
export function eventSignal<T extends EventSignalSubscriber = EventSignalSubscriber>(): EventSignal<T> {
|
|
40
|
+
const subscribers = new Set<EventSignalSubscriber>();
|
|
41
|
+
const errorListeners = new Set<EventSignalSubscriber>();
|
|
42
|
+
|
|
43
|
+
function onSignal(subscriber: T | ErrorSubscriber, what?: typeof ForErrors): Unsubscriber {
|
|
44
|
+
const listeners = what === ForErrors ? errorListeners : subscribers;
|
|
45
|
+
listeners.add(subscriber);
|
|
46
|
+
return () => {
|
|
47
|
+
listeners.delete(subscriber);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function signal(...args: Args<T>): void;
|
|
52
|
+
function signal(error: Error): void;
|
|
53
|
+
function signal(data: typeof ClearSignal): void;
|
|
54
|
+
function signal(data: typeof GetOnSignal): OnSignal<T>;
|
|
55
|
+
function signal(subscriber: T): Unsubscriber;
|
|
56
|
+
function signal(errorListener: EventSignalSubscriber, what: typeof ForErrors): Unsubscriber;
|
|
57
|
+
function signal(...args: any[]): Unsubscriber | OnSignal<T> | void {
|
|
58
|
+
const arg = args[0];
|
|
59
|
+
if (typeof arg === 'function') {
|
|
60
|
+
return onSignal(arg);
|
|
61
|
+
} else if (arg === ClearSignal) {
|
|
62
|
+
subscribers.clear();
|
|
63
|
+
errorListeners.clear();
|
|
64
|
+
} else if (arg === GetOnSignal) {
|
|
65
|
+
return onSignal as OnSignal<T>;
|
|
66
|
+
} else if (arg instanceof Error) {
|
|
67
|
+
errorListeners.forEach(listener => listener(arg));
|
|
68
|
+
} else {
|
|
69
|
+
subscribers.forEach(listener => listener(...args));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return signal;
|
|
74
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export declare type Unsubscriber = () => void;
|
|
5
|
-
export declare type OnSignal<T extends Subscriber = Subscriber> = {
|
|
6
|
-
(subscriber: T): Unsubscriber;
|
|
7
|
-
(errorListener: ErrorSubscriber, what: typeof ForErrors): Unsubscriber;
|
|
8
|
-
};
|
|
9
|
-
export declare type Signal<T extends Subscriber = Subscriber> = OnSignal<T> & {
|
|
10
|
-
(...args: Args<T>): void;
|
|
11
|
-
(data: Error): void;
|
|
12
|
-
(data: typeof ClearSignal): void;
|
|
13
|
-
(data: typeof GetOnSignal): OnSignal<T>;
|
|
14
|
-
};
|
|
15
|
-
export declare const ClearSignal: unique symbol;
|
|
16
|
-
export declare const GetOnSignal: unique symbol;
|
|
17
|
-
export declare const ForErrors: unique symbol;
|
|
18
|
-
/**
|
|
19
|
-
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
20
|
-
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
21
|
-
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
22
|
-
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
23
|
-
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
24
|
-
* @example
|
|
25
|
-
* const onLoad = signal();
|
|
26
|
-
*
|
|
27
|
-
* // Subscribe to data
|
|
28
|
-
* onLoad((data) => console.log('loaded', data));
|
|
29
|
-
* onLoad((error) => console.error('error', error), true);
|
|
30
|
-
*
|
|
31
|
-
* // Dispatch data
|
|
32
|
-
* onLoad('data'); // logs 'loaded data'
|
|
33
|
-
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
34
|
-
*/
|
|
35
|
-
export declare function signal<T extends Subscriber = Subscriber>(): Signal<T>;
|
|
36
|
-
export {};
|
|
1
|
+
export * from './eventSignal';
|
|
2
|
+
export * from './reactiveSignal';
|
|
3
|
+
export * from './signalStore';
|
package/index.js
CHANGED
|
@@ -1,51 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
/**
|
|
5
|
-
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
6
|
-
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
7
|
-
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
8
|
-
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
9
|
-
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
10
|
-
* @example
|
|
11
|
-
* const onLoad = signal();
|
|
12
|
-
*
|
|
13
|
-
* // Subscribe to data
|
|
14
|
-
* onLoad((data) => console.log('loaded', data));
|
|
15
|
-
* onLoad((error) => console.error('error', error), true);
|
|
16
|
-
*
|
|
17
|
-
* // Dispatch data
|
|
18
|
-
* onLoad('data'); // logs 'loaded data'
|
|
19
|
-
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
20
|
-
*/
|
|
21
|
-
export function signal() {
|
|
22
|
-
const subscribers = new Set();
|
|
23
|
-
const errorListeners = new Set();
|
|
24
|
-
function onSignal(subscriber, what) {
|
|
25
|
-
const listeners = what === ForErrors ? errorListeners : subscribers;
|
|
26
|
-
listeners.add(subscriber);
|
|
27
|
-
return () => {
|
|
28
|
-
listeners.delete(subscriber);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function signal(...args) {
|
|
32
|
-
const arg = args[0];
|
|
33
|
-
if (typeof arg === 'function') {
|
|
34
|
-
return onSignal(arg);
|
|
35
|
-
}
|
|
36
|
-
else if (arg === ClearSignal) {
|
|
37
|
-
subscribers.clear();
|
|
38
|
-
errorListeners.clear();
|
|
39
|
-
}
|
|
40
|
-
else if (arg === GetOnSignal) {
|
|
41
|
-
return onSignal;
|
|
42
|
-
}
|
|
43
|
-
else if (arg instanceof Error) {
|
|
44
|
-
errorListeners.forEach(listener => listener(arg));
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
subscribers.forEach(listener => listener(...args));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return signal;
|
|
51
|
-
}
|
|
1
|
+
export * from './eventSignal';
|
|
2
|
+
export * from './reactiveSignal';
|
|
3
|
+
export * from './signalStore';
|
package/index.ts
CHANGED
|
@@ -1,75 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export type Unsubscriber = () => void;
|
|
5
|
-
|
|
6
|
-
export type OnSignal<T extends Subscriber = Subscriber> = {
|
|
7
|
-
(subscriber: T): Unsubscriber;
|
|
8
|
-
(errorListener: ErrorSubscriber, what: typeof ForErrors): Unsubscriber;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type Signal<T extends Subscriber = Subscriber> = OnSignal<T> & {
|
|
12
|
-
(...args: Args<T>): void;
|
|
13
|
-
(data: Error): void;
|
|
14
|
-
(data: typeof ClearSignal): void;
|
|
15
|
-
(data: typeof GetOnSignal): OnSignal<T>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const ClearSignal = Symbol();
|
|
19
|
-
export const GetOnSignal = Symbol();
|
|
20
|
-
export const ForErrors = Symbol();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates a signal, a function that can be used to subscribe to events. The signal can be called with a subscriber
|
|
25
|
-
* function, which will be called when the signal is dispatched. The signal can also be called with data, which will
|
|
26
|
-
* dispatch to all subscribers. An optional second argument can be passed to subscribe to errors instead. When the
|
|
27
|
-
* signal is called with an instance of Error, it will dispatch to all error listeners.
|
|
28
|
-
* The signal can also be called with `ClearSignal`, which will clear all subscribers.
|
|
29
|
-
* @example
|
|
30
|
-
* const onLoad = signal();
|
|
31
|
-
*
|
|
32
|
-
* // Subscribe to data
|
|
33
|
-
* onLoad((data) => console.log('loaded', data));
|
|
34
|
-
* onLoad((error) => console.error('error', error), true);
|
|
35
|
-
*
|
|
36
|
-
* // Dispatch data
|
|
37
|
-
* onLoad('data'); // logs 'loaded data'
|
|
38
|
-
* onLoad(new Error('error')); // logs 'error Error: error'
|
|
39
|
-
*/
|
|
40
|
-
export function signal<T extends Subscriber = Subscriber>(): Signal<T> {
|
|
41
|
-
const subscribers = new Set<Subscriber>();
|
|
42
|
-
const errorListeners = new Set<Subscriber>();
|
|
43
|
-
|
|
44
|
-
function onSignal(subscriber: T | ErrorSubscriber, what?: typeof ForErrors): Unsubscriber {
|
|
45
|
-
const listeners = what === ForErrors ? errorListeners : subscribers;
|
|
46
|
-
listeners.add(subscriber);
|
|
47
|
-
return () => {
|
|
48
|
-
listeners.delete(subscriber);
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function signal(...args: Args<T>): void;
|
|
53
|
-
function signal(error: Error): void;
|
|
54
|
-
function signal(data: typeof ClearSignal): void;
|
|
55
|
-
function signal(data: typeof GetOnSignal): OnSignal<T>;
|
|
56
|
-
function signal(subscriber: T): Unsubscriber;
|
|
57
|
-
function signal(errorListener: Subscriber, what: typeof ForErrors): Unsubscriber;
|
|
58
|
-
function signal(...args: any[]): Unsubscriber | OnSignal<T> | void {
|
|
59
|
-
const arg = args[0];
|
|
60
|
-
if (typeof arg === 'function') {
|
|
61
|
-
return onSignal(arg);
|
|
62
|
-
} else if (arg === ClearSignal) {
|
|
63
|
-
subscribers.clear();
|
|
64
|
-
errorListeners.clear();
|
|
65
|
-
} else if (arg === GetOnSignal) {
|
|
66
|
-
return onSignal as OnSignal<T>;
|
|
67
|
-
} else if (arg instanceof Error) {
|
|
68
|
-
errorListeners.forEach(listener => listener(arg));
|
|
69
|
-
} else {
|
|
70
|
-
subscribers.forEach(listener => listener(...args));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return signal;
|
|
75
|
-
}
|
|
1
|
+
export * from './eventSignal';
|
|
2
|
+
export * from './reactiveSignal';
|
|
3
|
+
export * from './signalStore';
|
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export interface SignalOptions<T> {
|
|
2
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
3
|
+
}
|
|
4
|
+
export declare type Unsubscribe = () => void;
|
|
5
|
+
export declare type Cancel = () => void;
|
|
6
|
+
export declare const Timing: {
|
|
7
|
+
Tick: (fn: () => void) => void;
|
|
8
|
+
AnimationFrame: (fn: () => void) => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A Signal is a single getter/setter function that holds a value and notifies subscribers when the value changes
|
|
12
|
+
* The signal function can be called with no arguments to get the current value, with a function argument to run an
|
|
13
|
+
* update function which should receive the value and return a new one, or with a value argument which will replace the
|
|
14
|
+
* signal's current value. The signal function always returns the current value.
|
|
15
|
+
*
|
|
16
|
+
* The optional second argument, `set`, can be used to force the first argument as the new value. This is useful when
|
|
17
|
+
* the first argument is a function or `undefined` since the signal will assume any function is an updater function and
|
|
18
|
+
* any `undefined` value is a request to get the current value.
|
|
19
|
+
*/
|
|
20
|
+
export declare type ReactiveSignal<T> = {
|
|
21
|
+
(): T;
|
|
22
|
+
(value: T | ReactiveSignalUpdater<T>, set?: false): T;
|
|
23
|
+
(value: T, set: true): T;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A Computed Signal is a signal that is the result of a function that depends on other signals. The function is called
|
|
27
|
+
* whenever the computed signal is accessed if there are no subscribers, or whenever its dependent signals change if
|
|
28
|
+
* there are subscribers so that subscribers to the computed signal can be informed.
|
|
29
|
+
*/
|
|
30
|
+
export declare type ComputedSignal<T> = () => T;
|
|
31
|
+
/**
|
|
32
|
+
* A Signal Subscriber is a function that will be called whenever the signal's value changes. The subscriber will be
|
|
33
|
+
* called with the new value. The subscriber can be used to update the DOM or trigger other side effects.
|
|
34
|
+
*/
|
|
35
|
+
export declare type ReactiveSignalSubscriber<T> = (value: T) => void;
|
|
36
|
+
/**
|
|
37
|
+
* A Signal Updater is a function that will be called with the current value of the signal and should return a new
|
|
38
|
+
* value. The updater can be used to update the signal's value based on its current value.
|
|
39
|
+
*/
|
|
40
|
+
export declare type ReactiveSignalUpdater<T> = (prev: T) => T;
|
|
41
|
+
/**
|
|
42
|
+
* An Observer is a function that will be called whenever any of the signals it depends on change. The observer can be
|
|
43
|
+
* used to update the DOM or trigger other side effects.
|
|
44
|
+
* The observer will be called immediately (or after certain a timing option) and whenever any of the signals it depends
|
|
45
|
+
* on change.
|
|
46
|
+
*/
|
|
47
|
+
export declare type ReactiveSignalObserver = () => void;
|
|
48
|
+
/**
|
|
49
|
+
* A Timing is a function that will be called with a function to execute. The timing function should execute the passed
|
|
50
|
+
* function at some point in the future. The default timing is `Timing.Immediate` which executes the function
|
|
51
|
+
* immediately.
|
|
52
|
+
*/
|
|
53
|
+
export declare type Timing = (fn: () => void) => Cancel;
|
|
54
|
+
/**
|
|
55
|
+
* A Subscription Change is a function that will be called whenever the signal's subscribers changes from none to some
|
|
56
|
+
* or some to none. The subscription change will be called with a boolean indicating whether there are any subscribers.
|
|
57
|
+
*/
|
|
58
|
+
export declare type SubscriptionChange = (hasSubscribers: boolean) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Create a Signal with an initial value and optional options. The options can include an `equals` function which will
|
|
61
|
+
* be used to determine if the new value is different from the current value. If the new value is different, the signal
|
|
62
|
+
* will be updated and all subscribers will be notified.
|
|
63
|
+
* The returned signal function can be called with no arguments to get the current value, with a function argument to
|
|
64
|
+
* run an update function which should receive the value and return a new one, or with a value argument which will
|
|
65
|
+
* replace the signal's current value. The signal function always returns the current value.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reactiveSignal<T>(value: T, options?: SignalOptions<T>): ReactiveSignal<T>;
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to be notified whenever a Signal's value changes. The Subscriber function will be called immediately with
|
|
70
|
+
* the current value of the Signal and again whenever the Signal's value changes.
|
|
71
|
+
*
|
|
72
|
+
* The optional third argument, `timing`, can be used to specify when the function should be called. The default is
|
|
73
|
+
* `Timing.Immediate` which executes the function immediately.
|
|
74
|
+
*
|
|
75
|
+
* The returned function can be called to unsubscribe from the Signal.
|
|
76
|
+
*/
|
|
77
|
+
export declare function subscribe<T>(signal: ReactiveSignal<T>, subscriber: ReactiveSignalSubscriber<T>, timing?: Timing): Unsubscribe;
|
|
78
|
+
/**
|
|
79
|
+
* Get notified when a Signal's subscribers changes from none to some or some to none.
|
|
80
|
+
*/
|
|
81
|
+
export declare function onSubscriptionChange(signal: ReactiveSignal<any>, onChange: (hasSubscribers: boolean) => void): Unsubscribe;
|
|
82
|
+
/**
|
|
83
|
+
* Calls an Observer function after Timing amount of time (default is immediate, but can be on the next tick or the next
|
|
84
|
+
* animation frame) and again after Timing whenever any of the signals it depends on change.
|
|
85
|
+
* The Observer function will be called immediately (or after the timing) and again whenever any of the signals it
|
|
86
|
+
* depends on change.
|
|
87
|
+
* The returned function can be called to unsubscribe from the signals that are called when the effect is run.
|
|
88
|
+
*
|
|
89
|
+
* The optional second argument, `timing`, can be used to specify when the function should be called. The default is
|
|
90
|
+
* undefined which executes the function immediately.
|
|
91
|
+
*/
|
|
92
|
+
export declare function observe(fn: ReactiveSignalObserver, timing?: Timing): Unsubscribe;
|
|
93
|
+
/**
|
|
94
|
+
* Create a Computed Signal which is a signal that is the result of a function that depends on other signals. The
|
|
95
|
+
* function is called immediately whenever the computed signal is accessed if there are no subscribers, or whenever its
|
|
96
|
+
* dependent signals change if there are subscribers so that subscribers to the computed signal can be informed.
|
|
97
|
+
*
|
|
98
|
+
* The optional second argument, `when`, can be used to specify when updater function should be called. The default is
|
|
99
|
+
* undefined which executes the function immediately after any change to any signal it relies on. This can
|
|
100
|
+
* prevent unnecessary updates if the function is expensive to run.
|
|
101
|
+
*/
|
|
102
|
+
export declare function computedSignal<T>(fn: ReactiveSignalUpdater<T>, when?: Timing): ComputedSignal<T>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Different timings for when to execute an observer observing a signal
|
|
2
|
+
export const Timing = {
|
|
3
|
+
// Execute the function on the next tick of the event loop
|
|
4
|
+
Tick: (fn) => {
|
|
5
|
+
Promise.resolve(fn);
|
|
6
|
+
},
|
|
7
|
+
// Execute the function on the next animation frame
|
|
8
|
+
AnimationFrame: (fn) => {
|
|
9
|
+
globalThis.requestAnimationFrame(fn);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
// The context for the current run and its unsubscribes
|
|
13
|
+
let context = null;
|
|
14
|
+
// A map to keep track of listeners to subscription changes
|
|
15
|
+
const onSubscriptionChanges = new WeakMap();
|
|
16
|
+
/**
|
|
17
|
+
* Create a Signal with an initial value and optional options. The options can include an `equals` function which will
|
|
18
|
+
* be used to determine if the new value is different from the current value. If the new value is different, the signal
|
|
19
|
+
* will be updated and all subscribers will be notified.
|
|
20
|
+
* The returned signal function can be called with no arguments to get the current value, with a function argument to
|
|
21
|
+
* run an update function which should receive the value and return a new one, or with a value argument which will
|
|
22
|
+
* replace the signal's current value. The signal function always returns the current value.
|
|
23
|
+
*/
|
|
24
|
+
export function reactiveSignal(value, options) {
|
|
25
|
+
// A map to keep track of subscribers
|
|
26
|
+
const subscribers = new Map();
|
|
27
|
+
// The signal is a function that will return a value when called without arguments, or update the value when called
|
|
28
|
+
// with an argument. The update value can be a new value or an updater function.
|
|
29
|
+
const signal = ((newValue, set) => {
|
|
30
|
+
// If no new value is provided, subscribe the current run to this signal and return the current value
|
|
31
|
+
if (!set && newValue === undefined) {
|
|
32
|
+
// If there is a context (an observer is running), add the observer's subscriber to the signal
|
|
33
|
+
if (context) {
|
|
34
|
+
const { subscriber: run, unsubscribes } = context;
|
|
35
|
+
let unsubscribe = subscribers.get(run);
|
|
36
|
+
// If the run is not already subscribed, subscribe it
|
|
37
|
+
if (!unsubscribe) {
|
|
38
|
+
// Create the unsubscribe function
|
|
39
|
+
unsubscribe = () => {
|
|
40
|
+
subscribers.delete(run);
|
|
41
|
+
// If there are no more subscribers, notify the subscription changes
|
|
42
|
+
if (subscribers.size === 0) {
|
|
43
|
+
const onChanges = onSubscriptionChanges.get(signal);
|
|
44
|
+
if (onChanges)
|
|
45
|
+
onChanges.forEach(onChange => onChange(false));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
// Add the unsubscribe function to the signal's subscribers
|
|
49
|
+
subscribers.set(run, unsubscribe);
|
|
50
|
+
// If this changed the number of subscribers from 0 to 1, notify any subscription change subscribers
|
|
51
|
+
if (subscribers.size === 1) {
|
|
52
|
+
const onChanges = onSubscriptionChanges.get(signal);
|
|
53
|
+
if (onChanges)
|
|
54
|
+
onChanges.forEach(onChange => onChange(true));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Add the unsubscribe function to the run's unsubscribes
|
|
58
|
+
unsubscribes.add(unsubscribe);
|
|
59
|
+
}
|
|
60
|
+
// Return the current value
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
// If the new value is a function, call it with the current value as an argument
|
|
64
|
+
if (!set && typeof newValue === 'function') {
|
|
65
|
+
newValue = newValue(value);
|
|
66
|
+
}
|
|
67
|
+
// If the new value is different from the current value (according to the equals function if provided), update the
|
|
68
|
+
// value and notify all subscribers
|
|
69
|
+
if (options?.equals ? !options.equals(value, newValue) : value !== newValue) {
|
|
70
|
+
value = newValue;
|
|
71
|
+
subscribers.forEach((_, run) => run(value));
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
});
|
|
75
|
+
// Return the signal function
|
|
76
|
+
return signal;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Subscribe to be notified whenever a Signal's value changes. The Subscriber function will be called immediately with
|
|
80
|
+
* the current value of the Signal and again whenever the Signal's value changes.
|
|
81
|
+
*
|
|
82
|
+
* The optional third argument, `timing`, can be used to specify when the function should be called. The default is
|
|
83
|
+
* `Timing.Immediate` which executes the function immediately.
|
|
84
|
+
*
|
|
85
|
+
* The returned function can be called to unsubscribe from the Signal.
|
|
86
|
+
*/
|
|
87
|
+
export function subscribe(signal, subscriber, timing) {
|
|
88
|
+
if (timing) {
|
|
89
|
+
let queued = false;
|
|
90
|
+
const subFn = subscriber;
|
|
91
|
+
subscriber = () => {
|
|
92
|
+
if (!queued) {
|
|
93
|
+
queued = true;
|
|
94
|
+
timing(() => {
|
|
95
|
+
queued = false;
|
|
96
|
+
subFn(signal());
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Set the current context so we can get the unsubscribe
|
|
102
|
+
context = { subscriber, unsubscribes: new Set() };
|
|
103
|
+
// Get the current value of the signal
|
|
104
|
+
const value = signal();
|
|
105
|
+
// Get the unsubscribe function for the subscriber
|
|
106
|
+
const unsubscribe = context.unsubscribes.values().next().value;
|
|
107
|
+
// Clear the current context
|
|
108
|
+
context = null;
|
|
109
|
+
// Call the subscriber with the current value
|
|
110
|
+
subscriber(value);
|
|
111
|
+
// Return the unsubscribe function
|
|
112
|
+
return unsubscribe;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get notified when a Signal's subscribers changes from none to some or some to none.
|
|
116
|
+
*/
|
|
117
|
+
export function onSubscriptionChange(signal, onChange) {
|
|
118
|
+
// Get the set of onChange functions for the signal
|
|
119
|
+
let onChanges = onSubscriptionChanges.get(signal);
|
|
120
|
+
// If there is no set, create one and add it to the map
|
|
121
|
+
if (!onChanges)
|
|
122
|
+
onSubscriptionChanges.set(signal, (onChanges = new Set()));
|
|
123
|
+
// Add the onChange function to the set
|
|
124
|
+
onChanges.add(onChange);
|
|
125
|
+
// Return a function that removes the onChange function from the set
|
|
126
|
+
return () => {
|
|
127
|
+
onChanges.delete(onChange);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Calls an Observer function after Timing amount of time (default is immediate, but can be on the next tick or the next
|
|
132
|
+
* animation frame) and again after Timing whenever any of the signals it depends on change.
|
|
133
|
+
* The Observer function will be called immediately (or after the timing) and again whenever any of the signals it
|
|
134
|
+
* depends on change.
|
|
135
|
+
* The returned function can be called to unsubscribe from the signals that are called when the effect is run.
|
|
136
|
+
*
|
|
137
|
+
* The optional second argument, `timing`, can be used to specify when the function should be called. The default is
|
|
138
|
+
* undefined which executes the function immediately.
|
|
139
|
+
*/
|
|
140
|
+
export function observe(fn, timing) {
|
|
141
|
+
let dirty = true;
|
|
142
|
+
let unsubscribes = new Set();
|
|
143
|
+
// Subscribe to all the signals that are called when the effect is run
|
|
144
|
+
const subscriber = () => {
|
|
145
|
+
if (dirty)
|
|
146
|
+
return;
|
|
147
|
+
dirty = true;
|
|
148
|
+
if (timing)
|
|
149
|
+
timing(() => onChange());
|
|
150
|
+
else
|
|
151
|
+
onChange();
|
|
152
|
+
};
|
|
153
|
+
// Called immediately and whenever any of the signals it depends on change (after the timing)
|
|
154
|
+
const onChange = () => {
|
|
155
|
+
if (!dirty)
|
|
156
|
+
return;
|
|
157
|
+
dirty = false;
|
|
158
|
+
// Set the context for the effect
|
|
159
|
+
context = { subscriber, unsubscribes: new Set() };
|
|
160
|
+
// Run the effect collecting all the unsubscribes from the signals that are called when it is run
|
|
161
|
+
fn();
|
|
162
|
+
// Filter out unchanged unsubscribes, leaving only those which no longer apply
|
|
163
|
+
context.unsubscribes.forEach(u => unsubscribes.delete(u));
|
|
164
|
+
// Unsubscribe from all the signals that are no longer needed
|
|
165
|
+
unsubscribes.forEach(u => u());
|
|
166
|
+
// Set the new unsubscribes
|
|
167
|
+
unsubscribes = context.unsubscribes;
|
|
168
|
+
// Clear the context
|
|
169
|
+
context = null;
|
|
170
|
+
};
|
|
171
|
+
// Call immediately (or on the next timing)
|
|
172
|
+
if (timing)
|
|
173
|
+
timing(() => onChange());
|
|
174
|
+
else
|
|
175
|
+
onChange();
|
|
176
|
+
// Return a function that unsubscribes from all the signals that are called when the effect is run
|
|
177
|
+
return () => unsubscribes.forEach(u => u());
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create a Computed Signal which is a signal that is the result of a function that depends on other signals. The
|
|
181
|
+
* function is called immediately whenever the computed signal is accessed if there are no subscribers, or whenever its
|
|
182
|
+
* dependent signals change if there are subscribers so that subscribers to the computed signal can be informed.
|
|
183
|
+
*
|
|
184
|
+
* The optional second argument, `when`, can be used to specify when updater function should be called. The default is
|
|
185
|
+
* undefined which executes the function immediately after any change to any signal it relies on. This can
|
|
186
|
+
* prevent unnecessary updates if the function is expensive to run.
|
|
187
|
+
*/
|
|
188
|
+
export function computedSignal(fn, when) {
|
|
189
|
+
// Create the signal
|
|
190
|
+
const signal = reactiveSignal(undefined);
|
|
191
|
+
// Store the unsubscribe function from the observer. We will only observe the function when there are subscribers to
|
|
192
|
+
// this computed signal.
|
|
193
|
+
let unsubscribe = null;
|
|
194
|
+
// Subscribe to the signal's subscription changes so we know when to start and stop observing
|
|
195
|
+
onSubscriptionChange(signal, hasSubscribers => {
|
|
196
|
+
// If there are subscribers, start observing the function
|
|
197
|
+
if (hasSubscribers) {
|
|
198
|
+
if (!unsubscribe)
|
|
199
|
+
unsubscribe = observe(() => signal(fn), when);
|
|
200
|
+
}
|
|
201
|
+
else if (unsubscribe) {
|
|
202
|
+
// If there are no subscribers, stop observing the function
|
|
203
|
+
unsubscribe();
|
|
204
|
+
unsubscribe = null;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const computed = () => (unsubscribe ? signal() : signal(fn));
|
|
208
|
+
// Return the signal
|
|
209
|
+
return computed;
|
|
210
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// The options for a Signal
|
|
2
|
+
export interface SignalOptions<T> {
|
|
3
|
+
equals?: false | ((prev: T, next: T) => boolean);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// The types for an unsubscribe and cancel function
|
|
7
|
+
export type Unsubscribe = () => void;
|
|
8
|
+
export type Cancel = () => void;
|
|
9
|
+
|
|
10
|
+
// Different timings for when to execute an observer observing a signal
|
|
11
|
+
export const Timing = {
|
|
12
|
+
// Execute the function on the next tick of the event loop
|
|
13
|
+
Tick: (fn: () => void) => {
|
|
14
|
+
Promise.resolve(fn);
|
|
15
|
+
},
|
|
16
|
+
// Execute the function on the next animation frame
|
|
17
|
+
AnimationFrame: (fn: () => void) => {
|
|
18
|
+
(globalThis as any).requestAnimationFrame(fn);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// The context for the current run and its unsubscribes
|
|
23
|
+
let context: { subscriber: ReactiveSignalSubscriber<any>; unsubscribes: Set<Unsubscribe> } | null = null;
|
|
24
|
+
|
|
25
|
+
// A map to keep track of listeners to subscription changes
|
|
26
|
+
const onSubscriptionChanges = new WeakMap<ReactiveSignal<any>, Set<SubscriptionChange>>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A Signal is a single getter/setter function that holds a value and notifies subscribers when the value changes
|
|
30
|
+
* The signal function can be called with no arguments to get the current value, with a function argument to run an
|
|
31
|
+
* update function which should receive the value and return a new one, or with a value argument which will replace the
|
|
32
|
+
* signal's current value. The signal function always returns the current value.
|
|
33
|
+
*
|
|
34
|
+
* The optional second argument, `set`, can be used to force the first argument as the new value. This is useful when
|
|
35
|
+
* the first argument is a function or `undefined` since the signal will assume any function is an updater function and
|
|
36
|
+
* any `undefined` value is a request to get the current value.
|
|
37
|
+
*/
|
|
38
|
+
export type ReactiveSignal<T> = {
|
|
39
|
+
(): T;
|
|
40
|
+
(value: T | ReactiveSignalUpdater<T>, set?: false): T;
|
|
41
|
+
(value: T, set: true): T;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A Computed Signal is a signal that is the result of a function that depends on other signals. The function is called
|
|
46
|
+
* whenever the computed signal is accessed if there are no subscribers, or whenever its dependent signals change if
|
|
47
|
+
* there are subscribers so that subscribers to the computed signal can be informed.
|
|
48
|
+
*/
|
|
49
|
+
export type ComputedSignal<T> = () => T;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A Signal Subscriber is a function that will be called whenever the signal's value changes. The subscriber will be
|
|
53
|
+
* called with the new value. The subscriber can be used to update the DOM or trigger other side effects.
|
|
54
|
+
*/
|
|
55
|
+
export type ReactiveSignalSubscriber<T> = (value: T) => void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A Signal Updater is a function that will be called with the current value of the signal and should return a new
|
|
59
|
+
* value. The updater can be used to update the signal's value based on its current value.
|
|
60
|
+
*/
|
|
61
|
+
export type ReactiveSignalUpdater<T> = (prev: T) => T;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* An Observer is a function that will be called whenever any of the signals it depends on change. The observer can be
|
|
65
|
+
* used to update the DOM or trigger other side effects.
|
|
66
|
+
* The observer will be called immediately (or after certain a timing option) and whenever any of the signals it depends
|
|
67
|
+
* on change.
|
|
68
|
+
*/
|
|
69
|
+
export type ReactiveSignalObserver = () => void;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A Timing is a function that will be called with a function to execute. The timing function should execute the passed
|
|
73
|
+
* function at some point in the future. The default timing is `Timing.Immediate` which executes the function
|
|
74
|
+
* immediately.
|
|
75
|
+
*/
|
|
76
|
+
export type Timing = (fn: () => void) => Cancel;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A Subscription Change is a function that will be called whenever the signal's subscribers changes from none to some
|
|
80
|
+
* or some to none. The subscription change will be called with a boolean indicating whether there are any subscribers.
|
|
81
|
+
*/
|
|
82
|
+
export type SubscriptionChange = (hasSubscribers: boolean) => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a Signal with an initial value and optional options. The options can include an `equals` function which will
|
|
86
|
+
* be used to determine if the new value is different from the current value. If the new value is different, the signal
|
|
87
|
+
* will be updated and all subscribers will be notified.
|
|
88
|
+
* The returned signal function can be called with no arguments to get the current value, with a function argument to
|
|
89
|
+
* run an update function which should receive the value and return a new one, or with a value argument which will
|
|
90
|
+
* replace the signal's current value. The signal function always returns the current value.
|
|
91
|
+
*/
|
|
92
|
+
export function reactiveSignal<T>(value: T, options?: SignalOptions<T>): ReactiveSignal<T> {
|
|
93
|
+
// A map to keep track of subscribers
|
|
94
|
+
const subscribers = new Map<ReactiveSignalSubscriber<T>, Unsubscribe>();
|
|
95
|
+
|
|
96
|
+
// The signal is a function that will return a value when called without arguments, or update the value when called
|
|
97
|
+
// with an argument. The update value can be a new value or an updater function.
|
|
98
|
+
const signal = ((newValue?: T | ReactiveSignalUpdater<T>, set?: boolean) => {
|
|
99
|
+
// If no new value is provided, subscribe the current run to this signal and return the current value
|
|
100
|
+
if (!set && newValue === undefined) {
|
|
101
|
+
// If there is a context (an observer is running), add the observer's subscriber to the signal
|
|
102
|
+
if (context) {
|
|
103
|
+
const { subscriber: run, unsubscribes } = context;
|
|
104
|
+
let unsubscribe = subscribers.get(run);
|
|
105
|
+
|
|
106
|
+
// If the run is not already subscribed, subscribe it
|
|
107
|
+
if (!unsubscribe) {
|
|
108
|
+
// Create the unsubscribe function
|
|
109
|
+
unsubscribe = () => {
|
|
110
|
+
subscribers.delete(run);
|
|
111
|
+
|
|
112
|
+
// If there are no more subscribers, notify the subscription changes
|
|
113
|
+
if (subscribers.size === 0) {
|
|
114
|
+
const onChanges = onSubscriptionChanges.get(signal);
|
|
115
|
+
if (onChanges) onChanges.forEach(onChange => onChange(false));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Add the unsubscribe function to the signal's subscribers
|
|
120
|
+
subscribers.set(run, unsubscribe);
|
|
121
|
+
|
|
122
|
+
// If this changed the number of subscribers from 0 to 1, notify any subscription change subscribers
|
|
123
|
+
if (subscribers.size === 1) {
|
|
124
|
+
const onChanges = onSubscriptionChanges.get(signal);
|
|
125
|
+
if (onChanges) onChanges.forEach(onChange => onChange(true));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add the unsubscribe function to the run's unsubscribes
|
|
130
|
+
unsubscribes.add(unsubscribe);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Return the current value
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If the new value is a function, call it with the current value as an argument
|
|
138
|
+
if (!set && typeof newValue === 'function') {
|
|
139
|
+
newValue = (newValue as ReactiveSignalUpdater<T>)(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If the new value is different from the current value (according to the equals function if provided), update the
|
|
143
|
+
// value and notify all subscribers
|
|
144
|
+
if (options?.equals ? !options.equals(value!, newValue as T) : value !== newValue) {
|
|
145
|
+
value = newValue as T;
|
|
146
|
+
subscribers.forEach((_, run) => run(value!));
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}) as ReactiveSignal<T>;
|
|
150
|
+
|
|
151
|
+
// Return the signal function
|
|
152
|
+
return signal;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Subscribe to be notified whenever a Signal's value changes. The Subscriber function will be called immediately with
|
|
157
|
+
* the current value of the Signal and again whenever the Signal's value changes.
|
|
158
|
+
*
|
|
159
|
+
* The optional third argument, `timing`, can be used to specify when the function should be called. The default is
|
|
160
|
+
* `Timing.Immediate` which executes the function immediately.
|
|
161
|
+
*
|
|
162
|
+
* The returned function can be called to unsubscribe from the Signal.
|
|
163
|
+
*/
|
|
164
|
+
export function subscribe<T>(
|
|
165
|
+
signal: ReactiveSignal<T>,
|
|
166
|
+
subscriber: ReactiveSignalSubscriber<T>,
|
|
167
|
+
timing?: Timing
|
|
168
|
+
): Unsubscribe {
|
|
169
|
+
if (timing) {
|
|
170
|
+
let queued = false;
|
|
171
|
+
const subFn = subscriber;
|
|
172
|
+
subscriber = () => {
|
|
173
|
+
if (!queued) {
|
|
174
|
+
queued = true;
|
|
175
|
+
timing(() => {
|
|
176
|
+
queued = false;
|
|
177
|
+
subFn(signal());
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set the current context so we can get the unsubscribe
|
|
184
|
+
context = { subscriber, unsubscribes: new Set() };
|
|
185
|
+
|
|
186
|
+
// Get the current value of the signal
|
|
187
|
+
const value = signal();
|
|
188
|
+
|
|
189
|
+
// Get the unsubscribe function for the subscriber
|
|
190
|
+
const unsubscribe = context.unsubscribes.values().next().value;
|
|
191
|
+
|
|
192
|
+
// Clear the current context
|
|
193
|
+
context = null;
|
|
194
|
+
|
|
195
|
+
// Call the subscriber with the current value
|
|
196
|
+
subscriber(value);
|
|
197
|
+
|
|
198
|
+
// Return the unsubscribe function
|
|
199
|
+
return unsubscribe;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get notified when a Signal's subscribers changes from none to some or some to none.
|
|
204
|
+
*/
|
|
205
|
+
export function onSubscriptionChange(
|
|
206
|
+
signal: ReactiveSignal<any>,
|
|
207
|
+
onChange: (hasSubscribers: boolean) => void
|
|
208
|
+
): Unsubscribe {
|
|
209
|
+
// Get the set of onChange functions for the signal
|
|
210
|
+
let onChanges = onSubscriptionChanges.get(signal);
|
|
211
|
+
|
|
212
|
+
// If there is no set, create one and add it to the map
|
|
213
|
+
if (!onChanges) onSubscriptionChanges.set(signal, (onChanges = new Set()));
|
|
214
|
+
|
|
215
|
+
// Add the onChange function to the set
|
|
216
|
+
onChanges.add(onChange);
|
|
217
|
+
|
|
218
|
+
// Return a function that removes the onChange function from the set
|
|
219
|
+
return () => {
|
|
220
|
+
onChanges!.delete(onChange);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calls an Observer function after Timing amount of time (default is immediate, but can be on the next tick or the next
|
|
226
|
+
* animation frame) and again after Timing whenever any of the signals it depends on change.
|
|
227
|
+
* The Observer function will be called immediately (or after the timing) and again whenever any of the signals it
|
|
228
|
+
* depends on change.
|
|
229
|
+
* The returned function can be called to unsubscribe from the signals that are called when the effect is run.
|
|
230
|
+
*
|
|
231
|
+
* The optional second argument, `timing`, can be used to specify when the function should be called. The default is
|
|
232
|
+
* undefined which executes the function immediately.
|
|
233
|
+
*/
|
|
234
|
+
export function observe(fn: ReactiveSignalObserver, timing?: Timing): Unsubscribe {
|
|
235
|
+
let dirty = true;
|
|
236
|
+
let unsubscribes = new Set<Unsubscribe>();
|
|
237
|
+
|
|
238
|
+
// Subscribe to all the signals that are called when the effect is run
|
|
239
|
+
const subscriber = () => {
|
|
240
|
+
if (dirty) return;
|
|
241
|
+
dirty = true;
|
|
242
|
+
if (timing) timing(() => onChange());
|
|
243
|
+
else onChange();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Called immediately and whenever any of the signals it depends on change (after the timing)
|
|
247
|
+
const onChange = () => {
|
|
248
|
+
if (!dirty) return;
|
|
249
|
+
dirty = false;
|
|
250
|
+
|
|
251
|
+
// Set the context for the effect
|
|
252
|
+
context = { subscriber, unsubscribes: new Set() };
|
|
253
|
+
|
|
254
|
+
// Run the effect collecting all the unsubscribes from the signals that are called when it is run
|
|
255
|
+
fn();
|
|
256
|
+
|
|
257
|
+
// Filter out unchanged unsubscribes, leaving only those which no longer apply
|
|
258
|
+
context.unsubscribes.forEach(u => unsubscribes.delete(u));
|
|
259
|
+
|
|
260
|
+
// Unsubscribe from all the signals that are no longer needed
|
|
261
|
+
unsubscribes.forEach(u => u());
|
|
262
|
+
|
|
263
|
+
// Set the new unsubscribes
|
|
264
|
+
unsubscribes = context.unsubscribes;
|
|
265
|
+
|
|
266
|
+
// Clear the context
|
|
267
|
+
context = null;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Call immediately (or on the next timing)
|
|
271
|
+
if (timing) timing(() => onChange());
|
|
272
|
+
else onChange();
|
|
273
|
+
|
|
274
|
+
// Return a function that unsubscribes from all the signals that are called when the effect is run
|
|
275
|
+
return () => unsubscribes.forEach(u => u());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create a Computed Signal which is a signal that is the result of a function that depends on other signals. The
|
|
280
|
+
* function is called immediately whenever the computed signal is accessed if there are no subscribers, or whenever its
|
|
281
|
+
* dependent signals change if there are subscribers so that subscribers to the computed signal can be informed.
|
|
282
|
+
*
|
|
283
|
+
* The optional second argument, `when`, can be used to specify when updater function should be called. The default is
|
|
284
|
+
* undefined which executes the function immediately after any change to any signal it relies on. This can
|
|
285
|
+
* prevent unnecessary updates if the function is expensive to run.
|
|
286
|
+
*/
|
|
287
|
+
export function computedSignal<T>(fn: ReactiveSignalUpdater<T>, when?: Timing): ComputedSignal<T> {
|
|
288
|
+
// Create the signal
|
|
289
|
+
const signal = reactiveSignal<T>(undefined as T);
|
|
290
|
+
|
|
291
|
+
// Store the unsubscribe function from the observer. We will only observe the function when there are subscribers to
|
|
292
|
+
// this computed signal.
|
|
293
|
+
let unsubscribe: Unsubscribe | null = null;
|
|
294
|
+
|
|
295
|
+
// Subscribe to the signal's subscription changes so we know when to start and stop observing
|
|
296
|
+
onSubscriptionChange(signal, hasSubscribers => {
|
|
297
|
+
// If there are subscribers, start observing the function
|
|
298
|
+
if (hasSubscribers) {
|
|
299
|
+
if (!unsubscribe) unsubscribe = observe(() => signal(fn), when);
|
|
300
|
+
} else if (unsubscribe) {
|
|
301
|
+
// If there are no subscribers, stop observing the function
|
|
302
|
+
unsubscribe();
|
|
303
|
+
unsubscribe = null;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const computed = () => (unsubscribe ? signal() : signal(fn));
|
|
308
|
+
|
|
309
|
+
// Return the signal
|
|
310
|
+
return computed;
|
|
311
|
+
}
|
package/signalStore.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactiveSignal, ReactiveSignalSubscriber } from './reactiveSignal';
|
|
2
|
+
/**
|
|
3
|
+
* A store wrapper around a reactive signal. The store can be used to get, set, update, and subscribe to the signal.
|
|
4
|
+
* This is for use in Svelte 3-4.
|
|
5
|
+
*/
|
|
6
|
+
export declare function signalStore<T>(signal: ReactiveSignal<T>): {
|
|
7
|
+
get: ReactiveSignal<T>;
|
|
8
|
+
set: ReactiveSignal<T>;
|
|
9
|
+
update: ReactiveSignal<T>;
|
|
10
|
+
subscribe: (sub: ReactiveSignalSubscriber<T>) => import("./reactiveSignal").Unsubscribe;
|
|
11
|
+
};
|
package/signalStore.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { subscribe } from './reactiveSignal';
|
|
2
|
+
/**
|
|
3
|
+
* A store wrapper around a reactive signal. The store can be used to get, set, update, and subscribe to the signal.
|
|
4
|
+
* This is for use in Svelte 3-4.
|
|
5
|
+
*/
|
|
6
|
+
export function signalStore(signal) {
|
|
7
|
+
return {
|
|
8
|
+
get: signal,
|
|
9
|
+
set: signal,
|
|
10
|
+
update: signal,
|
|
11
|
+
subscribe: (sub) => subscribe(signal, sub),
|
|
12
|
+
};
|
|
13
|
+
}
|
package/signalStore.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactiveSignal, ReactiveSignalSubscriber, subscribe } from './reactiveSignal';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A store wrapper around a reactive signal. The store can be used to get, set, update, and subscribe to the signal.
|
|
5
|
+
* This is for use in Svelte 3-4.
|
|
6
|
+
*/
|
|
7
|
+
export function signalStore<T>(signal: ReactiveSignal<T>) {
|
|
8
|
+
return {
|
|
9
|
+
get: signal,
|
|
10
|
+
set: signal,
|
|
11
|
+
update: signal,
|
|
12
|
+
subscribe: (sub: ReactiveSignalSubscriber<T>) => subscribe(signal, sub),
|
|
13
|
+
};
|
|
14
|
+
}
|