@adbl/cells 0.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/.vscode/settings.json +4 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bun.lockb +0 -0
- package/index.js +3 -0
- package/jsconfig.json +25 -0
- package/library/classes.js +645 -0
- package/library/index.js +18 -0
- package/library/root.js +44 -0
- package/package.json +33 -0
- package/types/index.d.ts +1 -0
- package/types/library/classes.d.ts +324 -0
- package/types/library/index.d.ts +15 -0
- package/types/library/root.d.ts +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sefunmi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# @adbl/cells
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40adbl%2Fcells)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Cells is a powerful yet lightweight library for reactive state management in JavaScript applications. It offers an intuitive API that simplifies the complexities of managing and propagating state changes throughout your application.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Simple API**: Easy to learn and use, even for developers new to reactive programming.
|
|
11
|
+
- **Lightweight**: No external dependencies, keeping your project lean.
|
|
12
|
+
- **Flexible**: Works seamlessly with any JavaScript framework or vanilla JS.
|
|
13
|
+
- **Type-safe**: Built with TypeScript, providing excellent type inference and checking.
|
|
14
|
+
- **Performant**: Optimized for efficiency, with features like batched updates to minimize unnecessary computations.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Get started with Cells in your project:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @adbl/cells
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or if you prefer Yarn:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @adbl/cells
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Core Concepts
|
|
31
|
+
|
|
32
|
+
### 1. Source Cells
|
|
33
|
+
|
|
34
|
+
Source cells are the building blocks of your reactive state. They hold values that can change over time, automatically notifying dependents when updates occur.
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import { Cell } from '@adbl/cells';
|
|
38
|
+
|
|
39
|
+
const count = Cell.source(0);
|
|
40
|
+
console.log(count.value); // Output: 0
|
|
41
|
+
|
|
42
|
+
count.value = 5;
|
|
43
|
+
console.log(count.value); // Output: 5
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Derived Cells
|
|
47
|
+
|
|
48
|
+
Derived cells allow you to create computed values based on other cells. They update automatically when their dependencies change, ensuring your derived state is always in sync.
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const count = Cell.source(0);
|
|
52
|
+
const doubledCount = Cell.derived(() => count.value * 2);
|
|
53
|
+
|
|
54
|
+
console.log(doubledCount.value); // Output: 0
|
|
55
|
+
|
|
56
|
+
count.value = 5;
|
|
57
|
+
console.log(doubledCount.value); // Output: 10
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. Reactive Effects
|
|
61
|
+
|
|
62
|
+
Easily set up listeners to react to changes in cell values, allowing you to create side effects or update your UI in response to state changes.
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const count = Cell.source(0);
|
|
66
|
+
|
|
67
|
+
count.listen((newValue) => {
|
|
68
|
+
console.log(`Count changed to: ${newValue}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
count.value = 3; // Output: "Count changed to: 3"
|
|
72
|
+
count.value = 7; // Output: "Count changed to: 7"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 4. Global Effects
|
|
76
|
+
|
|
77
|
+
Cells allows you to set up global effects that run before or after any cell is updated, giving you fine-grained control over your application's reactive behavior.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
Cell.beforeUpdate((value) => {
|
|
81
|
+
console.log(`About to update a cell with value: ${value}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
Cell.afterUpdate((value) => {
|
|
85
|
+
console.log(`Just updated a cell with value: ${value}`);
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 5. Batch Updates
|
|
90
|
+
|
|
91
|
+
When you need to perform multiple updates but only want to trigger effects once, you can use batch updates to optimize performance:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const cell1 = Cell.source(0);
|
|
95
|
+
const cell2 = Cell.source(0);
|
|
96
|
+
|
|
97
|
+
Cell.afterUpdate(() => {
|
|
98
|
+
console.log('Update occurred');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
Cell.batch(() => {
|
|
102
|
+
cell1.value = 1;
|
|
103
|
+
cell2.value = 2;
|
|
104
|
+
});
|
|
105
|
+
// Output: "Update occurred" (only once)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 6. Async Operations
|
|
109
|
+
|
|
110
|
+
Cells provides utilities for handling asynchronous operations, making it easy to manage loading states, data, and errors:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const fetchUser = Cell.async(async (userId) => {
|
|
114
|
+
const response = await fetch(`https://api.example.com/users/${userId}`);
|
|
115
|
+
return response.json();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const { pending, data, error, run } = fetchUser;
|
|
119
|
+
|
|
120
|
+
pending.listen((isPending) => {
|
|
121
|
+
console.log(isPending ? 'Loading...' : 'Done!');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
data.listen((userData) => {
|
|
125
|
+
if (userData) {
|
|
126
|
+
console.log('User data:', userData);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
run(123); // Triggers the async operation
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 7. Flattening
|
|
134
|
+
|
|
135
|
+
Cells offers utility functions to work with nested cell structures, making it easier to handle complex state shapes:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const nestedCell = Cell.source(Cell.source(5));
|
|
139
|
+
const flattenedValue = Cell.flatten(nestedCell);
|
|
140
|
+
console.log(flattenedValue); // Output: 5
|
|
141
|
+
|
|
142
|
+
const arrayOfCells = [Cell.source(1), Cell.source(2), Cell.source(3)];
|
|
143
|
+
const flattenedArray = Cell.flattenArray(arrayOfCells);
|
|
144
|
+
console.log(flattenedArray); // Output: [1, 2, 3]
|
|
145
|
+
|
|
146
|
+
const objectWithCells = { a: Cell.source(1), b: Cell.source(2) };
|
|
147
|
+
const flattenedObject = Cell.flattenObject(objectWithCells);
|
|
148
|
+
console.log(flattenedObject); // Output: { a: 1, b: 2 }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 8. Custom Equality Checks
|
|
152
|
+
|
|
153
|
+
For more complex objects, you can provide custom equality functions to determine when a cell's value has truly changed:
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
const userCell = Cell.source(
|
|
157
|
+
{ name: 'Alice', age: 30 },
|
|
158
|
+
{
|
|
159
|
+
equals: (a, b) => a.name === b.name && a.age === b.age,
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 9. Named Effects
|
|
165
|
+
|
|
166
|
+
To aid in debugging, you can name your effects, making it easier to track and manage them:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
const count = Cell.source(0);
|
|
170
|
+
|
|
171
|
+
count.listen((value) => console.log(`Count is now: ${value}`), {
|
|
172
|
+
name: 'countLogger',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log(count.isListeningTo('countLogger')); // Output: true
|
|
176
|
+
|
|
177
|
+
count.stopListeningTo('countLogger');
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Advanced Features and API Details
|
|
181
|
+
|
|
182
|
+
### Cell Options
|
|
183
|
+
|
|
184
|
+
When creating a source cell, you have fine-grained control over its behavior:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
const cell = Cell.source(initialValue, {
|
|
188
|
+
immutable: boolean, // If true, the cell will not allow updates
|
|
189
|
+
shallowProxied: boolean, // If true, only top-level properties are proxied
|
|
190
|
+
equals: (oldValue, newValue) => boolean, // Custom equality function
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Effect Options
|
|
195
|
+
|
|
196
|
+
When setting up listeners or effects, you can customize their behavior:
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
cell.listen(callback, {
|
|
200
|
+
once: boolean, // If true, the effect will only run once
|
|
201
|
+
signal: AbortSignal, // An AbortSignal to cancel the effect
|
|
202
|
+
name: string, // A name for the effect (useful for debugging)
|
|
203
|
+
priority: number, // The priority of the effect (higher priority effects run first)
|
|
204
|
+
});
|
|
205
|
+
```
|
package/bun.lockb
ADDED
|
Binary file
|
package/index.js
ADDED
package/jsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"noUnusedLocals": true,
|
|
7
|
+
"noUnusedParameters": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"allowUnreachableCode": false,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
|
|
14
|
+
"noEmit": false,
|
|
15
|
+
"emitDeclarationOnly": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
|
|
18
|
+
"checkJs": true,
|
|
19
|
+
"strict": true,
|
|
20
|
+
|
|
21
|
+
"outDir": "types"
|
|
22
|
+
},
|
|
23
|
+
"include": ["library/**/*", "index.js"],
|
|
24
|
+
"exclude": ["types/**/*", "tests/**/*"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template Input, Output
|
|
3
|
+
* @typedef {Object} AsyncRequestAtoms
|
|
4
|
+
*
|
|
5
|
+
* @property {SourceCell<boolean>} pending
|
|
6
|
+
* Represents the loading state of an asynchronous request.
|
|
7
|
+
*
|
|
8
|
+
* @property {SourceCell<Output|null>} data
|
|
9
|
+
* Represents the data returned by the asynchronous request.
|
|
10
|
+
*
|
|
11
|
+
* @property {SourceCell<Error | null>} error
|
|
12
|
+
* Represents the errors returned by the asynchronous request, if any.
|
|
13
|
+
*
|
|
14
|
+
* @property {NeverIfAny<Input> extends never ? (input?: Input) => Promise<void> : (input: Input) => Promise<void>} run
|
|
15
|
+
* Triggers the asynchronous request.
|
|
16
|
+
*
|
|
17
|
+
* @property {(newInput?: Input, changeLoadingState?: boolean) => Promise<void>} reload Triggers the asynchronous request again with an optional new input and optionally changes the loading state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} EffectOptions
|
|
22
|
+
* @property {boolean} [once]
|
|
23
|
+
* Whether the effect should be removed after the first run.
|
|
24
|
+
* @property {AbortSignal} [signal]
|
|
25
|
+
* An AbortSignal to be used to ignore the effect if it is aborted.
|
|
26
|
+
* @property {string} [name]
|
|
27
|
+
* The name of the effect for debugging purposes.
|
|
28
|
+
* @property {number} [priority]
|
|
29
|
+
* The priority of the effect. Higher priority effects are executed first. The default priority is 0.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template T
|
|
34
|
+
* @typedef {object} CellOptions
|
|
35
|
+
* @property {boolean} [immutable]
|
|
36
|
+
* Whether the cell should be immutable. If set to true, the cell will not allow updates and will throw an error if the value is changed.
|
|
37
|
+
* @property {boolean} [shallowProxied]
|
|
38
|
+
* Whether the cell's value should be shallowly proxied. If set to true, the cell will only proxy the top-level properties of the value, preventing any changes to nested properties. This can be useful for performance optimizations.
|
|
39
|
+
* @property {(oldValue: T, newValue: T) => boolean} [equals]
|
|
40
|
+
* A function that determines whether two values are equal. If not provided, the default equality function will be used.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @template T
|
|
45
|
+
* @typedef {0 extends (1 & T) ? never : T} NeverIfAny
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { activeComputedValues, root } from './root.js';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @template T
|
|
52
|
+
*/
|
|
53
|
+
export class Cell {
|
|
54
|
+
/**
|
|
55
|
+
* @type {Array<({
|
|
56
|
+
* effect: (newValue: T) => void,
|
|
57
|
+
* options?: EffectOptions,
|
|
58
|
+
* })>}
|
|
59
|
+
* @protected
|
|
60
|
+
*/
|
|
61
|
+
effects = [];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @type {Array<WeakRef<DerivedCell<any>>>}
|
|
65
|
+
* @protected
|
|
66
|
+
*/
|
|
67
|
+
derivedCells = [];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @protected @type T
|
|
71
|
+
*/
|
|
72
|
+
wvalue = /** @type {T} */ (null);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @protected
|
|
76
|
+
* @param {T} value
|
|
77
|
+
*/
|
|
78
|
+
setValue(value) {
|
|
79
|
+
this.wvalue = value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Overrides `Object.prototype.valueOf()` to return the value stored in the Cell.
|
|
84
|
+
* @returns {T} The value of the Cell.
|
|
85
|
+
*/
|
|
86
|
+
valueOf() {
|
|
87
|
+
return this.wvalue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The value stored in the Cell.
|
|
92
|
+
* @protected @type {T}
|
|
93
|
+
*/
|
|
94
|
+
get revalued() {
|
|
95
|
+
const currentlyComputedValue = activeComputedValues.at(-1);
|
|
96
|
+
|
|
97
|
+
if (currentlyComputedValue !== undefined) {
|
|
98
|
+
const isAlreadySubscribed = this.derivedCells.some(
|
|
99
|
+
(ref) => ref.deref() === currentlyComputedValue
|
|
100
|
+
);
|
|
101
|
+
if (isAlreadySubscribed) return this.wvalue;
|
|
102
|
+
|
|
103
|
+
this.derivedCells.push(new WeakRef(currentlyComputedValue));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return this.wvalue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets a callback function that will be called whenever the value of the Cell changes.
|
|
111
|
+
* @param {(newValue: T) => void} callback - The function to be called when the value changes.
|
|
112
|
+
*/
|
|
113
|
+
set onchange(callback) {
|
|
114
|
+
this.listen(callback);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Adds the provided effect callback to the list of effects for this cell, and returns a function that can be called to remove the effect.
|
|
119
|
+
* @param {(newValue: T) => void} callback - The effect callback to add.
|
|
120
|
+
* @param {EffectOptions} [options] - The options for the effect.
|
|
121
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
122
|
+
*/
|
|
123
|
+
listen(callback, options) {
|
|
124
|
+
let effect = callback;
|
|
125
|
+
|
|
126
|
+
if (options?.signal?.aborted) {
|
|
127
|
+
return () => {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
options?.signal?.addEventListener('abort', () => {
|
|
131
|
+
this.ignore(effect);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (options?.once) {
|
|
135
|
+
effect = () => {
|
|
136
|
+
callback(this.wvalue);
|
|
137
|
+
this.ignore(effect);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options?.name) {
|
|
142
|
+
if (this.isListeningTo(options.name)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`An effect with the name "${options.name}" is already listening to this cell.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.effects.some(({ effect }) => effect === callback)) {
|
|
150
|
+
throw new Error('This effect is already listening to this cell.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.effects.push({ effect, options });
|
|
154
|
+
|
|
155
|
+
this.effects.sort((a, b) => {
|
|
156
|
+
const aPriority = a.options?.priority ?? 0;
|
|
157
|
+
const bPriority = b.options?.priority ?? 0;
|
|
158
|
+
if (aPriority === bPriority) {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
return aPriority < bPriority ? 1 : -1;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return () => this.ignore(effect);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Creates an effect that is immediately executed with the current value of the cell, and then added to the list of effects for the cell.
|
|
169
|
+
* @param {(newValue: T) => void} effect - The effect callback to add.
|
|
170
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
171
|
+
*/
|
|
172
|
+
runAndListen(effect) {
|
|
173
|
+
effect(this.wvalue);
|
|
174
|
+
return this.listen(effect);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Removes the specified effect callback from the list of effects for this cell.
|
|
179
|
+
* @param {(newValue: T) => void} callback - The effect callback to remove.
|
|
180
|
+
*/
|
|
181
|
+
ignore(callback) {
|
|
182
|
+
const index = this.effects.findIndex(({ effect }) => effect === callback);
|
|
183
|
+
if (index === -1) return;
|
|
184
|
+
|
|
185
|
+
this.effects.splice(index, 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Checks if the cell is listening to a watcher with the specified name.
|
|
190
|
+
* @param {string} name - The name of the watcher to check for.
|
|
191
|
+
* @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
|
|
192
|
+
*/
|
|
193
|
+
isListeningTo(name) {
|
|
194
|
+
return this.effects.some(({ options }) => options?.name === name);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Removes the watcher with the specified name from the list of effects for this cell.
|
|
199
|
+
* @param {string} name - The name of the watcher to stop listening to.
|
|
200
|
+
*/
|
|
201
|
+
stopListeningTo(name) {
|
|
202
|
+
const effectIndex = this.effects.findIndex(
|
|
203
|
+
({ options }) => options?.name === name
|
|
204
|
+
);
|
|
205
|
+
if (effectIndex === -1) return;
|
|
206
|
+
|
|
207
|
+
this.effects.splice(effectIndex, 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Updates the root object and notifies any registered watchers and computed dependents.
|
|
212
|
+
* This method is called whenever the root object's value changes.
|
|
213
|
+
*/
|
|
214
|
+
update() {
|
|
215
|
+
// Run watchers.
|
|
216
|
+
for (const { effect: watcher } of this.effects) {
|
|
217
|
+
if (root.batchNestingLevel > 0) {
|
|
218
|
+
root.batchedEffects.set(watcher, [this.wvalue]);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
watcher(this.wvalue);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Run computed dependents.
|
|
226
|
+
const computedDependents = this.derivedCells;
|
|
227
|
+
if (computedDependents !== undefined) {
|
|
228
|
+
for (const dependent of computedDependents) {
|
|
229
|
+
dependent.deref()?.update();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Periodically remove dead references.
|
|
233
|
+
this.derivedCells = this.derivedCells.filter(
|
|
234
|
+
(ref) => ref.deref() !== undefined
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// global effects
|
|
238
|
+
for (const [options, effect] of root.globalPostEffects) {
|
|
239
|
+
if (options.ignoreDerivedCells && this instanceof DerivedCell) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
effect(this.wvalue);
|
|
244
|
+
|
|
245
|
+
if (options.runOnce) {
|
|
246
|
+
root.globalPostEffects = root.globalPostEffects.filter(
|
|
247
|
+
([_, e]) => e !== effect
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Returns the current value of the cell without registering a watcher.
|
|
255
|
+
* @returns {T} - The current value of the cell.
|
|
256
|
+
*/
|
|
257
|
+
peek() {
|
|
258
|
+
return this.wvalue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Adds a global effect that runs before any Cell is updated.
|
|
263
|
+
* @param {(value: unknown) => void} effect - The effect function.
|
|
264
|
+
* @param {Partial<import('./root.js').GlobalEffectOptions>} [options] - The options for the effect.
|
|
265
|
+
* @example
|
|
266
|
+
* ```
|
|
267
|
+
* import { Cell } from '@adbl/cells';
|
|
268
|
+
*
|
|
269
|
+
* const cell = Cell.source(0);
|
|
270
|
+
* Cell.beforeUpdate((value) => console.log(value));
|
|
271
|
+
*
|
|
272
|
+
* cell.value = 1; // prints 1
|
|
273
|
+
* cell.value = 2; // prints 2
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
static beforeUpdate = (effect, options) => {
|
|
277
|
+
root.globalPreEffects.push([options ?? {}, effect]);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Adds a global post-update effect to the Cell system.
|
|
282
|
+
* @param {(value: unknown) => void} effect - The effect function to add.
|
|
283
|
+
* @param {Partial<import('./root.js').GlobalEffectOptions>} [options] - Options for the effect.
|
|
284
|
+
* @example
|
|
285
|
+
* ```
|
|
286
|
+
* import { Cell } from '@adbl/cells';
|
|
287
|
+
*
|
|
288
|
+
* const effect = (value) => console.log(value);
|
|
289
|
+
* Cell.afterUpdate(effect);
|
|
290
|
+
*
|
|
291
|
+
* const cell = Cell.source(0);
|
|
292
|
+
* cell.value = 1; // prints 1
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
static afterUpdate = (effect, options) => {
|
|
296
|
+
root.globalPostEffects.push([options ?? {}, effect]);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
static removeGlobalEffects = () => {
|
|
300
|
+
root.globalPreEffects = [];
|
|
301
|
+
root.globalPostEffects = [];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Removes a global effect.
|
|
306
|
+
* @param {(value: unknown) => void} effect - The effect function added previously.
|
|
307
|
+
* @example
|
|
308
|
+
* ```
|
|
309
|
+
* import { Cell } from '@adbl/cells';
|
|
310
|
+
*
|
|
311
|
+
* const effect = (value) => console.log(value);
|
|
312
|
+
* Cell.beforeUpdate(effect);
|
|
313
|
+
*
|
|
314
|
+
* const cell = Cell.source(0);
|
|
315
|
+
* cell.value = 1; // prints 1
|
|
316
|
+
*
|
|
317
|
+
* Cell.removeGlobalEffect(effect);
|
|
318
|
+
*
|
|
319
|
+
* cell.value = 2; // prints nothing
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
static removeGlobalEffect = (effect) => {
|
|
323
|
+
root.globalPreEffects = root.globalPreEffects.filter(
|
|
324
|
+
([_, e]) => e !== effect
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* @template T
|
|
330
|
+
* Creates a new Cell instance with the provided value.
|
|
331
|
+
* @param {T} value - The value to be stored in the Cell.
|
|
332
|
+
* @param {Partial<CellOptions<T>>} [options] - The options for the cell.
|
|
333
|
+
* @returns {SourceCell<T>} A new Cell instance.
|
|
334
|
+
* ```
|
|
335
|
+
* import { Cell } from '@adbl/cells';
|
|
336
|
+
*
|
|
337
|
+
* const cell = Cell.source('Hello world');
|
|
338
|
+
* console.log(cell.value); // Hello world.
|
|
339
|
+
*
|
|
340
|
+
* cell.value = 'Greetings!';
|
|
341
|
+
* console.log(cell.value) // Greetings!
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
static source = (value, options) => new SourceCell(value, options);
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @template T
|
|
348
|
+
* Creates a new Derived instance with the provided callback function.
|
|
349
|
+
* @param {() => T} callback - The callback function to be used by the Derived instance.
|
|
350
|
+
* @returns {DerivedCell<T>} A new Derived instance.
|
|
351
|
+
* ```
|
|
352
|
+
* import { Cell } from '@adbl/cells';
|
|
353
|
+
*
|
|
354
|
+
* const cell = Cell.source(2);
|
|
355
|
+
* const derived = Cell.derived(() => cell.value * 2);
|
|
356
|
+
*
|
|
357
|
+
* console.log(derived.value); // 4
|
|
358
|
+
*
|
|
359
|
+
* cell.value = 3;
|
|
360
|
+
* console.log(derived.value); // 6
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
static derived = (callback) => new DerivedCell(callback);
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Batches all the effects created to run only once.
|
|
367
|
+
* @param {() => void} callback - The function to be executed in a batched manner.
|
|
368
|
+
*/
|
|
369
|
+
static batch = (callback) => {
|
|
370
|
+
root.batchNestingLevel++;
|
|
371
|
+
callback();
|
|
372
|
+
root.batchNestingLevel--;
|
|
373
|
+
if (root.batchNestingLevel === 0) {
|
|
374
|
+
for (const [effect, args] of root.batchedEffects) {
|
|
375
|
+
effect(...args);
|
|
376
|
+
}
|
|
377
|
+
root.batchedEffects = new Map();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Checks if the provided value is an instance of the Cell class.
|
|
383
|
+
* @param {any} value - The value to check.
|
|
384
|
+
* @returns {value is Cell<any>} True if the value is an instance of Cell, false otherwise.
|
|
385
|
+
*/
|
|
386
|
+
static isCell = (value) => value instanceof Cell;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @template T
|
|
390
|
+
* Flattens the provided value by returning the value if it is not a Cell instance, or the value of the Cell instance if it is.
|
|
391
|
+
* @param {T | Cell<T>} value - The value to be flattened.
|
|
392
|
+
* @returns {T} The flattened value.
|
|
393
|
+
*/
|
|
394
|
+
static flatten = (value) => {
|
|
395
|
+
// @ts-ignore:
|
|
396
|
+
return value instanceof Cell
|
|
397
|
+
? Cell.flatten(value.wvalue)
|
|
398
|
+
: Array.isArray(value)
|
|
399
|
+
? Cell.flattenArray(value)
|
|
400
|
+
: value instanceof Object
|
|
401
|
+
? Cell.flattenObject(value)
|
|
402
|
+
: value;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Flattens an array by applying the `flatten` function to each element.
|
|
407
|
+
* @template T
|
|
408
|
+
* @param {Array<T | Cell<T>>} array - The array to be flattened.
|
|
409
|
+
* @returns {Array<T>} A new array with the flattened elements.
|
|
410
|
+
*/
|
|
411
|
+
static flattenArray = (array) => array.map(Cell.flatten);
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Flattens an object by applying the `flatten` function to each value.
|
|
415
|
+
* @template {object} T
|
|
416
|
+
* @param {T} object - The object to be flattened.
|
|
417
|
+
* @returns {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} A new object with the flattened values.
|
|
418
|
+
*/
|
|
419
|
+
static flattenObject = (object) => {
|
|
420
|
+
const result =
|
|
421
|
+
/** @type {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} */ ({});
|
|
422
|
+
for (const [key, value] of Object.entries(object)) {
|
|
423
|
+
Reflect.set(result, key, Cell.flatten(value));
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Wraps an asynchronous function with managed state.
|
|
430
|
+
*
|
|
431
|
+
* @template X - The type of the input parameter for the getter function.
|
|
432
|
+
* @template Y - The type of the output returned by the getter function.
|
|
433
|
+
* @param {(input: X) => Promise<Y>} getter - A function that performs the asynchronous operation.
|
|
434
|
+
* @returns {AsyncRequestAtoms<X, Y>} An object containing cells for pending, data, and error states,
|
|
435
|
+
* as well as functions to run and reload the operation.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* const { pending, data, error, run, reload } = Cell.async(async (input) => {
|
|
439
|
+
* const response = await fetch(`https://example.com/api/data?input=${input}`);
|
|
440
|
+
* return response.json();
|
|
441
|
+
* });
|
|
442
|
+
*
|
|
443
|
+
* run('input');
|
|
444
|
+
*/
|
|
445
|
+
static async(getter) {
|
|
446
|
+
const pending = Cell.source(false);
|
|
447
|
+
const data = Cell.source(/** @type {Y | null} */ (null));
|
|
448
|
+
const error = Cell.source(/** @type {Error | null} */ (null));
|
|
449
|
+
|
|
450
|
+
/** @type {X | undefined} */
|
|
451
|
+
let initialInput = undefined;
|
|
452
|
+
|
|
453
|
+
async function run(input = initialInput) {
|
|
454
|
+
pending.value = true;
|
|
455
|
+
try {
|
|
456
|
+
initialInput = input;
|
|
457
|
+
const result = await getter(/** @type {X} */ (input));
|
|
458
|
+
data.value = result;
|
|
459
|
+
} catch (e) {
|
|
460
|
+
if (e instanceof Error) {
|
|
461
|
+
error.value = e;
|
|
462
|
+
} else {
|
|
463
|
+
throw e;
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
pending.value = false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @param {X} [newInput]
|
|
472
|
+
* @param {boolean} [changeLoadingState]
|
|
473
|
+
*/
|
|
474
|
+
async function reload(newInput, changeLoadingState = true) {
|
|
475
|
+
if (changeLoadingState) {
|
|
476
|
+
pending.value = true;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const result = await getter(
|
|
480
|
+
/** @type {X} */ (newInput ?? initialInput)
|
|
481
|
+
);
|
|
482
|
+
data.value = result;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
if (e instanceof Error) {
|
|
485
|
+
error.value = e;
|
|
486
|
+
} else {
|
|
487
|
+
throw e;
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
if (changeLoadingState) {
|
|
491
|
+
pending.value = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
pending,
|
|
498
|
+
data,
|
|
499
|
+
error,
|
|
500
|
+
run,
|
|
501
|
+
reload,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* A class that represents a computed value that depends on other reactive values.
|
|
508
|
+
* The computed value is automatically updated when any of its dependencies change.
|
|
509
|
+
* @template T
|
|
510
|
+
* @extends {Cell<T>}
|
|
511
|
+
*/
|
|
512
|
+
export class DerivedCell extends Cell {
|
|
513
|
+
/**
|
|
514
|
+
* @type {() => T}
|
|
515
|
+
* @protected
|
|
516
|
+
*/
|
|
517
|
+
computedFn;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* @param {() => T} computedFn - A function that generates the value of the computed.
|
|
521
|
+
*/
|
|
522
|
+
constructor(computedFn) {
|
|
523
|
+
super();
|
|
524
|
+
this.computedFn = computedFn;
|
|
525
|
+
activeComputedValues.push(this);
|
|
526
|
+
this.setValue(computedFn());
|
|
527
|
+
activeComputedValues.pop();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @readonly
|
|
532
|
+
*/
|
|
533
|
+
get value() {
|
|
534
|
+
return this.revalued;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @readonly
|
|
539
|
+
*/
|
|
540
|
+
set value(_) {
|
|
541
|
+
throw new Error('Cannot set a derived Cell value.');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Updates the current value with the result of the computed function.
|
|
546
|
+
*/
|
|
547
|
+
update() {
|
|
548
|
+
// global effects
|
|
549
|
+
for (const [options, effect] of root.globalPreEffects) {
|
|
550
|
+
if (options.ignoreDerivedCells) continue;
|
|
551
|
+
|
|
552
|
+
effect(this.wvalue);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (root.batchNestingLevel > 0) {
|
|
556
|
+
root.batchedEffects.set(() => this.setValue(this.computedFn()), []);
|
|
557
|
+
} else {
|
|
558
|
+
this.setValue(this.computedFn());
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
super.update();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* @template T
|
|
567
|
+
* @extends {Cell<T>}
|
|
568
|
+
*/
|
|
569
|
+
export class SourceCell extends Cell {
|
|
570
|
+
/** @type {Partial<CellOptions<T>>} */
|
|
571
|
+
options;
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Creates a new Cell with the provided value.
|
|
575
|
+
* @param {T} value
|
|
576
|
+
* @param {Partial<CellOptions<T>>} [options]
|
|
577
|
+
*/
|
|
578
|
+
constructor(value, options) {
|
|
579
|
+
super();
|
|
580
|
+
|
|
581
|
+
this.setValue(options?.shallowProxied ? value : this.proxy(value));
|
|
582
|
+
this.options = options ?? {};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
get value() {
|
|
586
|
+
return this.revalued;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Sets the value stored in the Cell and triggers an update.
|
|
591
|
+
* @param {T} value
|
|
592
|
+
*/
|
|
593
|
+
set value(value) {
|
|
594
|
+
if (this.options.immutable) {
|
|
595
|
+
throw new Error('Cannot set the value of an immutable cell.');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const oldValue = this.wvalue;
|
|
599
|
+
|
|
600
|
+
const isEqual = this.options.equals
|
|
601
|
+
? this.options.equals(oldValue, value)
|
|
602
|
+
: oldValue === value;
|
|
603
|
+
|
|
604
|
+
if (isEqual) return;
|
|
605
|
+
|
|
606
|
+
// global effects
|
|
607
|
+
for (const [options, effect] of root.globalPreEffects) {
|
|
608
|
+
effect(this.wvalue);
|
|
609
|
+
|
|
610
|
+
if (options.runOnce) {
|
|
611
|
+
root.globalPreEffects = root.globalPreEffects.filter(
|
|
612
|
+
([_, e]) => e !== effect
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
this.setValue(value);
|
|
618
|
+
this.update();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Proxies the provided value deeply, allowing it to be observed and updated.
|
|
623
|
+
* @template T
|
|
624
|
+
* @param {T} value - The value to be proxied.
|
|
625
|
+
* @returns {T} - The proxied value.
|
|
626
|
+
* @private
|
|
627
|
+
*/
|
|
628
|
+
proxy(value) {
|
|
629
|
+
if (typeof value !== 'object' || value === null) {
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return new Proxy(value, {
|
|
634
|
+
get: (target, prop) => {
|
|
635
|
+
this.revalued;
|
|
636
|
+
return this.proxy(Reflect.get(target, prop));
|
|
637
|
+
},
|
|
638
|
+
set: (target, prop, value) => {
|
|
639
|
+
Reflect.set(target, prop, value);
|
|
640
|
+
this.update();
|
|
641
|
+
return true;
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
package/library/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DerivedCell, SourceCell, Cell } from './classes.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a partial map of cells, where each key in the object type `T` is mapped to either a `Cell<T[key]>` or the raw type `T[key]`.
|
|
5
|
+
* This type can be used to represent a partial set of cells for an object, where some properties are represented as cells and others are the raw values.
|
|
6
|
+
* @template {object} T The object type whose properties are mapped to cells or raw values.
|
|
7
|
+
* @typedef {{ [key in keyof T]: Cell<T[key]> | T[key] }} PartialCellMap
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a full set of cells for an object,
|
|
12
|
+
* where all properties are represented as cells.
|
|
13
|
+
* @template {object} T The object type whose properties are mapped to cells.
|
|
14
|
+
* @typedef {{ [key in keyof T]: Cell<T[key]> }} CellMap
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { SourceCell, DerivedCell, Cell };
|
|
18
|
+
export default Cell;
|
package/library/root.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./classes.js').Cell<any>} Watchable
|
|
3
|
+
* @typedef {import('./classes.js').DerivedCell<any>} DerivedCell
|
|
4
|
+
*
|
|
5
|
+
* @typedef GlobalEffectOptions
|
|
6
|
+
* @property {boolean} runOnce - Whether the effect should be removed after the first run.
|
|
7
|
+
* @property {boolean} ignoreDerivedCells - Whether the effect should be run even if the cell is a derived cell.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const root = {
|
|
11
|
+
/**
|
|
12
|
+
* An array of global effects that run before a source Cell is updated.
|
|
13
|
+
* @type {[Partial<GlobalEffectOptions>, ((value: unknown) => void)][]}
|
|
14
|
+
*/
|
|
15
|
+
globalPreEffects: [],
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An array of global effects that run after a source Cell is updated.
|
|
19
|
+
* @type {[Partial<GlobalEffectOptions>, ((value: unknown) => void)][]}
|
|
20
|
+
*/
|
|
21
|
+
globalPostEffects: [],
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The nesting level of batch operations.
|
|
25
|
+
* This will prevent nested batch operations from triggering effects when they finish.
|
|
26
|
+
* @type {number}
|
|
27
|
+
*/
|
|
28
|
+
batchNestingLevel: 0,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A map of effect tuples to be executed in a batch.
|
|
32
|
+
* The key in each entry is the effect, and the value is the list of arguments call it with.
|
|
33
|
+
* All callbacks in this map will be executed only once in a batch.
|
|
34
|
+
* @type {Map<Function, any[]>}
|
|
35
|
+
*/
|
|
36
|
+
batchedEffects: new Map(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A value representing the computed values that are currently being calculated.
|
|
41
|
+
* It is an array so it can keep track of nested computed values.
|
|
42
|
+
* @type {DerivedCell[]}
|
|
43
|
+
*/
|
|
44
|
+
export const activeComputedValues = [];
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adbl/cells",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A simple implementation of reactive updates for JavaScript",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"module": "./index.js",
|
|
9
|
+
"typings": "./types/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"types": "tsc --p jsconfig.json"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/adebola-io/signals.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"reactive",
|
|
20
|
+
"signals",
|
|
21
|
+
"events"
|
|
22
|
+
],
|
|
23
|
+
"author": "Sefunmi Adebola Akomolafe",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/adebola-io/signals/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/adebola-io/signals#readme",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.4.5",
|
|
31
|
+
"vitest": "^1.6.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./library/index.js";
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
*/
|
|
4
|
+
export class Cell<T> {
|
|
5
|
+
/**
|
|
6
|
+
* Adds a global effect that runs before any Cell is updated.
|
|
7
|
+
* @param {(value: unknown) => void} effect - The effect function.
|
|
8
|
+
* @param {Partial<import('./root.js').GlobalEffectOptions>} [options] - The options for the effect.
|
|
9
|
+
* @example
|
|
10
|
+
* ```
|
|
11
|
+
* import { Cell } from '@adbl/cells';
|
|
12
|
+
*
|
|
13
|
+
* const cell = Cell.source(0);
|
|
14
|
+
* Cell.beforeUpdate((value) => console.log(value));
|
|
15
|
+
*
|
|
16
|
+
* cell.value = 1; // prints 1
|
|
17
|
+
* cell.value = 2; // prints 2
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
static beforeUpdate: (effect: (value: unknown) => void, options?: Partial<import("./root.js").GlobalEffectOptions> | undefined) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Adds a global post-update effect to the Cell system.
|
|
23
|
+
* @param {(value: unknown) => void} effect - The effect function to add.
|
|
24
|
+
* @param {Partial<import('./root.js').GlobalEffectOptions>} [options] - Options for the effect.
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* import { Cell } from '@adbl/cells';
|
|
28
|
+
*
|
|
29
|
+
* const effect = (value) => console.log(value);
|
|
30
|
+
* Cell.afterUpdate(effect);
|
|
31
|
+
*
|
|
32
|
+
* const cell = Cell.source(0);
|
|
33
|
+
* cell.value = 1; // prints 1
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
static afterUpdate: (effect: (value: unknown) => void, options?: Partial<import("./root.js").GlobalEffectOptions> | undefined) => void;
|
|
37
|
+
static removeGlobalEffects: () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Removes a global effect.
|
|
40
|
+
* @param {(value: unknown) => void} effect - The effect function added previously.
|
|
41
|
+
* @example
|
|
42
|
+
* ```
|
|
43
|
+
* import { Cell } from '@adbl/cells';
|
|
44
|
+
*
|
|
45
|
+
* const effect = (value) => console.log(value);
|
|
46
|
+
* Cell.beforeUpdate(effect);
|
|
47
|
+
*
|
|
48
|
+
* const cell = Cell.source(0);
|
|
49
|
+
* cell.value = 1; // prints 1
|
|
50
|
+
*
|
|
51
|
+
* Cell.removeGlobalEffect(effect);
|
|
52
|
+
*
|
|
53
|
+
* cell.value = 2; // prints nothing
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
static removeGlobalEffect: (effect: (value: unknown) => void) => void;
|
|
57
|
+
/**
|
|
58
|
+
* @template T
|
|
59
|
+
* Creates a new Cell instance with the provided value.
|
|
60
|
+
* @param {T} value - The value to be stored in the Cell.
|
|
61
|
+
* @param {Partial<CellOptions<T>>} [options] - The options for the cell.
|
|
62
|
+
* @returns {SourceCell<T>} A new Cell instance.
|
|
63
|
+
* ```
|
|
64
|
+
* import { Cell } from '@adbl/cells';
|
|
65
|
+
*
|
|
66
|
+
* const cell = Cell.source('Hello world');
|
|
67
|
+
* console.log(cell.value); // Hello world.
|
|
68
|
+
*
|
|
69
|
+
* cell.value = 'Greetings!';
|
|
70
|
+
* console.log(cell.value) // Greetings!
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
static source: <T_1>(value: T_1, options?: Partial<CellOptions<T_1>> | undefined) => SourceCell<T_1>;
|
|
74
|
+
/**
|
|
75
|
+
* @template T
|
|
76
|
+
* Creates a new Derived instance with the provided callback function.
|
|
77
|
+
* @param {() => T} callback - The callback function to be used by the Derived instance.
|
|
78
|
+
* @returns {DerivedCell<T>} A new Derived instance.
|
|
79
|
+
* ```
|
|
80
|
+
* import { Cell } from '@adbl/cells';
|
|
81
|
+
*
|
|
82
|
+
* const cell = Cell.source(2);
|
|
83
|
+
* const derived = Cell.derived(() => cell.value * 2);
|
|
84
|
+
*
|
|
85
|
+
* console.log(derived.value); // 4
|
|
86
|
+
*
|
|
87
|
+
* cell.value = 3;
|
|
88
|
+
* console.log(derived.value); // 6
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
static derived: <T_2>(callback: () => T_2) => DerivedCell<T_2>;
|
|
92
|
+
/**
|
|
93
|
+
* Batches all the effects created to run only once.
|
|
94
|
+
* @param {() => void} callback - The function to be executed in a batched manner.
|
|
95
|
+
*/
|
|
96
|
+
static batch: (callback: () => void) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Checks if the provided value is an instance of the Cell class.
|
|
99
|
+
* @param {any} value - The value to check.
|
|
100
|
+
* @returns {value is Cell<any>} True if the value is an instance of Cell, false otherwise.
|
|
101
|
+
*/
|
|
102
|
+
static isCell: (value: any) => value is Cell<any>;
|
|
103
|
+
/**
|
|
104
|
+
* @template T
|
|
105
|
+
* Flattens the provided value by returning the value if it is not a Cell instance, or the value of the Cell instance if it is.
|
|
106
|
+
* @param {T | Cell<T>} value - The value to be flattened.
|
|
107
|
+
* @returns {T} The flattened value.
|
|
108
|
+
*/
|
|
109
|
+
static flatten: <T_3>(value: T_3 | Cell<T_3>) => T_3;
|
|
110
|
+
/**
|
|
111
|
+
* Flattens an array by applying the `flatten` function to each element.
|
|
112
|
+
* @template T
|
|
113
|
+
* @param {Array<T | Cell<T>>} array - The array to be flattened.
|
|
114
|
+
* @returns {Array<T>} A new array with the flattened elements.
|
|
115
|
+
*/
|
|
116
|
+
static flattenArray: <T_4>(array: (T_4 | Cell<T_4>)[]) => T_4[];
|
|
117
|
+
/**
|
|
118
|
+
* Flattens an object by applying the `flatten` function to each value.
|
|
119
|
+
* @template {object} T
|
|
120
|
+
* @param {T} object - The object to be flattened.
|
|
121
|
+
* @returns {{ [K in keyof T]: T[K] extends Cell<infer U> ? U : T[K] }} A new object with the flattened values.
|
|
122
|
+
*/
|
|
123
|
+
static flattenObject: <T_5 extends object>(object: T_5) => { [K in keyof T_5]: T_5[K] extends Cell<infer U> ? U : T_5[K]; };
|
|
124
|
+
/**
|
|
125
|
+
* Wraps an asynchronous function with managed state.
|
|
126
|
+
*
|
|
127
|
+
* @template X - The type of the input parameter for the getter function.
|
|
128
|
+
* @template Y - The type of the output returned by the getter function.
|
|
129
|
+
* @param {(input: X) => Promise<Y>} getter - A function that performs the asynchronous operation.
|
|
130
|
+
* @returns {AsyncRequestAtoms<X, Y>} An object containing cells for pending, data, and error states,
|
|
131
|
+
* as well as functions to run and reload the operation.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const { pending, data, error, run, reload } = Cell.async(async (input) => {
|
|
135
|
+
* const response = await fetch(`https://example.com/api/data?input=${input}`);
|
|
136
|
+
* return response.json();
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* run('input');
|
|
140
|
+
*/
|
|
141
|
+
static async<X, Y>(getter: (input: X) => Promise<Y>): AsyncRequestAtoms<X, Y>;
|
|
142
|
+
/**
|
|
143
|
+
* @type {Array<({
|
|
144
|
+
* effect: (newValue: T) => void,
|
|
145
|
+
* options?: EffectOptions,
|
|
146
|
+
* })>}
|
|
147
|
+
* @protected
|
|
148
|
+
*/
|
|
149
|
+
protected effects: Array<({
|
|
150
|
+
effect: (newValue: T) => void;
|
|
151
|
+
options?: EffectOptions;
|
|
152
|
+
})>;
|
|
153
|
+
/**
|
|
154
|
+
* @type {Array<WeakRef<DerivedCell<any>>>}
|
|
155
|
+
* @protected
|
|
156
|
+
*/
|
|
157
|
+
protected derivedCells: Array<WeakRef<DerivedCell<any>>>;
|
|
158
|
+
/**
|
|
159
|
+
* @protected @type T
|
|
160
|
+
*/
|
|
161
|
+
protected wvalue: T;
|
|
162
|
+
/**
|
|
163
|
+
* @protected
|
|
164
|
+
* @param {T} value
|
|
165
|
+
*/
|
|
166
|
+
protected setValue(value: T): void;
|
|
167
|
+
/**
|
|
168
|
+
* Overrides `Object.prototype.valueOf()` to return the value stored in the Cell.
|
|
169
|
+
* @returns {T} The value of the Cell.
|
|
170
|
+
*/
|
|
171
|
+
valueOf(): T;
|
|
172
|
+
/**
|
|
173
|
+
* The value stored in the Cell.
|
|
174
|
+
* @protected @type {T}
|
|
175
|
+
*/
|
|
176
|
+
protected get revalued(): T;
|
|
177
|
+
/**
|
|
178
|
+
* Sets a callback function that will be called whenever the value of the Cell changes.
|
|
179
|
+
* @param {(newValue: T) => void} callback - The function to be called when the value changes.
|
|
180
|
+
*/
|
|
181
|
+
set onchange(callback: (newValue: T) => void);
|
|
182
|
+
/**
|
|
183
|
+
* Adds the provided effect callback to the list of effects for this cell, and returns a function that can be called to remove the effect.
|
|
184
|
+
* @param {(newValue: T) => void} callback - The effect callback to add.
|
|
185
|
+
* @param {EffectOptions} [options] - The options for the effect.
|
|
186
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
187
|
+
*/
|
|
188
|
+
listen(callback: (newValue: T) => void, options?: EffectOptions | undefined): () => void;
|
|
189
|
+
/**
|
|
190
|
+
* Creates an effect that is immediately executed with the current value of the cell, and then added to the list of effects for the cell.
|
|
191
|
+
* @param {(newValue: T) => void} effect - The effect callback to add.
|
|
192
|
+
* @returns {() => void} A function that can be called to remove the effect.
|
|
193
|
+
*/
|
|
194
|
+
runAndListen(effect: (newValue: T) => void): () => void;
|
|
195
|
+
/**
|
|
196
|
+
* Removes the specified effect callback from the list of effects for this cell.
|
|
197
|
+
* @param {(newValue: T) => void} callback - The effect callback to remove.
|
|
198
|
+
*/
|
|
199
|
+
ignore(callback: (newValue: T) => void): void;
|
|
200
|
+
/**
|
|
201
|
+
* Checks if the cell is listening to a watcher with the specified name.
|
|
202
|
+
* @param {string} name - The name of the watcher to check for.
|
|
203
|
+
* @returns {boolean} `true` if the cell is listening to a watcher with the specified name, `false` otherwise.
|
|
204
|
+
*/
|
|
205
|
+
isListeningTo(name: string): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Updates the root object and notifies any registered watchers and computed dependents.
|
|
208
|
+
* This method is called whenever the root object's value changes.
|
|
209
|
+
*/
|
|
210
|
+
update(): void;
|
|
211
|
+
/**
|
|
212
|
+
* Returns the current value of the cell without registering a watcher.
|
|
213
|
+
* @returns {T} - The current value of the cell.
|
|
214
|
+
*/
|
|
215
|
+
peek(): T;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A class that represents a computed value that depends on other reactive values.
|
|
219
|
+
* The computed value is automatically updated when any of its dependencies change.
|
|
220
|
+
* @template T
|
|
221
|
+
* @extends {Cell<T>}
|
|
222
|
+
*/
|
|
223
|
+
export class DerivedCell<T> extends Cell<T> {
|
|
224
|
+
/**
|
|
225
|
+
* @param {() => T} computedFn - A function that generates the value of the computed.
|
|
226
|
+
*/
|
|
227
|
+
constructor(computedFn: () => T);
|
|
228
|
+
/**
|
|
229
|
+
* @type {() => T}
|
|
230
|
+
* @protected
|
|
231
|
+
*/
|
|
232
|
+
protected computedFn: () => T;
|
|
233
|
+
/**
|
|
234
|
+
* @readonly
|
|
235
|
+
*/
|
|
236
|
+
readonly set value(_: T);
|
|
237
|
+
/**
|
|
238
|
+
* @readonly
|
|
239
|
+
*/
|
|
240
|
+
readonly get value(): T;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* @template T
|
|
244
|
+
* @extends {Cell<T>}
|
|
245
|
+
*/
|
|
246
|
+
export class SourceCell<T> extends Cell<T> {
|
|
247
|
+
/**
|
|
248
|
+
* Creates a new Cell with the provided value.
|
|
249
|
+
* @param {T} value
|
|
250
|
+
* @param {Partial<CellOptions<T>>} [options]
|
|
251
|
+
*/
|
|
252
|
+
constructor(value: T, options?: Partial<CellOptions<T>> | undefined);
|
|
253
|
+
/** @type {Partial<CellOptions<T>>} */
|
|
254
|
+
options: Partial<CellOptions<T>>;
|
|
255
|
+
/**
|
|
256
|
+
* Sets the value stored in the Cell and triggers an update.
|
|
257
|
+
* @param {T} value
|
|
258
|
+
*/
|
|
259
|
+
set value(value: T);
|
|
260
|
+
get value(): T;
|
|
261
|
+
/**
|
|
262
|
+
* Proxies the provided value deeply, allowing it to be observed and updated.
|
|
263
|
+
* @template T
|
|
264
|
+
* @param {T} value - The value to be proxied.
|
|
265
|
+
* @returns {T} - The proxied value.
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
private proxy;
|
|
269
|
+
}
|
|
270
|
+
export type AsyncRequestAtoms<Input, Output> = {
|
|
271
|
+
/**
|
|
272
|
+
* Represents the loading state of an asynchronous request.
|
|
273
|
+
*/
|
|
274
|
+
pending: SourceCell<boolean>;
|
|
275
|
+
/**
|
|
276
|
+
* Represents the data returned by the asynchronous request.
|
|
277
|
+
*/
|
|
278
|
+
data: SourceCell<Output | null>;
|
|
279
|
+
/**
|
|
280
|
+
* Represents the errors returned by the asynchronous request, if any.
|
|
281
|
+
*/
|
|
282
|
+
error: SourceCell<Error | null>;
|
|
283
|
+
/**
|
|
284
|
+
* Triggers the asynchronous request.
|
|
285
|
+
*/
|
|
286
|
+
run: NeverIfAny<Input> extends never ? (input?: Input) => Promise<void> : (input: Input) => Promise<void>;
|
|
287
|
+
/**
|
|
288
|
+
* Triggers the asynchronous request again with an optional new input and optionally changes the loading state.
|
|
289
|
+
*/
|
|
290
|
+
reload: (newInput?: Input, changeLoadingState?: boolean) => Promise<void>;
|
|
291
|
+
};
|
|
292
|
+
export type EffectOptions = {
|
|
293
|
+
/**
|
|
294
|
+
* Whether the effect should be removed after the first run.
|
|
295
|
+
*/
|
|
296
|
+
once?: boolean | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* An AbortSignal to be used to ignore the effect if it is aborted.
|
|
299
|
+
*/
|
|
300
|
+
signal?: AbortSignal | undefined;
|
|
301
|
+
/**
|
|
302
|
+
* The name of the effect for debugging purposes.
|
|
303
|
+
*/
|
|
304
|
+
name?: string | undefined;
|
|
305
|
+
/**
|
|
306
|
+
* The priority of the effect. Higher priority effects are executed first. The default priority is 0.
|
|
307
|
+
*/
|
|
308
|
+
priority?: number | undefined;
|
|
309
|
+
};
|
|
310
|
+
export type CellOptions<T> = {
|
|
311
|
+
/**
|
|
312
|
+
* Whether the cell should be immutable. If set to true, the cell will not allow updates and will throw an error if the value is changed.
|
|
313
|
+
*/
|
|
314
|
+
immutable?: boolean | undefined;
|
|
315
|
+
/**
|
|
316
|
+
* Whether the cell's value should be shallowly proxied. If set to true, the cell will only proxy the top-level properties of the value, preventing any changes to nested properties. This can be useful for performance optimizations.
|
|
317
|
+
*/
|
|
318
|
+
shallowProxied?: boolean | undefined;
|
|
319
|
+
/**
|
|
320
|
+
* A function that determines whether two values are equal. If not provided, the default equality function will be used.
|
|
321
|
+
*/
|
|
322
|
+
equals?: ((oldValue: T, newValue: T) => boolean) | undefined;
|
|
323
|
+
};
|
|
324
|
+
export type NeverIfAny<T> = 0 extends (1 & T) ? never : T;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default Cell;
|
|
2
|
+
/**
|
|
3
|
+
* Represents a partial map of cells, where each key in the object type `T` is mapped to either a `Cell<T[key]>` or the raw type `T[key]`.
|
|
4
|
+
* This type can be used to represent a partial set of cells for an object, where some properties are represented as cells and others are the raw values.
|
|
5
|
+
*/
|
|
6
|
+
export type PartialCellMap<T extends object> = { [key in keyof T]: T[key] | Cell<T[key]>; };
|
|
7
|
+
/**
|
|
8
|
+
* Represents a full set of cells for an object,
|
|
9
|
+
* where all properties are represented as cells.
|
|
10
|
+
*/
|
|
11
|
+
export type CellMap<T extends object> = { [key in keyof T]: Cell<T[key]>; };
|
|
12
|
+
import { SourceCell } from './classes.js';
|
|
13
|
+
import { DerivedCell } from './classes.js';
|
|
14
|
+
import { Cell } from './classes.js';
|
|
15
|
+
export { SourceCell, DerivedCell, Cell };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export namespace root {
|
|
2
|
+
let globalPreEffects: [Partial<GlobalEffectOptions>, ((value: unknown) => void)][];
|
|
3
|
+
let globalPostEffects: [Partial<GlobalEffectOptions>, ((value: unknown) => void)][];
|
|
4
|
+
let batchNestingLevel: number;
|
|
5
|
+
let batchedEffects: Map<Function, any[]>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A value representing the computed values that are currently being calculated.
|
|
9
|
+
* It is an array so it can keep track of nested computed values.
|
|
10
|
+
* @type {DerivedCell[]}
|
|
11
|
+
*/
|
|
12
|
+
export const activeComputedValues: import("./classes.js").DerivedCell<any>[];
|
|
13
|
+
export type Watchable = import('./classes.js').Cell<any>;
|
|
14
|
+
export type DerivedCell = import('./classes.js').DerivedCell<any>;
|
|
15
|
+
export type GlobalEffectOptions = {
|
|
16
|
+
/**
|
|
17
|
+
* - Whether the effect should be removed after the first run.
|
|
18
|
+
*/
|
|
19
|
+
runOnce: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* - Whether the effect should be run even if the cell is a derived cell.
|
|
22
|
+
*/
|
|
23
|
+
ignoreDerivedCells: boolean;
|
|
24
|
+
};
|