@cognite/signal-utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/dist/core/array-signal.d.ts +61 -0
- package/dist/core/array-signal.js +36 -0
- package/dist/core/batch.d.ts +23 -0
- package/dist/core/batch.js +7 -0
- package/dist/core/computed.d.ts +19 -0
- package/dist/core/computed.js +11 -0
- package/dist/core/effect.d.ts +34 -0
- package/dist/core/effect.js +7 -0
- package/dist/core/signal.d.ts +88 -0
- package/dist/core/signal.js +19 -0
- package/dist/core/untracked.d.ts +28 -0
- package/dist/core/untracked.js +7 -0
- package/dist/extensions/fragmentSignal.d.ts +89 -0
- package/dist/extensions/fragmentSignal.js +21 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/misc/debounced-computed.d.ts +25 -0
- package/dist/misc/debounced-computed.js +17 -0
- package/dist/react/use-signal-value.d.ts +9 -0
- package/dist/react/use-signal-value.js +7 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +2 -0
- package/dist/utils/arrays.d.ts +30 -0
- package/dist/utils/arrays.js +17 -0
- package/dist/utils/debounce.d.ts +10 -0
- package/dist/utils/debounce.js +12 -0
- package/dist/utils/repeat.d.ts +7 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Signals
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This package is developed and published for Cognite's internal use. It is made publicly available on npm as a
|
|
5
|
+
> side effect of our internal tooling, but no support, stability, or continued availability is guaranteed for
|
|
6
|
+
> external users. Cognite makes no warranty regarding this package and accepts no liability for any security
|
|
7
|
+
> vulnerabilities, defects, or issues arising from its use, by internal or external consumers.
|
|
8
|
+
|
|
9
|
+
This is a library for dealing with signals. A signal contains a value, and you can be notified of changes to the value. Signal is not a new concept and has been in the frontend space for some time. There is even [a proposal](https://github.com/tc39/proposal-signals) to make signals part of the language. Several frontend frameworks use signals in some shape or form. This library is a thin wrapper on top of [Preact Signals](https://github.com/preactjs/signals). We want to control the signals interface, and we hope that we can change the underlying implementation in the future with a native solution.
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### `signal(value?)`
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { signal } from '@cognite/signal-utils';
|
|
17
|
+
|
|
18
|
+
// Create a signal with an initial value
|
|
19
|
+
const mySignal = signal(42); // Will have the type Signal<number>
|
|
20
|
+
console.log(mySignal()); // 42
|
|
21
|
+
mySignal(43);
|
|
22
|
+
console.log(mySignal()); // 43
|
|
23
|
+
|
|
24
|
+
// Create a signal without an initial value
|
|
25
|
+
const mySignal = signal<string>(); // Will have the type Signal<string | undefined>
|
|
26
|
+
console.log(mySignal()); // undefined
|
|
27
|
+
mySignal('hello');
|
|
28
|
+
console.log(mySignal()); // 'hello'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### `signal.peek()`
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { signal } from '@cognite/signal-utils';
|
|
35
|
+
|
|
36
|
+
// Peek the value without creating a dependency
|
|
37
|
+
const mySignal = signal(42);
|
|
38
|
+
mySignal.peek(); // 42
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `computed(fn)`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { signal, computed } from '@cognite/signal-utils';
|
|
45
|
+
|
|
46
|
+
const myNumber = signal(42);
|
|
47
|
+
const double = computed(() => myNumber() * 2);
|
|
48
|
+
double() // 84
|
|
49
|
+
myNumber(43);
|
|
50
|
+
double() // 86
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `effect(fn)`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { signal, effect } from '@cognite/signal-utils';
|
|
57
|
+
|
|
58
|
+
const mySignal = signal(42);
|
|
59
|
+
effect(() => {
|
|
60
|
+
console.log(mySignal());
|
|
61
|
+
});
|
|
62
|
+
// 42 is logged
|
|
63
|
+
mySignal(43);
|
|
64
|
+
// 43 is logged
|
|
65
|
+
|
|
66
|
+
// Cleanup
|
|
67
|
+
effect(() => {
|
|
68
|
+
addEventListener('click', handler);
|
|
69
|
+
return () => { removeEventListener('click', handler); };
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### `batch(fn)`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { signal, computed, batch, effect } from '@cognite/signal-utils';
|
|
78
|
+
|
|
79
|
+
const givenName = signal('Jane');
|
|
80
|
+
const familyName = signal('Doe');
|
|
81
|
+
const fullName = computed(() => givenName() + ' ' + familyName());
|
|
82
|
+
|
|
83
|
+
effect(() => console.log(fullName()));
|
|
84
|
+
// "Jane Doe" logged
|
|
85
|
+
|
|
86
|
+
// Combines both signal writes into one atomic update.
|
|
87
|
+
batch(() => {
|
|
88
|
+
givenName('Foo');
|
|
89
|
+
familyName('Bar');
|
|
90
|
+
});
|
|
91
|
+
// "Foo Bar" logged
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `untracked(fn)`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { signal, effect, untracked } from '@cognite/signal-utils';
|
|
98
|
+
|
|
99
|
+
const signalA = signal('A');
|
|
100
|
+
const signalB = signal('B');
|
|
101
|
+
|
|
102
|
+
function someFunction() {
|
|
103
|
+
console.log(signalB());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
effect(() => {
|
|
107
|
+
console.log(signalA());
|
|
108
|
+
untracked(someFunction);
|
|
109
|
+
})
|
|
110
|
+
// A & B is logged
|
|
111
|
+
|
|
112
|
+
signalA('A2');
|
|
113
|
+
// A2 & B is logged
|
|
114
|
+
|
|
115
|
+
signalB('B2');
|
|
116
|
+
// Nothing is logged as we didn't take a dependency on signalB
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `arraySignal(value=[])`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { arraySignal } from '@cognite/signal-utils';
|
|
123
|
+
|
|
124
|
+
const myArraySignal = arraySignal([1, 2, 3]);
|
|
125
|
+
|
|
126
|
+
// Getter
|
|
127
|
+
const currentArray = myArraySignal();
|
|
128
|
+
|
|
129
|
+
// Setter
|
|
130
|
+
myArraySignal([4, 5, 6]);
|
|
131
|
+
|
|
132
|
+
// Push new values
|
|
133
|
+
myArraySignal.push(7, 8);
|
|
134
|
+
|
|
135
|
+
// Insert value at specific index
|
|
136
|
+
myArraySignal.insertAt(1, 9);
|
|
137
|
+
|
|
138
|
+
// Remove specific value
|
|
139
|
+
myArraySignal.remove(2);
|
|
140
|
+
|
|
141
|
+
// Remove value at specific index
|
|
142
|
+
myArraySignal.removeAt(0);
|
|
143
|
+
|
|
144
|
+
// Swap values at two indices
|
|
145
|
+
myArraySignal.swap(0, 1);
|
|
146
|
+
|
|
147
|
+
// Clear the array
|
|
148
|
+
myArraySignal.clear();
|
|
149
|
+
|
|
150
|
+
// Peek the current value without taking a dependency
|
|
151
|
+
const peekedArray = myArraySignal.peek();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `debouncedComputed(fn, [delayInMs], [immediate=false])`
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { signal, debouncedComputed } from '@cognite/signal-utils';
|
|
158
|
+
|
|
159
|
+
const mySignal = signal(42);
|
|
160
|
+
|
|
161
|
+
// `double` is a read-only signal (computed) which updates at a maximum frequency of once every 250 ms.
|
|
162
|
+
const double = debouncedComputed(() => mySignal() * 2, 250);
|
|
163
|
+
|
|
164
|
+
double(); // 84
|
|
165
|
+
|
|
166
|
+
mySignal(12);
|
|
167
|
+
double(); // 84
|
|
168
|
+
|
|
169
|
+
await sleep(250);
|
|
170
|
+
double(); // 24
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `fragmentSignal(key, defaultValue?, serialize?, deserialize?)`
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { fragmentSignal } from '@cognite/signal-utils/extensions';
|
|
177
|
+
|
|
178
|
+
// Create a signal that syncs with the URL hash fragment
|
|
179
|
+
const pageSignal = fragmentSignal('page', 1);
|
|
180
|
+
|
|
181
|
+
// Read the current value (from URL if present, otherwise default)
|
|
182
|
+
pageSignal(); // 1
|
|
183
|
+
|
|
184
|
+
// Update the value (also updates URL to #page=2)
|
|
185
|
+
pageSignal(2);
|
|
186
|
+
|
|
187
|
+
// Clean up when done
|
|
188
|
+
pageSignal.dispose();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## API (React)
|
|
192
|
+
|
|
193
|
+
### `useSignalValue(signal)`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { signal } from '@cognite/signal-utils';
|
|
197
|
+
import { useSignalValue } from '@cognite/signal-utils/react';
|
|
198
|
+
|
|
199
|
+
const mySignal = signal(42);
|
|
200
|
+
|
|
201
|
+
// React hook to read the signal value and keep it up-to-date.
|
|
202
|
+
// This works for any ReadonlySignal, including signal, computed, arraySignal.
|
|
203
|
+
const value = useSignalValue(mySignal); // 42
|
|
204
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ReadonlySignal, Signal } from './signal';
|
|
2
|
+
declare const arraySignalSymbol: unique symbol;
|
|
3
|
+
export type ReadonlyArraySignal<T> = ReadonlySignal<readonly T[]>;
|
|
4
|
+
export type ArraySignal<T> = Signal<readonly T[]> & {
|
|
5
|
+
push(...values: T[]): void;
|
|
6
|
+
insertAt(index: number, value: T): void;
|
|
7
|
+
remove(value: T): void;
|
|
8
|
+
removeAt(index: number): void;
|
|
9
|
+
swap(i: number, j: number): void;
|
|
10
|
+
clear(): void;
|
|
11
|
+
[arraySignalSymbol]: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates an `ArraySignal` which is a reactive signal for an array of values.
|
|
15
|
+
*
|
|
16
|
+
* @template T - The type of elements in the array.
|
|
17
|
+
* @param {readonly T[]} [value=[]] - The initial value of the array.
|
|
18
|
+
* @returns {ArraySignal<T>} A reactive signal for the array.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const myArraySignal = arraySignal([1, 2, 3]);
|
|
23
|
+
*
|
|
24
|
+
* // Getter
|
|
25
|
+
* const currentArray = myArraySignal();
|
|
26
|
+
*
|
|
27
|
+
* // Setter
|
|
28
|
+
* myArraySignal([4, 5, 6]);
|
|
29
|
+
*
|
|
30
|
+
* // Push new values
|
|
31
|
+
* myArraySignal.push(7, 8);
|
|
32
|
+
*
|
|
33
|
+
* // Insert value at specific index
|
|
34
|
+
* myArraySignal.insertAt(1, 9);
|
|
35
|
+
*
|
|
36
|
+
* // Remove specific value
|
|
37
|
+
* myArraySignal.remove(2);
|
|
38
|
+
*
|
|
39
|
+
* // Remove value at specific index
|
|
40
|
+
* myArraySignal.removeAt(0);
|
|
41
|
+
*
|
|
42
|
+
* // Swap values at two indices
|
|
43
|
+
* myArraySignal.swap(0, 1);
|
|
44
|
+
*
|
|
45
|
+
* // Clear the array
|
|
46
|
+
* myArraySignal.clear();
|
|
47
|
+
*
|
|
48
|
+
* // Peek the current value without taking a dependency
|
|
49
|
+
* const peekedArray = myArraySignal.peek();
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function arraySignal<T>(value?: readonly T[]): ArraySignal<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Checks if the given value is an ArraySignal.
|
|
55
|
+
*
|
|
56
|
+
* @template T - The type of elements in the array signal.
|
|
57
|
+
* @param value - The value to check.
|
|
58
|
+
* @returns A boolean indicating whether the value is an ArraySignal.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isArraySignal<T>(value: unknown): value is ArraySignal<T>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { insertAt as e, remove as t, removeAt as n, swap as r } from "../utils/arrays.js";
|
|
2
|
+
import { signal as i } from "./signal.js";
|
|
3
|
+
//#region libs/shared/signal-utils/src/core/array-signal.ts
|
|
4
|
+
var a = Symbol("ArraySignal");
|
|
5
|
+
function o(o = []) {
|
|
6
|
+
let s = i([...o]);
|
|
7
|
+
function c(e) {
|
|
8
|
+
if (arguments.length === 1) {
|
|
9
|
+
s([...e]);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
return s();
|
|
13
|
+
}
|
|
14
|
+
return c[a] = !0, c.push = (...e) => {
|
|
15
|
+
s([...s.peek(), ...e]);
|
|
16
|
+
}, c.insertAt = (t, n) => {
|
|
17
|
+
let r = [...s.peek()];
|
|
18
|
+
e(r, t, n), s(r);
|
|
19
|
+
}, c.remove = (e) => {
|
|
20
|
+
let n = [...s.peek()];
|
|
21
|
+
t(n, e), s(n);
|
|
22
|
+
}, c.removeAt = (e) => {
|
|
23
|
+
let t = [...s.peek()];
|
|
24
|
+
n(t, e), s(t);
|
|
25
|
+
}, c.swap = (e, t) => {
|
|
26
|
+
let n = [...s.peek()];
|
|
27
|
+
r(n, e, t), s(n);
|
|
28
|
+
}, c.clear = () => {
|
|
29
|
+
s([]);
|
|
30
|
+
}, c.peek = () => s.peek(), c.subscribe = (e) => s.subscribe(e), c;
|
|
31
|
+
}
|
|
32
|
+
function s(e) {
|
|
33
|
+
return typeof e == "function" && a in e;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { o as arraySignal, s as isArraySignal };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a function within a batch, ensuring that signal updates are batched together (atomic operation).
|
|
3
|
+
*
|
|
4
|
+
* @param fn - The function to be executed within the batch.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const givenName = signal('Jane');
|
|
9
|
+
* const familyName = signal('Doe');
|
|
10
|
+
* const fullName = computed(() => givenName() + ' ' + familyName());
|
|
11
|
+
*
|
|
12
|
+
* effect(() => console.log(fullName()));
|
|
13
|
+
* // "Jane Doe" logged
|
|
14
|
+
*
|
|
15
|
+
* // Combines both signal writes into one atomic update.
|
|
16
|
+
* batch(() => {
|
|
17
|
+
* givenName('Foo');
|
|
18
|
+
* familyName('Bar');
|
|
19
|
+
* });
|
|
20
|
+
* // "Foo Bar" logged
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function batch(fn: VoidFunction): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReadonlySignal } from './signal';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a computed signal that derives its value from the provided function.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of the value returned by the computed function.
|
|
6
|
+
* @param {() => T} fn - A function that returns the value for the computed signal.
|
|
7
|
+
* @returns {ReadonlySignal<T>} A readonly signal that provides the computed value.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const myNumber = signal(42);
|
|
12
|
+
* const double = computed(() => myNumber() * 2);
|
|
13
|
+
* double() // 84
|
|
14
|
+
*
|
|
15
|
+
* myNumber(43);
|
|
16
|
+
* double() // 86
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { computed as e } from "@preact/signals-core";
|
|
2
|
+
//#region libs/shared/signal-utils/src/core/computed.ts
|
|
3
|
+
function t(t) {
|
|
4
|
+
let n = e(t);
|
|
5
|
+
function r() {
|
|
6
|
+
return n.value;
|
|
7
|
+
}
|
|
8
|
+
return r.peek = () => n.peek(), r.subscribe = (e) => n.subscribe(e), r;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { t as computed };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls the provided function, first initially, and when a signal dependency changes.
|
|
3
|
+
*
|
|
4
|
+
* The provided function can optionally return a cleanup function that will run only once.
|
|
5
|
+
* It will run when the effect is disposed or right before the next invocation of provided function.
|
|
6
|
+
*
|
|
7
|
+
* @param fn
|
|
8
|
+
* @returns A method to dispose the effect.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const mySignal = signal(42);
|
|
13
|
+
* effect(() => {
|
|
14
|
+
* console.log(mySignal());
|
|
15
|
+
* });
|
|
16
|
+
* // 42 is logged
|
|
17
|
+
*
|
|
18
|
+
* mySignal(43);
|
|
19
|
+
* // 43 is logged
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Cleanup
|
|
25
|
+
* effect(() => {
|
|
26
|
+
* addEventListener('click', handler);
|
|
27
|
+
* return () => { removeEventListener('click', handler); };
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function effect(fn: EffectFn): Dispose;
|
|
32
|
+
type Dispose = VoidFunction;
|
|
33
|
+
type EffectFn = () => void | Dispose;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
declare const signalSymbol: unique symbol;
|
|
2
|
+
/**
|
|
3
|
+
* Represents a readonly signal that can be used to get the current value.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of the signal value.
|
|
6
|
+
*/
|
|
7
|
+
export type ReadonlySignal<T> = {
|
|
8
|
+
(): T;
|
|
9
|
+
/**
|
|
10
|
+
* Let you peek the signal value without taking dependency on the signal.
|
|
11
|
+
* Peeking is useful in scenarios where you want to read the value, but not update on value changes.
|
|
12
|
+
*
|
|
13
|
+
* @returns The current signal value.
|
|
14
|
+
*/
|
|
15
|
+
peek(): T;
|
|
16
|
+
/**
|
|
17
|
+
* Subscribes to value changes. The provided function will be called once initially with the current value.
|
|
18
|
+
* @param fn The provided function will be invoked with a single argument, the newest signal value.
|
|
19
|
+
* @returns A function to unsubscribe.
|
|
20
|
+
*/
|
|
21
|
+
subscribe: (fn: Subscriber<T>) => Unsubscribe;
|
|
22
|
+
};
|
|
23
|
+
export type Subscriber<T> = (value: T) => void;
|
|
24
|
+
export type Unsubscribe = VoidFunction;
|
|
25
|
+
/**
|
|
26
|
+
* Represents a signal that can be used to get and set the current value.
|
|
27
|
+
*
|
|
28
|
+
* @template T - The type of the signal value.
|
|
29
|
+
*/
|
|
30
|
+
export type Signal<T> = ReadonlySignal<T> & {
|
|
31
|
+
(value: T): void;
|
|
32
|
+
[signalSymbol]?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new signal with an optional initial value.
|
|
36
|
+
*
|
|
37
|
+
* A signal is a reactive primitive that holds a value and can be used to track changes to that value.
|
|
38
|
+
* The signal can be read by calling it as a function, and it can be updated by calling it with a new value.
|
|
39
|
+
* The `peek` method can be used to read the current value without taking dependency on it.
|
|
40
|
+
*
|
|
41
|
+
* @template T - The type of the signal value.
|
|
42
|
+
* @param {T} [value] - The initial value of the signal.
|
|
43
|
+
* @returns {Signal<T | undefined> | Signal<T>} - The created signal.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* // Create a signal with an initial value
|
|
48
|
+
* const mySignal = signal(42); // Will have the type Signal<number>
|
|
49
|
+
* console.log(mySignal()); // 42
|
|
50
|
+
* mySignal(43);
|
|
51
|
+
* console.log(mySignal()); // 43
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* // Create a signal without an initial value
|
|
57
|
+
* const mySignal = signal<string>(); // Will have the type Signal<string | undefined>
|
|
58
|
+
* console.log(mySignal()); // undefined
|
|
59
|
+
* mySignal('hello');
|
|
60
|
+
* console.log(mySignal()); // 'hello'
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // Peek the value without creating a dependency
|
|
66
|
+
* const mySignal = signal(42);
|
|
67
|
+
* mySignal.peek(); // 42
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* // Subscribe to value changes
|
|
73
|
+
* const mySignal = signal(42);
|
|
74
|
+
* mySignal.subscribe(console.log); // Logs 42
|
|
75
|
+
* mySignal(43); // Logs 43
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function signal<T>(): Signal<T | undefined>;
|
|
79
|
+
export declare function signal<T>(value: T): Signal<T>;
|
|
80
|
+
/**
|
|
81
|
+
* Checks if the given value is a Signal.
|
|
82
|
+
*
|
|
83
|
+
* @template T - The value type of the signal.
|
|
84
|
+
* @param value - The value to check.
|
|
85
|
+
* @returns A boolean indicating whether the value is a Signal.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isSignal<T>(value: unknown): value is Signal<T>;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { signal as e } from "@preact/signals-core";
|
|
2
|
+
//#region libs/shared/signal-utils/src/core/signal.ts
|
|
3
|
+
var t = Symbol("signal");
|
|
4
|
+
function n(n) {
|
|
5
|
+
let r = e(n);
|
|
6
|
+
function i(e) {
|
|
7
|
+
if (arguments.length === 1) {
|
|
8
|
+
r.value = e;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
return r.value;
|
|
12
|
+
}
|
|
13
|
+
return i[t] = !0, i.peek = () => r.peek(), i.subscribe = (e) => r.subscribe(e), i;
|
|
14
|
+
}
|
|
15
|
+
function r(e) {
|
|
16
|
+
return typeof e == "function" && t in e;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { r as isSignal, n as signal };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls the provided function, without taking dependency on any signals called within it.
|
|
3
|
+
* @param fn
|
|
4
|
+
* @returns The value returned from `fn`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const signalA = signal('A');
|
|
9
|
+
* const signalB = signal('B');
|
|
10
|
+
*
|
|
11
|
+
* function someFunction() {
|
|
12
|
+
* console.log(signalB());
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* effect(() => {
|
|
16
|
+
* console.log(signalA());
|
|
17
|
+
* untracked(someFunction);
|
|
18
|
+
* })
|
|
19
|
+
* // A & B is logged
|
|
20
|
+
*
|
|
21
|
+
* signalA('A2');
|
|
22
|
+
* // A2 & B is logged
|
|
23
|
+
*
|
|
24
|
+
* signalB('B2');
|
|
25
|
+
* // Nothing is logged as we didn't take a dependency on signalB
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function untracked<T>(fn: () => T): T;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Signal } from '../core/signal';
|
|
2
|
+
export type FragmentSignal<T> = Signal<T> & {
|
|
3
|
+
dispose: () => void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Creates a signal that synchronizes its value with the URL hash fragment.
|
|
7
|
+
*
|
|
8
|
+
* This function creates a reactive signal that automatically reads from and writes to
|
|
9
|
+
* the URL hash fragment (the part after `#`). When the signal value changes, the URL
|
|
10
|
+
* hash is updated. When the URL hash changes (e.g., via browser navigation), the
|
|
11
|
+
* signal value is updated accordingly.
|
|
12
|
+
*
|
|
13
|
+
* @template T - The type of the value stored in the signal
|
|
14
|
+
*
|
|
15
|
+
* @param key - The parameter name to use in the URL hash fragment
|
|
16
|
+
* @param defaultValue - The default value to use when the URL parameter is not present
|
|
17
|
+
* @param serialize - Optional function to serialize the value to a string. Defaults to `JSON.stringify`
|
|
18
|
+
* @param deserialize - Optional function to deserialize a string to a value. Defaults to `JSON.parse`
|
|
19
|
+
*
|
|
20
|
+
* @returns A signal that synchronizes with the URL hash fragment, including a `dispose()` method to clean up event listeners
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Basic usage with a default value
|
|
25
|
+
* const pageSignal = fragmentSignal('page', 1);
|
|
26
|
+
*
|
|
27
|
+
* // Read the current value
|
|
28
|
+
* console.log(pageSignal()); // 1 (or value from URL if present)
|
|
29
|
+
*
|
|
30
|
+
* // Update the value (also updates URL to #page=2)
|
|
31
|
+
* pageSignal(2);
|
|
32
|
+
*
|
|
33
|
+
* // Clear the value (removes from URL and resets to default)
|
|
34
|
+
* pageSignal(null);
|
|
35
|
+
*
|
|
36
|
+
* // Clean up when done
|
|
37
|
+
* pageSignal.dispose();
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // Usage without default value (signal will be undefined if not in URL)
|
|
43
|
+
* const optionalSignal = fragmentSignal<string>('filter');
|
|
44
|
+
*
|
|
45
|
+
* console.log(optionalSignal()); // undefined (if not in URL)
|
|
46
|
+
* optionalSignal('active'); // Sets URL to #filter=active
|
|
47
|
+
* optionalSignal(undefined); // Removes from URL
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* // Custom serialization for complex objects
|
|
53
|
+
* interface Filter {
|
|
54
|
+
* category: string;
|
|
55
|
+
* priceRange: [number, number];
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* const filterSignal = fragmentSignal<Filter>(
|
|
59
|
+
* 'filter',
|
|
60
|
+
* { category: 'all', priceRange: [0, 100] },
|
|
61
|
+
* (filter) => btoa(JSON.stringify(filter)), // Base64 encode
|
|
62
|
+
* (str) => JSON.parse(atob(str)) // Base64 decode
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Multiple fragment signals can coexist
|
|
69
|
+
* const searchSignal = fragmentSignal('q', '');
|
|
70
|
+
* const sortSignal = fragmentSignal('sort', 'name');
|
|
71
|
+
*
|
|
72
|
+
* searchSignal('react');
|
|
73
|
+
* sortSignal('date');
|
|
74
|
+
* // URL becomes: #q=react&sort=date
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* - The signal automatically listens to `hashchange` events and updates accordingly
|
|
79
|
+
* - Setting the signal to `null` or `undefined` resets the parameter to the default value.
|
|
80
|
+
* If it is initialized without a default value, then it will remove the hash key from the URL.
|
|
81
|
+
* - The URL hash uses `URLSearchParams` format for encoding multiple parameters
|
|
82
|
+
* - The signal persists across page reloads as long as the URL hash remains
|
|
83
|
+
* - Call `dispose()` on the returned signal to remove the `hashchange` event listener when no longer needed
|
|
84
|
+
* - Two overloads are available:
|
|
85
|
+
* 1. With default value: `fragmentSignal(key, defaultValue, serialize?, deserialize?)` - returns `Signal<T>`
|
|
86
|
+
* 2. Without default value: `fragmentSignal(key, serialize?, deserialize?)` - returns `Signal<T | undefined>`
|
|
87
|
+
*/
|
|
88
|
+
export declare function fragmentSignal<T extends NonNullable<unknown>>(key: string, defaultValue: T, serialize?: (value: T) => string, deserialize?: (hashString: string) => T): FragmentSignal<T>;
|
|
89
|
+
export declare function fragmentSignal<T>(key: string, serialize?: (value: T) => string, deserialize?: (hashString: string) => T): FragmentSignal<T | undefined | null>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { signal as e } from "../core/signal.js";
|
|
2
|
+
//#region libs/shared/signal-utils/src/extensions/fragmentSignal.ts
|
|
3
|
+
function t(t, n, r = JSON.stringify, i = JSON.parse) {
|
|
4
|
+
let a = u().get(t), o = a === null ? n : i(a), s = e(o), c = new AbortController();
|
|
5
|
+
window.addEventListener("hashchange", () => {
|
|
6
|
+
let e = u().get(t), r = e === null ? n : i(e);
|
|
7
|
+
s(r);
|
|
8
|
+
}, { signal: c.signal });
|
|
9
|
+
let l = new Proxy(s, { apply: function(e, n, i) {
|
|
10
|
+
if (i.length === 0) return Reflect.apply(e, n, i);
|
|
11
|
+
let a = u(), o = i[0];
|
|
12
|
+
return o == null ? a.delete(t) : a.set(t, r(o)), window.location.hash = a.toString(), o;
|
|
13
|
+
} });
|
|
14
|
+
return Object.assign(l, { dispose: () => c.abort() });
|
|
15
|
+
function u() {
|
|
16
|
+
let e = window.location.hash.substring(1);
|
|
17
|
+
return new URLSearchParams(e);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { t as fragmentSignal };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fragmentSignal, type FragmentSignal, } from './extensions/fragmentSignal';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { arraySignal, isArraySignal } from './core/array-signal';
|
|
2
|
+
export { batch } from './core/batch';
|
|
3
|
+
export { computed } from './core/computed';
|
|
4
|
+
export { debouncedComputed } from './misc/debounced-computed';
|
|
5
|
+
export { effect } from './core/effect';
|
|
6
|
+
export { signal, isSignal } from './core/signal';
|
|
7
|
+
export { untracked } from './core/untracked';
|
|
8
|
+
export type { ArraySignal, ReadonlyArraySignal } from './core/array-signal';
|
|
9
|
+
export type { DebouncedComputed } from './misc/debounced-computed';
|
|
10
|
+
export type { Signal, ReadonlySignal } from './core/signal';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isSignal as e, signal as t } from "./core/signal.js";
|
|
2
|
+
import { arraySignal as n, isArraySignal as r } from "./core/array-signal.js";
|
|
3
|
+
import { batch as i } from "./core/batch.js";
|
|
4
|
+
import { computed as a } from "./core/computed.js";
|
|
5
|
+
import { effect as o } from "./core/effect.js";
|
|
6
|
+
import { debouncedComputed as s } from "./misc/debounced-computed.js";
|
|
7
|
+
import { untracked as c } from "./core/untracked.js";
|
|
8
|
+
export { n as arraySignal, i as batch, a as computed, s as debouncedComputed, o as effect, r as isArraySignal, e as isSignal, t as signal, c as untracked };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReadonlySignal } from '../core/signal';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a debounced computed value.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of the computed value.
|
|
6
|
+
*
|
|
7
|
+
* @property {ReadonlySignal<T>} value - The debounced computed value.
|
|
8
|
+
* @property {VoidFunction} dispose - A function to dispose updates to the computed value.
|
|
9
|
+
*/
|
|
10
|
+
export type DebouncedComputed<T> = {
|
|
11
|
+
value: ReadonlySignal<T>;
|
|
12
|
+
dispose: VoidFunction;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates a debounced computed value based on the provided function.
|
|
16
|
+
* The value will be set immediately, and debounce thereafter. This means the computed value will be updated at a maximum
|
|
17
|
+
* frequency of once every `delayInMs` milliseconds.
|
|
18
|
+
*
|
|
19
|
+
* @template T - The type of the computed value.
|
|
20
|
+
* @param {() => T} func - The function that returns the computed value.
|
|
21
|
+
* @param {number} [delayInMs] - The minimum amount of time that must pass between between each value update. If not provided, a default delay will be used.
|
|
22
|
+
* @param {boolean} [immediate=false] - If true, the function will be called at the start of the wait period, rather than the end.
|
|
23
|
+
* @returns {DebouncedComputed<T>} An object containing the debounced computed value and a dispose function.
|
|
24
|
+
*/
|
|
25
|
+
export declare function debouncedComputed<T>(func: () => T, delayInMs?: number, immediate?: boolean): DebouncedComputed<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { signal as e } from "../core/signal.js";
|
|
2
|
+
import { computed as t } from "../core/computed.js";
|
|
3
|
+
import { effect as n } from "../core/effect.js";
|
|
4
|
+
import { debounce as r } from "../utils/debounce.js";
|
|
5
|
+
//#region libs/shared/signal-utils/src/misc/debounced-computed.ts
|
|
6
|
+
function i(i, a, o = !1) {
|
|
7
|
+
let s = !1, c = e(void 0), l = r(c, a, o), u = t(i), d = n(() => {
|
|
8
|
+
let e = u();
|
|
9
|
+
s ? l(e) : (c(e), s = !0);
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
value: t(() => c()),
|
|
13
|
+
dispose: d
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { i as debouncedComputed };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReadonlySignal } from '../core/signal';
|
|
2
|
+
/**
|
|
3
|
+
* A custom hook that returns the signal's value and keeps the value up-to-date.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of the value held by the signal.
|
|
6
|
+
* @param {ReadonlySignal<T>} signal - The signal.
|
|
7
|
+
* @returns {T} - The current value of the signal.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useSignalValue<T>(signal: ReadonlySignal<T>): T;
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSignalValue } from './react/use-signal-value';
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inserts an element at a specific index in the array.
|
|
3
|
+
*
|
|
4
|
+
* @param array
|
|
5
|
+
* @param index
|
|
6
|
+
* @param item
|
|
7
|
+
*/
|
|
8
|
+
export declare function insertAt<T>(array: T[], index: number, item: T): void;
|
|
9
|
+
/**
|
|
10
|
+
* Removes an element from the array.
|
|
11
|
+
*
|
|
12
|
+
* @param array
|
|
13
|
+
* @param item
|
|
14
|
+
*/
|
|
15
|
+
export declare function remove<T>(array: T[], item: T): void;
|
|
16
|
+
/**
|
|
17
|
+
* Removes an element at the index from the array.
|
|
18
|
+
*
|
|
19
|
+
* @param array
|
|
20
|
+
* @param item
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeAt(array: unknown[], index: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* Swaps two elements in the array on index `i` and `j`.
|
|
25
|
+
*
|
|
26
|
+
* @param array
|
|
27
|
+
* @param i
|
|
28
|
+
* @param j
|
|
29
|
+
*/
|
|
30
|
+
export declare function swap(array: unknown[], i: number, j: number): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region libs/shared/signal-utils/src/utils/arrays.ts
|
|
2
|
+
function e(e, t, n) {
|
|
3
|
+
e.splice(t, 0, n);
|
|
4
|
+
}
|
|
5
|
+
function t(e, t) {
|
|
6
|
+
let r = e.indexOf(t);
|
|
7
|
+
r !== -1 && n(e, r);
|
|
8
|
+
}
|
|
9
|
+
function n(e, t) {
|
|
10
|
+
e.splice(t, 1);
|
|
11
|
+
}
|
|
12
|
+
function r(e, t, n) {
|
|
13
|
+
if (t < 0 || n < 0 || t >= e.length || n >= e.length) throw Error("Index out of bounds");
|
|
14
|
+
[e[t], e[n]] = [e[n], e[t]];
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { e as insertAt, t as remove, n as removeAt, r as swap };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a function in a debounce function that will only execute
|
|
3
|
+
* the provided function at a maximum frequency of once every `delayInMs` milliseconds.
|
|
4
|
+
*
|
|
5
|
+
* @param callback The function to debounce.
|
|
6
|
+
* @param delayInMs The minimum amount of time that must pass between calls to the function.
|
|
7
|
+
* @param immediate If true, the function will be called at the start of the wait period, rather than the end.
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export declare function debounce<T extends (...args: any[]) => void>(callback: T, delayInMs?: number, immediate?: boolean): <U>(this: U, ...args: Parameters<typeof callback>) => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region libs/shared/signal-utils/src/utils/debounce.ts
|
|
2
|
+
function e(e, t = 250, n = !1) {
|
|
3
|
+
let r = null;
|
|
4
|
+
return function(...i) {
|
|
5
|
+
let a = () => {
|
|
6
|
+
r = null, n || e.apply(this, i);
|
|
7
|
+
}, o = n && r === null;
|
|
8
|
+
r !== null && clearTimeout(r), r = setTimeout(a, t), o && e.apply(this, i);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { e as debounce };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repeats the execution of a given function a specified number of times.
|
|
3
|
+
*
|
|
4
|
+
* @param fn - The function to be executed repeatedly. It should not take any arguments and should not return any value.
|
|
5
|
+
* @param times - The number of times to execute the function.
|
|
6
|
+
*/
|
|
7
|
+
export declare function repeat(fn: VoidFunction, times: number): void;
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cognite/signal-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A thin, framework-agnostic wrapper around Preact Signals for reactive state management.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./react": {
|
|
17
|
+
"import": "./dist/react.js",
|
|
18
|
+
"types": "./dist/react.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./extensions": {
|
|
21
|
+
"import": "./dist/extensions.js",
|
|
22
|
+
"types": "./dist/extensions.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@preact/signals-core": "^1.8.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@nx/eslint-plugin": "22.7.8",
|
|
33
|
+
"@testing-library/react": "16.0.1",
|
|
34
|
+
"@types/react": "18.3.14",
|
|
35
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
36
|
+
"react": "^18.3.1",
|
|
37
|
+
"vite": "8.2.2",
|
|
38
|
+
"vitest": "^4.1.10"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": "^18.3.1"
|
|
42
|
+
}
|
|
43
|
+
}
|