@xyn-html/xyn-signal 0.1.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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/.gitkeep +0 -0
- package/dist/xyn_signal.js +581 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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,88 @@
|
|
|
1
|
+
# xyn-signal
|
|
2
|
+
|
|
3
|
+
Lightweight, reactive signal-based state management for JavaScript applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install xyn-signal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { createSignal, watch, timing } from 'xyn-signal';
|
|
15
|
+
|
|
16
|
+
// Create a signal
|
|
17
|
+
const count = createSignal(0);
|
|
18
|
+
|
|
19
|
+
// Subscribe to changes
|
|
20
|
+
count.subscribe((value) => {
|
|
21
|
+
console.log('Count changed:', value);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Update the value
|
|
25
|
+
count.value = 1;
|
|
26
|
+
count.value = 2;
|
|
27
|
+
|
|
28
|
+
// Object signals with reactive properties
|
|
29
|
+
const user = createSignal({ name: 'John', age: 30 });
|
|
30
|
+
user.value.name = 'Jane'; // Triggers subscribers
|
|
31
|
+
|
|
32
|
+
// Array signals with reactive methods
|
|
33
|
+
const items = createSignal([1, 2, 3]);
|
|
34
|
+
items.value.push(4); // Triggers subscribers
|
|
35
|
+
|
|
36
|
+
// Watch multiple signals
|
|
37
|
+
const firstName = createSignal('John');
|
|
38
|
+
const lastName = createSignal('Doe');
|
|
39
|
+
|
|
40
|
+
watch(firstName)
|
|
41
|
+
.watch(lastName)
|
|
42
|
+
.effect(() => {
|
|
43
|
+
console.log(`Full name: ${firstName.value} ${lastName.value}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Derived signals
|
|
47
|
+
const { signal: fullName } = watch(firstName)
|
|
48
|
+
.watch(lastName)
|
|
49
|
+
.derived(() => `${firstName.value} ${lastName.value}`);
|
|
50
|
+
|
|
51
|
+
// Timing functions
|
|
52
|
+
const debouncedEffect = timing(300).debounce(() => {
|
|
53
|
+
console.log('Debounced!');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
watch(firstName).effect(debouncedEffect);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### createSignal(initialValue)
|
|
62
|
+
|
|
63
|
+
Creates a reactive signal that can hold any value type.
|
|
64
|
+
|
|
65
|
+
- **Primitives**: Simple value storage with change detection
|
|
66
|
+
- **Objects**: Reactive property assignment and deletion
|
|
67
|
+
- **Arrays**: Reactive array methods (push, pop, shift, unshift, splice)
|
|
68
|
+
- **Map/Set**: Reactive collection operations
|
|
69
|
+
|
|
70
|
+
### watch(signal)
|
|
71
|
+
|
|
72
|
+
Creates a watcher for observing multiple signals.
|
|
73
|
+
|
|
74
|
+
- `.watch(signal)` - Chain additional signals
|
|
75
|
+
- `.effect(fn)` - Subscribe to all watched signals
|
|
76
|
+
- `.derived(fn, wrappingFn?)` - Create a derived signal
|
|
77
|
+
|
|
78
|
+
### timing(delay)
|
|
79
|
+
|
|
80
|
+
Creates timing-controlled function wrappers.
|
|
81
|
+
|
|
82
|
+
- `.debounce(fn)` - Execute after inactivity period
|
|
83
|
+
- `.throttle(fn)` - Limit execution rate
|
|
84
|
+
- `.delay(fn)` - Execute after fixed delay
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/dist/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview XynSignal is a library for creating reactive signals, it is
|
|
3
|
+
* used in XynHTML to render the data to HTML elements. It may also be used
|
|
4
|
+
* independently of XynHTML.
|
|
5
|
+
*
|
|
6
|
+
* @license MIT
|
|
7
|
+
* Copyright (c) 2024 XynHTML
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions:
|
|
15
|
+
*
|
|
16
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
* copies or substantial portions of the Software.
|
|
18
|
+
*
|
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
* SOFTWARE.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} XynChange
|
|
30
|
+
* @template T
|
|
31
|
+
* @property {T} value
|
|
32
|
+
* @property {T} previousValue
|
|
33
|
+
* @description XynChange is a class for storing the values of a change.
|
|
34
|
+
* @example
|
|
35
|
+
* const change = XynChange.create(1, 0);
|
|
36
|
+
* console.log(change.value); // 1
|
|
37
|
+
* console.log(change.previousValue); // 0
|
|
38
|
+
* console.log(change.values); // { value: 1, previousValue: 0 }
|
|
39
|
+
*/
|
|
40
|
+
class XynChange {
|
|
41
|
+
#value;
|
|
42
|
+
#previousValue;
|
|
43
|
+
|
|
44
|
+
constructor(value, previousValue) {
|
|
45
|
+
this.#value = value;
|
|
46
|
+
this.#previousValue = previousValue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static create(value, previousValue) {
|
|
50
|
+
return new XynChange(value, previousValue);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get value() {
|
|
54
|
+
return this.#value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get previousValue() {
|
|
58
|
+
return this.#previousValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get values() {
|
|
62
|
+
return {
|
|
63
|
+
value: this.#value,
|
|
64
|
+
previousValue: this.#previousValue,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} XynCollectionChange
|
|
71
|
+
* @template T
|
|
72
|
+
* @property {int} index
|
|
73
|
+
* @property {T} value
|
|
74
|
+
* @property {T} previousValue
|
|
75
|
+
* @description XynListChange is a class for storing the values of a collection change.
|
|
76
|
+
* @example
|
|
77
|
+
* const change = XynListChange.create(0, 1, 0);
|
|
78
|
+
* console.log(change.index); // 0
|
|
79
|
+
* console.log(change.value); // 1
|
|
80
|
+
* console.log(change.previousValue); // 0
|
|
81
|
+
* console.log(change.values); // { index: 0, value: 1, previousValue: 0 }
|
|
82
|
+
*/
|
|
83
|
+
class XynCollectionChange {
|
|
84
|
+
#index;
|
|
85
|
+
#value;
|
|
86
|
+
#previousValue;
|
|
87
|
+
|
|
88
|
+
constructor(index, value, previousValue) {
|
|
89
|
+
this.#index = index;
|
|
90
|
+
this.#value = value;
|
|
91
|
+
this.#previousValue = previousValue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static create(index, value, previousValue) {
|
|
95
|
+
return new XynCollectionChange(index, value, previousValue);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get index() {
|
|
99
|
+
return this.#index;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get value() {
|
|
103
|
+
return this.#value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get previousValue() {
|
|
107
|
+
return this.#previousValue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get values() {
|
|
111
|
+
return {
|
|
112
|
+
value: this.#value,
|
|
113
|
+
previousValue: this.#previousValue,
|
|
114
|
+
index: this.#index,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @class None
|
|
121
|
+
* @description None is a class for storing a value that doesn't exist.
|
|
122
|
+
* @example
|
|
123
|
+
* const none = new None("none");
|
|
124
|
+
* console.log(none.is(new None("none"))); // true
|
|
125
|
+
* console.log(none.is(new None("other"))); // false
|
|
126
|
+
* console.log(none.value); // "none"
|
|
127
|
+
*/
|
|
128
|
+
class None {
|
|
129
|
+
#value;
|
|
130
|
+
|
|
131
|
+
constructor(value) {
|
|
132
|
+
this.#value = value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
is(none) {
|
|
136
|
+
return none instanceof None && this.#value === none.value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get value() {
|
|
140
|
+
return this.#value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @enum {None}
|
|
146
|
+
* @readonly
|
|
147
|
+
* @description CollectionValue is an enum of sentinals for the values of a collection change that don't or didn't exist.
|
|
148
|
+
*/
|
|
149
|
+
export const CollectionValue = Object.freeze({
|
|
150
|
+
INSERT: new None("insert"),
|
|
151
|
+
DELETE: new None("delete"),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @template T
|
|
156
|
+
* @param {T} value
|
|
157
|
+
* @returns {{ value: T, subscribe: (subscriber: Function) => void }}
|
|
158
|
+
* @description Creates a signal with the given value.
|
|
159
|
+
* The signal can be subscribed to and will notify subscribers when the value changes.
|
|
160
|
+
* @example
|
|
161
|
+
* const signal = createSignal(0);
|
|
162
|
+
* signal.subscribe(({value, previousValue}) =>
|
|
163
|
+
* console.log(`Value changed from ${previousValue} to ${value}`)
|
|
164
|
+
* );
|
|
165
|
+
*/
|
|
166
|
+
export function createSignal(value) {
|
|
167
|
+
if (typeof value === "function") {
|
|
168
|
+
value = value();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (value != null) {
|
|
172
|
+
if (value instanceof Map || value instanceof Set) {
|
|
173
|
+
return createCollectionSignal(value);
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
return createListSignal(value);
|
|
177
|
+
}
|
|
178
|
+
if (typeof value === "object" && value !== null) {
|
|
179
|
+
return createObjectSignal(value);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const subscribers = new Set();
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @type {Object}
|
|
186
|
+
* @property {T} value
|
|
187
|
+
* @property {function(function({value: any, previousValue: any}): void): function(): void} subscribe
|
|
188
|
+
* @description Subscribes to the signal and returns a function to unsubscribe.
|
|
189
|
+
* The subscriber function is called with the current value and the previous value.
|
|
190
|
+
* The subscriber function is called immediately with the current value.
|
|
191
|
+
* @example
|
|
192
|
+
* const signal = createSignal(0);
|
|
193
|
+
* const unsubscribe = signal.subscribe(({value, previousValue}) =>
|
|
194
|
+
* console.log(`Value changed from ${previousValue} to ${value}`)
|
|
195
|
+
* );
|
|
196
|
+
*/
|
|
197
|
+
const signalProxy = new Proxy(
|
|
198
|
+
{
|
|
199
|
+
value,
|
|
200
|
+
subscribe(subscriber) {
|
|
201
|
+
subscribers.add(subscriber);
|
|
202
|
+
return () => subscribers.delete(subscriber);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
get(target, prop) {
|
|
207
|
+
return Reflect.get(target, prop);
|
|
208
|
+
},
|
|
209
|
+
set(target, prop, newValue) {
|
|
210
|
+
if (prop === "value") {
|
|
211
|
+
const previousValue = Reflect.get(target, prop);
|
|
212
|
+
Reflect.set(target, prop, newValue);
|
|
213
|
+
if (previousValue === newValue) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
subscribers.forEach((subscriber) =>
|
|
217
|
+
subscriber(XynChange.create(newValue, previousValue)),
|
|
218
|
+
);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return Reflect.set(target, prop, newValue);
|
|
222
|
+
},
|
|
223
|
+
apply(target, thisArg, args) {
|
|
224
|
+
return Reflect.apply(target, thisArg, args);
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return signalProxy;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @function proxyFactory
|
|
234
|
+
* @param {Object} obj
|
|
235
|
+
* @param {Set<Function>} subscribers
|
|
236
|
+
* @returns {Proxy | Number | String | Boolean | null | undefined | Symbol | BigInt }
|
|
237
|
+
* @description Creates a proxy for the given object that notifies subscribers when the object changes.
|
|
238
|
+
* @example
|
|
239
|
+
* const obj = { a: 1, b: 2 };
|
|
240
|
+
* const subscribers = new Set();
|
|
241
|
+
* const proxy = proxyFactory(obj, subscribers);
|
|
242
|
+
* proxy.a = 3; // Notifies subscribers
|
|
243
|
+
* proxy.b = 4; // Notifies subscribers
|
|
244
|
+
* proxy.c = 5; // Notifies subscribers
|
|
245
|
+
* delete proxy.a; // Notifies subscribers
|
|
246
|
+
* proxy.a = 6; // Notifies subscribers
|
|
247
|
+
* proxy.a = 7; // Notifies subscribers
|
|
248
|
+
*/
|
|
249
|
+
function proxyFactory(value, subscribers) {
|
|
250
|
+
if (value instanceof Map || value instanceof Set) {
|
|
251
|
+
return createCollectionProxy(value, subscribers);
|
|
252
|
+
} else if (Array.isArray(value)) {
|
|
253
|
+
return createListProxy(value, subscribers);
|
|
254
|
+
} else if (typeof value === "object" && value != null) {
|
|
255
|
+
return createObjectProxy(value, subscribers);
|
|
256
|
+
} else if (typeof value === "symbol") {
|
|
257
|
+
return String(value);
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function createCollectionProxy(collection, subscribers) {
|
|
263
|
+
return new Proxy(collection, {
|
|
264
|
+
get(target, prop) {
|
|
265
|
+
const value = Reflect.get(target, prop);
|
|
266
|
+
|
|
267
|
+
if (typeof value === "function") {
|
|
268
|
+
return (...args) => {
|
|
269
|
+
const prevDeleteValue =
|
|
270
|
+
prop === "delete" ? Reflect.get(target, args[0]) : null;
|
|
271
|
+
|
|
272
|
+
const result = value.apply(target, args);
|
|
273
|
+
if (prop === "get") {
|
|
274
|
+
return proxyFactory(result, subscribers);
|
|
275
|
+
}
|
|
276
|
+
if (prop === "set" || prop === "add") {
|
|
277
|
+
if (prop === "set" && Reflect.has(target, args[0])) {
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
subscribers.forEach((subscriber) => {
|
|
281
|
+
subscriber(
|
|
282
|
+
XynCollectionChange.create(
|
|
283
|
+
args[0],
|
|
284
|
+
args[1],
|
|
285
|
+
CollectionValue.INSERT,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (prop === "delete") {
|
|
291
|
+
subscribers.forEach((subscriber) => {
|
|
292
|
+
subscriber(
|
|
293
|
+
XynCollectionChange.create(
|
|
294
|
+
args[0],
|
|
295
|
+
CollectionValue.DELETE,
|
|
296
|
+
prevDeleteValue,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (prop === "clear") {
|
|
302
|
+
subscribers.forEach((subscriber) => {
|
|
303
|
+
subscriber(
|
|
304
|
+
XynCollectionChange.create(
|
|
305
|
+
CollectionValue.DELETE,
|
|
306
|
+
CollectionValue.DELETE,
|
|
307
|
+
CollectionValue.DELETE,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (typeof value === "object" && value !== null) {
|
|
317
|
+
return proxyFactory(value, subscribers);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return value;
|
|
321
|
+
},
|
|
322
|
+
getPrototypeOf(target) {
|
|
323
|
+
return Reflect.getPrototypeOf(target);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createListProxy(list, subscribers) {
|
|
329
|
+
return new Proxy(list, {
|
|
330
|
+
get(target, prop) {
|
|
331
|
+
const value = Reflect.get(target, prop);
|
|
332
|
+
if (typeof value === "function") {
|
|
333
|
+
return (...args) => {
|
|
334
|
+
const result = value.apply(target, args);
|
|
335
|
+
const LAST_INDEX = Reflect.get(target, "length") - 1;
|
|
336
|
+
|
|
337
|
+
if (prop === "push" || prop === "unshift") {
|
|
338
|
+
subscribers.forEach((subscriber) =>
|
|
339
|
+
subscriber(
|
|
340
|
+
XynCollectionChange.create(
|
|
341
|
+
prop === "push" ? LAST_INDEX : 0,
|
|
342
|
+
args[0],
|
|
343
|
+
CollectionValue.INSERT,
|
|
344
|
+
),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (prop === "pop" || prop === "shift") {
|
|
350
|
+
subscribers.forEach((subscriber) =>
|
|
351
|
+
subscriber(
|
|
352
|
+
XynCollectionChange.create(
|
|
353
|
+
prop === "pop" ? LAST_INDEX : 0,
|
|
354
|
+
CollectionValue.DELETE,
|
|
355
|
+
result,
|
|
356
|
+
),
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (prop === "splice") {
|
|
362
|
+
const [start, deleteCount, ...items] = args;
|
|
363
|
+
subscribers.forEach((subscriber) =>
|
|
364
|
+
subscriber(
|
|
365
|
+
XynCollectionChange.create([start, deleteCount], items, result),
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return result;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (typeof value === "object" && value !== null) {
|
|
375
|
+
return proxyFactory(value, subscribers);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return value;
|
|
379
|
+
},
|
|
380
|
+
getPrototypeOf(target) {
|
|
381
|
+
return Reflect.getPrototypeOf(target);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function createObjectProxy(obj, subscribers) {
|
|
387
|
+
return new Proxy(obj, {
|
|
388
|
+
get(target, prop, receiver) {
|
|
389
|
+
const value = Reflect.get(target, prop, receiver);
|
|
390
|
+
if (typeof value === "function") {
|
|
391
|
+
value = value();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (typeof value === "object" && value !== null) {
|
|
395
|
+
return proxyFactory(value, subscribers);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return value;
|
|
399
|
+
},
|
|
400
|
+
set(target, prop, newValue) {
|
|
401
|
+
const previousValue = Reflect.get(target, prop);
|
|
402
|
+
Reflect.set(target, prop, newValue);
|
|
403
|
+
subscribers.forEach((subscriber) =>
|
|
404
|
+
subscriber(XynCollectionChange.create(prop, newValue, previousValue)),
|
|
405
|
+
);
|
|
406
|
+
return true;
|
|
407
|
+
},
|
|
408
|
+
deleteProperty(target, prop) {
|
|
409
|
+
const previousValue = Reflect.get(target, prop);
|
|
410
|
+
if (typeof prop === "symbol") {
|
|
411
|
+
prop = prop.description || prop.toString();
|
|
412
|
+
}
|
|
413
|
+
Reflect.deleteProperty(target, prop);
|
|
414
|
+
subscribers.forEach((subscriber) =>
|
|
415
|
+
subscriber(
|
|
416
|
+
XynCollectionChange.create(
|
|
417
|
+
prop,
|
|
418
|
+
CollectionValue.DELETE,
|
|
419
|
+
previousValue,
|
|
420
|
+
),
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
return true;
|
|
424
|
+
},
|
|
425
|
+
getPrototypeOf(target) {
|
|
426
|
+
return Reflect.getPrototypeOf(target);
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function createObjectSignal(obj) {
|
|
432
|
+
const subscribers = new Set();
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
value: createObjectProxy(obj, subscribers),
|
|
436
|
+
subscribe(subscriber) {
|
|
437
|
+
subscribers.add(subscriber);
|
|
438
|
+
return () => subscribers.delete(subscriber);
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function createListSignal(list) {
|
|
444
|
+
const subscribers = new Set();
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
value: createListProxy(list, subscribers),
|
|
448
|
+
subscribe(subscriber) {
|
|
449
|
+
subscribers.add(subscriber);
|
|
450
|
+
return () => subscribers.delete(subscriber);
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function createCollectionSignal(collection) {
|
|
456
|
+
const subscribers = new Set();
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
value: createCollectionProxy(collection, subscribers),
|
|
460
|
+
subscribe(subscriber) {
|
|
461
|
+
subscribers.add(subscriber);
|
|
462
|
+
return () => subscribers.delete(subscriber);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @typedef {Object} Watch
|
|
469
|
+
* @property {function(Object): Watch} watch
|
|
470
|
+
* @property {function(function({value: any, previousValue: any}): void): function(): void} effect
|
|
471
|
+
* @property {function(function(...any): any): {signal: Object, unsubscribe: function(): void}} derived
|
|
472
|
+
*/
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @function watch
|
|
476
|
+
* @param {Object} signal
|
|
477
|
+
* @returns {Watch}
|
|
478
|
+
* @description Watches the given signal and returns an object with methods to watch other signals and create effects.
|
|
479
|
+
* The effect method takes a subscriber function and returns a function to unsubscribe.
|
|
480
|
+
* The derived method takes a function and returns a signal and a function to unsubscribe.
|
|
481
|
+
* @example
|
|
482
|
+
* const signal = createSignal(0);
|
|
483
|
+
* const signal2 = createSignal(1);
|
|
484
|
+
* const unsubscribe = watch(signal)
|
|
485
|
+
* .watch(signal2)
|
|
486
|
+
* .effect(({value, previousValue}) => console.log(`Value changed from ${previousValue} to ${value}`));
|
|
487
|
+
*/
|
|
488
|
+
export function watch(signal) {
|
|
489
|
+
const signals = new Set();
|
|
490
|
+
|
|
491
|
+
const watchers = (sig) => {
|
|
492
|
+
if (sig && typeof sig.subscribe === "function") {
|
|
493
|
+
signals.add(sig);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
watch(s) {
|
|
497
|
+
return watchers(s);
|
|
498
|
+
},
|
|
499
|
+
effect(subscriber) {
|
|
500
|
+
const unsubscribers = Array.from(signals.keys()).map((s) =>
|
|
501
|
+
s.subscribe(subscriber),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
return () => {
|
|
505
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
derived(fn, wrappingFn = (fn) => (change) => fn(change)) {
|
|
509
|
+
const derivedSignal = createSignal(fn());
|
|
510
|
+
const unsubscribers = Array.from(signals.keys()).map((s) =>
|
|
511
|
+
s.subscribe(
|
|
512
|
+
wrappingFn((change) => {
|
|
513
|
+
derivedSignal.value = fn(change);
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
signal: derivedSignal,
|
|
520
|
+
unsubscribe: () =>
|
|
521
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe()),
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
return watchers(signal);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* @typedef {Object} Timing
|
|
532
|
+
* @property {function(function(...any): void): function(...any): void} debounce
|
|
533
|
+
* @property {function(function(...any): void): function(...any): void} throttle
|
|
534
|
+
* @property {function(function(...any): void): function(...any): void} delay
|
|
535
|
+
* @description Timing is an object with methods to debounce, throttle, and delay functions.
|
|
536
|
+
*/
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @function timing
|
|
540
|
+
* @param {int} delay
|
|
541
|
+
* @returns {Timing}
|
|
542
|
+
* @description Returns an object with methods to debounce, throttle, and delay functions.
|
|
543
|
+
* The debounce method takes a function and returns a function that will only call the given function after the given delay.
|
|
544
|
+
* The throttle method takes a function and returns a function that will only call the given function once per delay.
|
|
545
|
+
* The delay method takes a function and returns a function that will call the given function after the given delay.
|
|
546
|
+
* @example
|
|
547
|
+
* const debounced = timing(100).debounce(() => console.log("debounced"));
|
|
548
|
+
* const throttled = timing(100).throttle(() => console.log("throttled"));
|
|
549
|
+
* const delayed = timing(100).delay(() => console.log("delayed"));
|
|
550
|
+
* debounced(); // Will log "debounced" after 100ms
|
|
551
|
+
* throttled(); // Will log "throttled" immediately
|
|
552
|
+
* throttled(); // Will not log "throttled" again until 100ms have passed
|
|
553
|
+
* delayed(); // Will log "delayed" after 100ms
|
|
554
|
+
* debounced(); // Will not log "debounced" again until 100ms have passed
|
|
555
|
+
*/
|
|
556
|
+
export function timing(delay) {
|
|
557
|
+
return {
|
|
558
|
+
debounce(fn) {
|
|
559
|
+
let timeoutId;
|
|
560
|
+
return (...args) => {
|
|
561
|
+
clearTimeout(timeoutId);
|
|
562
|
+
timeoutId = setTimeout(fn, delay, ...args);
|
|
563
|
+
};
|
|
564
|
+
},
|
|
565
|
+
throttle(fn) {
|
|
566
|
+
let lastCall = 0;
|
|
567
|
+
return (...args) => {
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
if (now - lastCall >= delay) {
|
|
570
|
+
lastCall = now;
|
|
571
|
+
fn(...args);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
},
|
|
575
|
+
delay(fn) {
|
|
576
|
+
return (...args) => {
|
|
577
|
+
setTimeout(fn, delay, ...args);
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xyn-html/xyn-signal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight, reactive signal-based state management for JavaScript applications",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/xyn_signal.js",
|
|
7
|
+
"module": "dist/xyn_signal.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/xyn_signal.js",
|
|
11
|
+
"default": "./dist/xyn_signal.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "cp ../../src/xyn_signal.js dist/xyn_signal.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"signal",
|
|
25
|
+
"reactive",
|
|
26
|
+
"state-management",
|
|
27
|
+
"javascript",
|
|
28
|
+
"frontend",
|
|
29
|
+
"xyn"
|
|
30
|
+
],
|
|
31
|
+
"author": "Jeffrey Tackett",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github/jfftck/XynHTML"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/jfftck/XynHTML"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/jfftck/XynHTML"
|
|
41
|
+
}
|