avosignals 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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/avosignals-util.d.ts +4 -0
- package/dist/avosignals-util.js +31 -0
- package/dist/avosignals.d.ts +53 -0
- package/dist/avosignals.js +232 -0
- package/dist/singal-watch.d.ts +8 -0
- package/dist/singal-watch.js +23 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anatoli Radulov
|
|
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,150 @@
|
|
|
1
|
+
# avosignals
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe reactive state management library for TypeScript. It features automatic dependency tracking, efficient updates, and first-class support for **Lit** components.
|
|
4
|
+
|
|
5
|
+
# Features
|
|
6
|
+
|
|
7
|
+
- ⚡️ **Fine-Grained Reactivity**: Updates only what needs to change.
|
|
8
|
+
- 🛡️ **Circular Dependency Protection**: Detects and prevents infinite loops during computation.
|
|
9
|
+
|
|
10
|
+
- 🧹**Automatic Garbage Collection**: Uses `WeakRef` for internal computed subscriptions to prevent memory leaks in derived state graphs.
|
|
11
|
+
|
|
12
|
+
- 🔒 **Safety Guardrails**: Prevents state mutations during reactive evaluations to ensure data consistency.
|
|
13
|
+
|
|
14
|
+
- 🔥 **Lit Integration**: Includes a specific controller (`SignalWatcher`) to make Lit components reactive automatically.
|
|
15
|
+
|
|
16
|
+
# Instalation
|
|
17
|
+
|
|
18
|
+
### Node / NPM
|
|
19
|
+
```bash
|
|
20
|
+
npm install avosignals
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Browser / Esm.sh
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import { Signal, Computed, effect } from "https://esm.sh/avosignals"
|
|
27
|
+
</script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
# Core Concepts
|
|
31
|
+
1. Signals
|
|
32
|
+
|
|
33
|
+
Signals are the atoms of state. They hold a value and notify subscribers when that value changes.
|
|
34
|
+
|
|
35
|
+
```Typescript
|
|
36
|
+
import { Signal } from 'avosignals';
|
|
37
|
+
|
|
38
|
+
const count = new Signal(0, 'count');
|
|
39
|
+
|
|
40
|
+
console.log(count.get()); // 0
|
|
41
|
+
|
|
42
|
+
count.set(1);
|
|
43
|
+
count.update(c => c + 1); // 2
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Computed
|
|
47
|
+
|
|
48
|
+
Computed values are derived signals. They depend on other signals and re-evaluate only when their dependencies change. They are lazy—they only recalculate when read.
|
|
49
|
+
|
|
50
|
+
```Typescript
|
|
51
|
+
import { Signal, Computed } from 'avosignals';
|
|
52
|
+
|
|
53
|
+
const count = new Signal(1);
|
|
54
|
+
const double = new Computed(() => count.get() * 2, 'double');
|
|
55
|
+
|
|
56
|
+
console.log(double.get()); // 2
|
|
57
|
+
|
|
58
|
+
count.set(10);
|
|
59
|
+
console.log(double.get()); // 20
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. Effects
|
|
63
|
+
|
|
64
|
+
Effects are side effects that run automatically whenever the signals they access change. Useful for logging, manual DOM manipulation, or syncing with external APIs.
|
|
65
|
+
|
|
66
|
+
```Typescript
|
|
67
|
+
import { Signal, effect } from 'avosignals';
|
|
68
|
+
|
|
69
|
+
const count = new Signal(0);
|
|
70
|
+
|
|
71
|
+
const dispose = effect(() => {
|
|
72
|
+
console.log(`The count is now ${count.get()}`);
|
|
73
|
+
|
|
74
|
+
// Optional cleanup function (runs before next execution or on dispose)
|
|
75
|
+
return () => console.log('Cleaning up...');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
count.set(1);
|
|
79
|
+
// Logs: "The count is now 1"
|
|
80
|
+
|
|
81
|
+
dispose();
|
|
82
|
+
// Logs: 'Cleaning up...'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
# Usage with Lit
|
|
86
|
+
|
|
87
|
+
`avosignals` was built with Lit in mind. The `SignalWatcher` class hooks into the Lit lifecycle to automatically track signals accessed during render. Its core design is to allow for production ready signals which can be easily replaced with Lit's official signals once **TC39 signals** becomes mainstream and production ready.
|
|
88
|
+
|
|
89
|
+
# The `SignalWatcher` Controller
|
|
90
|
+
|
|
91
|
+
You do not need to manually subscribe to signals. simply add the controller, and any signal read inside `render()` will trigger a component update when it changes.
|
|
92
|
+
|
|
93
|
+
```Typescript
|
|
94
|
+
import { LitElement, html } from 'lit';
|
|
95
|
+
import { customElement } from 'lit/decorators.js';
|
|
96
|
+
import { Signal, SignalWatcher } from 'avosignals';
|
|
97
|
+
|
|
98
|
+
// Shared state
|
|
99
|
+
const counter = new Signal(0);
|
|
100
|
+
|
|
101
|
+
@customElement('my-counter')
|
|
102
|
+
export class MyCounter extends LitElement {
|
|
103
|
+
// 1. Register the watcher
|
|
104
|
+
private watcher = new SignalWatcher(this);
|
|
105
|
+
|
|
106
|
+
render() {
|
|
107
|
+
// 2. Access signals directly.
|
|
108
|
+
// The component now auto-subscribes to 'counter'.
|
|
109
|
+
return html`
|
|
110
|
+
<p>Count: ${counter.get()}</p>
|
|
111
|
+
<button @click=${() => counter.update(c => c + 1)}>
|
|
112
|
+
Increment
|
|
113
|
+
</button>
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
# Advanced Architecture
|
|
120
|
+
|
|
121
|
+
### Memory Management (WeakRefs)
|
|
122
|
+
|
|
123
|
+
Unlike many other signal libraries, `Computed` nodes in avosignals hold weak references to their subscribers where possible. This means if you create a derived signal but stop referencing it in your application, JavaScript's Garbage Collector can clean it up, even if the source signal is still active. This prevents the common "detached listener" memory leaks found in observer patterns.
|
|
124
|
+
|
|
125
|
+
### Cycle Detection
|
|
126
|
+
|
|
127
|
+
`avosignals` maintains a stack of active consumers. If a computed value attempts to read itself during its own evaluation **(A -> B -> A)**, the library throws a descriptive error helping you identify the cycle immediately.
|
|
128
|
+
|
|
129
|
+
### Read/Write Consistency
|
|
130
|
+
|
|
131
|
+
To ensure unidirectional data flow, `avosignals` forbids writing to a `Signal` while a `Computed` value is currently being evaluated. This prevents side-effects from occurring during the "read" phase of your application loop.
|
|
132
|
+
|
|
133
|
+
# API Reference
|
|
134
|
+
|
|
135
|
+
`Signal<T>`
|
|
136
|
+
|
|
137
|
+
- `constructor(initial: T, name?: string)`
|
|
138
|
+
- `get(): T`: Returns current value and tracks dependency.
|
|
139
|
+
- `set(value: T)`: Updates value and notifies listeners.
|
|
140
|
+
- `update(fn: (prev: T) => T)`: Convenience method for updating based on previous value.
|
|
141
|
+
|
|
142
|
+
`Computed<T>`
|
|
143
|
+
- `constructor(fn: () => T, name?: string)`
|
|
144
|
+
- `get(): T`: Evaluates (if dirty) and returns the value.
|
|
145
|
+
|
|
146
|
+
`effect`
|
|
147
|
+
- `effect(fn: () => void | cleanupFn)`: Runs immediately and tracks dependencies. Returns a dispose function.
|
|
148
|
+
|
|
149
|
+
# License
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Signal } from "./avosignals";
|
|
2
|
+
//TODO - function which returns JSON with possibly nested Signals resolved
|
|
3
|
+
export function singalToJSON(value) {
|
|
4
|
+
const result = {};
|
|
5
|
+
const val = value.get();
|
|
6
|
+
if (Array.isArray(val)) {
|
|
7
|
+
return val.map(item => {
|
|
8
|
+
if (item instanceof Signal) {
|
|
9
|
+
return singalToJSON(item);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return item;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
else if (typeof val === 'object' && val !== null) {
|
|
17
|
+
for (const key in val) {
|
|
18
|
+
const item = val[key];
|
|
19
|
+
if (item instanceof Signal) {
|
|
20
|
+
result[key] = singalToJSON(item);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result[key] = item;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return val;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LitElement } from "lit";
|
|
2
|
+
type Job = () => void;
|
|
3
|
+
type Unsubscribe = () => void;
|
|
4
|
+
interface Debuggable {
|
|
5
|
+
_debugParent?: Watcher;
|
|
6
|
+
}
|
|
7
|
+
interface Watcher extends Debuggable {
|
|
8
|
+
track(signal: Observable<unknown>): void;
|
|
9
|
+
}
|
|
10
|
+
declare abstract class Observable<T> {
|
|
11
|
+
#private;
|
|
12
|
+
name?: string;
|
|
13
|
+
constructor(initial: T, name?: string);
|
|
14
|
+
protected _get(): T;
|
|
15
|
+
protected _set(next: T): boolean;
|
|
16
|
+
subscribe(fn: Job, weak?: boolean): Unsubscribe;
|
|
17
|
+
_notify(): void;
|
|
18
|
+
get id(): number;
|
|
19
|
+
get previousValue(): T | undefined;
|
|
20
|
+
toString(): string;
|
|
21
|
+
}
|
|
22
|
+
export declare class Signal<T> extends Observable<T> {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(initial: T, name?: string);
|
|
25
|
+
static get activeConsumer(): Watcher;
|
|
26
|
+
static push(consumer: Watcher): void;
|
|
27
|
+
static pop(): void;
|
|
28
|
+
get(): T;
|
|
29
|
+
set(value: T): void;
|
|
30
|
+
update(fn: (current: T) => T): void;
|
|
31
|
+
}
|
|
32
|
+
export declare class Computed<T> extends Observable<T> implements Watcher {
|
|
33
|
+
#private;
|
|
34
|
+
_debugParent: Watcher | undefined;
|
|
35
|
+
constructor(fn: () => T, name?: string, options?: {
|
|
36
|
+
weak?: boolean;
|
|
37
|
+
});
|
|
38
|
+
track(signal: Observable<unknown>): void;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
get(): T;
|
|
41
|
+
subscribe(fn: Job, weak?: boolean): Unsubscribe;
|
|
42
|
+
}
|
|
43
|
+
export declare function effect(fn: () => (void | (() => void)), name?: string): () => void;
|
|
44
|
+
export declare class SignalWatcher implements Watcher {
|
|
45
|
+
#private;
|
|
46
|
+
_debugParent: Watcher | undefined;
|
|
47
|
+
constructor(host: LitElement);
|
|
48
|
+
track(signal: Observable<unknown>): void;
|
|
49
|
+
hostUpdate(): void;
|
|
50
|
+
hostUpdated(): void;
|
|
51
|
+
hostDisconnected(): void;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
let __DEV__ = true; //TODO - add logic to detect debug query param
|
|
2
|
+
function safe_not_equal(a, b) {
|
|
3
|
+
return a != a
|
|
4
|
+
? b == b
|
|
5
|
+
: a !== b || ((a && typeof a === 'object') || typeof a === 'function');
|
|
6
|
+
}
|
|
7
|
+
function isWeakRef(value) {
|
|
8
|
+
return value && typeof value.deref === 'function';
|
|
9
|
+
}
|
|
10
|
+
class Observable {
|
|
11
|
+
static #nextId = 0;
|
|
12
|
+
#value;
|
|
13
|
+
#previousValue;
|
|
14
|
+
#subs = new Set();
|
|
15
|
+
#id = Observable.#nextId++;
|
|
16
|
+
name;
|
|
17
|
+
constructor(initial, name) {
|
|
18
|
+
this.#value = initial;
|
|
19
|
+
this.#previousValue = initial;
|
|
20
|
+
this.name = name;
|
|
21
|
+
}
|
|
22
|
+
_get() {
|
|
23
|
+
if (Signal.activeConsumer) {
|
|
24
|
+
Signal.activeConsumer.track(this);
|
|
25
|
+
}
|
|
26
|
+
return this.#value;
|
|
27
|
+
}
|
|
28
|
+
_set(next) {
|
|
29
|
+
if (!safe_not_equal(next, this.#value))
|
|
30
|
+
return false;
|
|
31
|
+
this.#previousValue = this.#value;
|
|
32
|
+
this.#value = next;
|
|
33
|
+
this._notify();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
subscribe(fn, weak = false) {
|
|
37
|
+
let entry = fn;
|
|
38
|
+
if (weak) {
|
|
39
|
+
entry = new WeakRef(fn);
|
|
40
|
+
}
|
|
41
|
+
this.#subs.add(entry);
|
|
42
|
+
return () => this.#subs.delete(entry);
|
|
43
|
+
}
|
|
44
|
+
_notify() {
|
|
45
|
+
const subs = [...this.#subs];
|
|
46
|
+
for (const entry of subs) {
|
|
47
|
+
let job;
|
|
48
|
+
if (isWeakRef(entry)) {
|
|
49
|
+
job = entry.deref();
|
|
50
|
+
// If the object is garbage collected, remove the dead link
|
|
51
|
+
if (!job) {
|
|
52
|
+
this.#subs.delete(entry);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
job = entry;
|
|
58
|
+
}
|
|
59
|
+
job();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
get id() {
|
|
63
|
+
return this.#id;
|
|
64
|
+
}
|
|
65
|
+
get previousValue() {
|
|
66
|
+
return this.#previousValue;
|
|
67
|
+
}
|
|
68
|
+
toString() {
|
|
69
|
+
return this.name
|
|
70
|
+
? `${this.constructor.name}(${this.name})`
|
|
71
|
+
: `${this.constructor.name}#${this.id}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class Signal extends Observable {
|
|
75
|
+
static #stack = [];
|
|
76
|
+
constructor(initial, name) {
|
|
77
|
+
super(initial, name);
|
|
78
|
+
}
|
|
79
|
+
static get activeConsumer() {
|
|
80
|
+
return this.#stack[this.#stack.length - 1];
|
|
81
|
+
}
|
|
82
|
+
static push(consumer) {
|
|
83
|
+
if (__DEV__) {
|
|
84
|
+
consumer._debugParent = this.activeConsumer;
|
|
85
|
+
}
|
|
86
|
+
this.#stack.push(consumer);
|
|
87
|
+
}
|
|
88
|
+
static pop() {
|
|
89
|
+
if (__DEV__ && !this.#stack.length) {
|
|
90
|
+
throw new Error('Signal.pop() without matching push()');
|
|
91
|
+
}
|
|
92
|
+
this.#stack.pop();
|
|
93
|
+
}
|
|
94
|
+
get() {
|
|
95
|
+
return this._get();
|
|
96
|
+
}
|
|
97
|
+
set(value) {
|
|
98
|
+
if (__DEV__ && Signal.activeConsumer) {
|
|
99
|
+
throw new Error(`Signal write during reactive evaluation:\n` +
|
|
100
|
+
` writing ${this}\n` +
|
|
101
|
+
` while computing ${Signal.activeConsumer}`);
|
|
102
|
+
}
|
|
103
|
+
this._set(value);
|
|
104
|
+
}
|
|
105
|
+
update(fn) {
|
|
106
|
+
this.set(fn(this.get()));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export class Computed extends Observable {
|
|
110
|
+
#fn;
|
|
111
|
+
#dirty = true;
|
|
112
|
+
#computing = false;
|
|
113
|
+
#deps = new Map();
|
|
114
|
+
_debugParent;
|
|
115
|
+
#weak;
|
|
116
|
+
// 1. Create a stable, bound reference to the notification logic
|
|
117
|
+
#notifyCallback = () => {
|
|
118
|
+
if (this.#dirty)
|
|
119
|
+
return;
|
|
120
|
+
this.#dirty = true;
|
|
121
|
+
this._notify();
|
|
122
|
+
};
|
|
123
|
+
constructor(fn, name, options) {
|
|
124
|
+
super(undefined, name);
|
|
125
|
+
this.#fn = fn;
|
|
126
|
+
// Default to TRUE (Weak) for standard computeds to prevent leaks
|
|
127
|
+
this.#weak = options?.weak ?? true;
|
|
128
|
+
}
|
|
129
|
+
track(signal) {
|
|
130
|
+
if (this.#deps.has(signal))
|
|
131
|
+
return;
|
|
132
|
+
// 2. Subscribe using the STABLE callback and WEAK flag
|
|
133
|
+
const unsub = signal.subscribe(this.#notifyCallback, this.#weak);
|
|
134
|
+
this.#deps.set(signal, unsub);
|
|
135
|
+
}
|
|
136
|
+
dispose() {
|
|
137
|
+
this.#cleanup();
|
|
138
|
+
}
|
|
139
|
+
#cleanup() {
|
|
140
|
+
for (const unsub of this.#deps.values())
|
|
141
|
+
unsub();
|
|
142
|
+
this.#deps.clear();
|
|
143
|
+
}
|
|
144
|
+
get() {
|
|
145
|
+
if (Signal.activeConsumer) {
|
|
146
|
+
Signal.activeConsumer.track(this);
|
|
147
|
+
}
|
|
148
|
+
if (!this.#dirty)
|
|
149
|
+
return this._get();
|
|
150
|
+
if (this.#computing) {
|
|
151
|
+
throw new Error(`Cycle detected in ${this}\n` +
|
|
152
|
+
`↳ while computing ${this._debugParent ?? 'root'}`);
|
|
153
|
+
}
|
|
154
|
+
this.#computing = true;
|
|
155
|
+
this.#cleanup();
|
|
156
|
+
Signal.push(this);
|
|
157
|
+
try {
|
|
158
|
+
const value = this.#fn();
|
|
159
|
+
this._set(value);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
Signal.pop();
|
|
163
|
+
this.#dirty = false;
|
|
164
|
+
this.#computing = false;
|
|
165
|
+
}
|
|
166
|
+
return this._get();
|
|
167
|
+
}
|
|
168
|
+
subscribe(fn, weak = false) {
|
|
169
|
+
const unsub = super.subscribe(fn, weak);
|
|
170
|
+
//ensure we don't affect tracking
|
|
171
|
+
if (!Signal.activeConsumer)
|
|
172
|
+
this.get();
|
|
173
|
+
return unsub;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function effect(fn, name) {
|
|
177
|
+
let cleanup;
|
|
178
|
+
const computer = new Computed(() => {
|
|
179
|
+
if (typeof cleanup === 'function')
|
|
180
|
+
cleanup();
|
|
181
|
+
cleanup = fn();
|
|
182
|
+
}, name, { weak: false });
|
|
183
|
+
const unsub = computer.subscribe(() => {
|
|
184
|
+
computer.get();
|
|
185
|
+
});
|
|
186
|
+
computer.get();
|
|
187
|
+
return () => {
|
|
188
|
+
unsub();
|
|
189
|
+
computer.dispose();
|
|
190
|
+
if (typeof cleanup === 'function')
|
|
191
|
+
cleanup();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Lit
|
|
195
|
+
export class SignalWatcher {
|
|
196
|
+
#host;
|
|
197
|
+
#deps = new Map();
|
|
198
|
+
_debugParent;
|
|
199
|
+
#active = false;
|
|
200
|
+
constructor(host) {
|
|
201
|
+
this.#host = host;
|
|
202
|
+
host.addController(this);
|
|
203
|
+
}
|
|
204
|
+
track(signal) {
|
|
205
|
+
if (this.#deps.has(signal))
|
|
206
|
+
return;
|
|
207
|
+
this.#deps.set(signal, signal.subscribe(() => this.#host.requestUpdate()));
|
|
208
|
+
}
|
|
209
|
+
hostUpdate() {
|
|
210
|
+
for (const unsub of this.#deps.values())
|
|
211
|
+
unsub();
|
|
212
|
+
this.#deps.clear();
|
|
213
|
+
Signal.push(this);
|
|
214
|
+
this.#active = true;
|
|
215
|
+
}
|
|
216
|
+
hostUpdated() {
|
|
217
|
+
if (this.#active) {
|
|
218
|
+
Signal.pop();
|
|
219
|
+
this.#active = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
hostDisconnected() {
|
|
223
|
+
for (const unsub of this.#deps.values())
|
|
224
|
+
unsub();
|
|
225
|
+
this.#deps.clear();
|
|
226
|
+
// optional safety if the component disconnects mid-render
|
|
227
|
+
if (this.#active) {
|
|
228
|
+
Signal.pop();
|
|
229
|
+
this.#active = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import { Signal, SignalWatcher } from "./avosignals";
|
|
3
|
+
export declare class SignalWatch extends LitElement {
|
|
4
|
+
watcher: SignalWatcher;
|
|
5
|
+
accessor signals: Signal<unknown>[];
|
|
6
|
+
_subscribers: (() => void)[];
|
|
7
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LitElement, html } from "lit";
|
|
2
|
+
import { SignalWatcher } from "./avosignals";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
|
+
import { singalToJSON } from "./avosignals-util";
|
|
5
|
+
@customElement("signal-watch")
|
|
6
|
+
export class SignalWatch extends LitElement {
|
|
7
|
+
watcher = new SignalWatcher(this);
|
|
8
|
+
@property()
|
|
9
|
+
accessor signals = [];
|
|
10
|
+
_subscribers = [];
|
|
11
|
+
render() {
|
|
12
|
+
// JSON viewer is included in storybook-head
|
|
13
|
+
// also see https://github.com/alenaksu/json-viewer
|
|
14
|
+
return html `
|
|
15
|
+
${this.signals.map(signal => html `
|
|
16
|
+
<div>
|
|
17
|
+
<pre>Signal [${signal.name || 'unnamed_' + signal.id}]</pre>
|
|
18
|
+
<json-viewer .data=${singalToJSON(signal)}></json-viewer>
|
|
19
|
+
</div>
|
|
20
|
+
`)}
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "avosignals",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight signaling library for web components and modern web applications with Lit integration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"signals",
|
|
7
|
+
"events",
|
|
8
|
+
"typescript",
|
|
9
|
+
"observer",
|
|
10
|
+
"reactive"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/avosignals.js",
|
|
14
|
+
"types": "dist/avosignals.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist", "LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"author": "Anatoli Radulov",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.3.3",
|
|
25
|
+
"vitest": "^1.4.0",
|
|
26
|
+
"@vitest/coverage-v8": "^1.4.0",
|
|
27
|
+
"lit": "3.3.2"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/anatolipr/avos.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/anatolipr/avos/issues"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|