@juulsgaard/signal-tools 18.0.1 → 18.0.2

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/dist/async.mjs ADDED
@@ -0,0 +1,96 @@
1
+ import { assertInInjectionContext, signal, effect, DestroyRef, inject } from '@angular/core';
2
+
3
+ function debouncedSignal(inputSignal, delay = 500, options) {
4
+ if (options?.injector) assertInInjectionContext(debouncedSignal);
5
+ const output = signal(inputSignal());
6
+ effect((onDestroy) => {
7
+ const value = inputSignal();
8
+ const timeout = setTimeout(() => output.set(value), delay);
9
+ onDestroy(() => clearTimeout(timeout));
10
+ }, { allowSignalWrites: true, injector: options?.injector });
11
+ return output;
12
+ }
13
+ function throttledSignal(inputSignal, delay = 500, options) {
14
+ if (options?.injector) assertInInjectionContext(throttledSignal);
15
+ const output = signal(inputSignal());
16
+ let nextValue = inputSignal();
17
+ let timeout = void 0;
18
+ effect(() => {
19
+ const value = inputSignal();
20
+ nextValue = value;
21
+ if (timeout) return;
22
+ if (options?.emitLeading) {
23
+ output.set(value);
24
+ }
25
+ timeout = setTimeout(() => {
26
+ output.set(nextValue);
27
+ timeout = void 0;
28
+ }, delay);
29
+ }, { allowSignalWrites: true, injector: options?.injector });
30
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
31
+ onDestroy.onDestroy(() => clearTimeout(timeout));
32
+ return output;
33
+ }
34
+ function debouncedThrottledSignal(inputSignal, throttleDelay = 1e3, debounceDelay = 300, options) {
35
+ if (options?.injector) assertInInjectionContext(debouncedThrottledSignal);
36
+ const output = signal(inputSignal());
37
+ let nextValue = inputSignal();
38
+ let longTimeout = void 0;
39
+ function clearThrottle() {
40
+ if (!longTimeout) return;
41
+ clearTimeout(longTimeout);
42
+ longTimeout = void 0;
43
+ }
44
+ effect((onDestroy2) => {
45
+ const value = inputSignal();
46
+ const shortTimeout = setTimeout(() => {
47
+ output.set(value);
48
+ clearThrottle();
49
+ }, debounceDelay);
50
+ onDestroy2(() => clearTimeout(shortTimeout));
51
+ nextValue = value;
52
+ if (longTimeout) return;
53
+ longTimeout = setTimeout(() => {
54
+ output.set(nextValue);
55
+ longTimeout = void 0;
56
+ }, throttleDelay);
57
+ }, { allowSignalWrites: true, injector: options?.injector });
58
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
59
+ onDestroy.onDestroy(() => clearTimeout(longTimeout));
60
+ return output;
61
+ }
62
+ function searchSignal(inputSignal, throttleDelay = 1e3, debounceDelay = 300, options) {
63
+ if (options?.injector) assertInInjectionContext(searchSignal);
64
+ const query = signal(inputSignal());
65
+ let nextValue = inputSignal();
66
+ let longTimeout = void 0;
67
+ function clearThrottle() {
68
+ if (!longTimeout) return;
69
+ clearTimeout(longTimeout);
70
+ longTimeout = void 0;
71
+ }
72
+ effect((onDestroy2) => {
73
+ const value = inputSignal();
74
+ if (value === void 0 || value === "") {
75
+ query.set(value);
76
+ clearThrottle();
77
+ return;
78
+ }
79
+ const shortTimeout = setTimeout(() => {
80
+ query.set(value);
81
+ clearThrottle();
82
+ }, debounceDelay);
83
+ onDestroy2(() => clearTimeout(shortTimeout));
84
+ nextValue = value;
85
+ if (longTimeout) return;
86
+ longTimeout = setTimeout(() => {
87
+ query.set(nextValue);
88
+ longTimeout = void 0;
89
+ }, throttleDelay);
90
+ }, { allowSignalWrites: true, injector: options?.injector });
91
+ const onDestroy = options?.injector?.get(DestroyRef) ?? inject(DestroyRef);
92
+ onDestroy.onDestroy(() => clearTimeout(longTimeout));
93
+ return query;
94
+ }
95
+
96
+ export { debouncedSignal, debouncedThrottledSignal, searchSignal, throttledSignal };
@@ -0,0 +1,134 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+
3
+ class SignalMap {
4
+ constructor(values) {
5
+ this._map = signal(new Map(values));
6
+ this.value = this._map.asReadonly();
7
+ this.size = computed(() => this.value().size);
8
+ this.empty = computed(() => this.size() <= 0);
9
+ }
10
+ [Symbol.iterator]() {
11
+ return this.value()[Symbol.iterator]();
12
+ }
13
+ getCopy() {
14
+ return new Map(untracked(this.value));
15
+ }
16
+ //<editor-fold desc="Actions">
17
+ /**
18
+ * Remove all keys not in the whitelist
19
+ * @param whitelist - The keys to keep
20
+ */
21
+ filter(whitelist) {
22
+ const length = whitelist && "size" in whitelist ? whitelist.size : whitelist?.length;
23
+ if (!length) {
24
+ return this.clear();
25
+ }
26
+ const whitelistSet = whitelist instanceof Set ? whitelist : new Set(whitelist);
27
+ const map = this.getCopy();
28
+ for (let [key] of untracked(this.value)) {
29
+ if (whitelistSet.has(key)) continue;
30
+ map.delete(key);
31
+ }
32
+ if (untracked(this.size) !== map.size) {
33
+ this._map.set(map);
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ /** Clear the collection */
39
+ clear() {
40
+ if (!untracked(this.size)) return false;
41
+ this._map.set(/* @__PURE__ */ new Map());
42
+ return true;
43
+ }
44
+ /**
45
+ * Add a value to the collection if the key is not in use
46
+ * @param key - The key to use
47
+ * @param value - The value to add
48
+ * @return added - Returns true if the value was added
49
+ */
50
+ add(key, value) {
51
+ if (untracked(this.value).has(key)) return false;
52
+ const map = this.getCopy();
53
+ map.set(key, value);
54
+ this._map.set(map);
55
+ return true;
56
+ }
57
+ /**
58
+ * Add a value to the collection
59
+ * @param key - The key to use
60
+ * @param value - The value to add
61
+ * @return added - Returns true if the value was added or changed
62
+ */
63
+ set(key, value) {
64
+ const state = untracked(this.value);
65
+ if (state.has(key) && state.get(key) === value) return false;
66
+ const map = this.getCopy();
67
+ map.set(key, value);
68
+ this._map.set(map);
69
+ return true;
70
+ }
71
+ /**
72
+ * Remove a key from the collection
73
+ * @param key - The key to remove
74
+ * @return removed - True if an item was removed
75
+ */
76
+ delete(key) {
77
+ if (!untracked(this.value).has(key)) return false;
78
+ const map = this.getCopy();
79
+ map.delete(key);
80
+ this._map.set(map);
81
+ return true;
82
+ }
83
+ /**
84
+ * Remove keys from the collection
85
+ * @param values - The keys to remove
86
+ * @return removed - True if items were removed
87
+ */
88
+ deleteRange(values) {
89
+ const map = this.getCopy();
90
+ const size = map.size;
91
+ values.forEach((v) => map.delete(v));
92
+ if (map.size === size) return false;
93
+ this._map.set(map);
94
+ return true;
95
+ }
96
+ /**
97
+ * Toggle a value in the map
98
+ * @param key - The key to toggle
99
+ * @param value - The value to insert if applicable
100
+ * @param state - A forced state (`true` = always add, `false` = always delete)
101
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
102
+ */
103
+ toggle(key, value, state) {
104
+ if (untracked(this.value).has(key)) {
105
+ if (state === true) return void 0;
106
+ const map2 = this.getCopy();
107
+ map2.delete(key);
108
+ this._map.set(map2);
109
+ return false;
110
+ }
111
+ if (state === false) return void 0;
112
+ const map = this.getCopy();
113
+ map.set(key, value);
114
+ this._map.set(map);
115
+ return true;
116
+ }
117
+ /** Manually modify the inner collection */
118
+ modify(modify) {
119
+ const map = this.getCopy();
120
+ modify(map);
121
+ this._map.set(map);
122
+ }
123
+ /** @inheritDoc */
124
+ has(key) {
125
+ return computed(() => this.value().has(key));
126
+ }
127
+ /** @inheritDoc */
128
+ get(key) {
129
+ return computed(() => this.value().get(key));
130
+ }
131
+ //</editor-fold>
132
+ }
133
+
134
+ export { SignalMap };
@@ -0,0 +1,115 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+ import { pairwiseSignal } from '../operators.mjs';
3
+
4
+ class SignalQueue {
5
+ constructor(options) {
6
+ this._items = signal([]);
7
+ /** A signal emitting the items of the queue */
8
+ this.items = this._items.asReadonly();
9
+ /** A signal emitting the number of items in the queue */
10
+ this.length = computed(() => this.items().length);
11
+ /** A signal emitting true if there are no items in the queue */
12
+ this.empty = computed(() => this.length() <= 0);
13
+ this._maxSize = options?.size;
14
+ this.front = computed(() => this.items().at(0));
15
+ this.back = computed(() => this.items().at(-1));
16
+ const delta = pairwiseSignal(this.items, []);
17
+ this.frontDelta = computed(() => {
18
+ const [prev, next] = delta();
19
+ const item = next.at(0);
20
+ return { item, added: !item ? false : !prev.includes(item) };
21
+ }, { equal: (a, b) => a.item === b.item });
22
+ this.backDelta = computed(() => {
23
+ const [prev, next] = delta();
24
+ const item = next.at(-1);
25
+ return { item, added: !item ? false : !prev.includes(item) };
26
+ }, { equal: (a, b) => a.item === b.item });
27
+ }
28
+ /** The max size of the queue */
29
+ get maxSize() {
30
+ return this._maxSize;
31
+ }
32
+ //</editor-fold>
33
+ //<editor-fold desc="Actions">
34
+ /**
35
+ * Removes an item from the scheduler
36
+ * @param item
37
+ */
38
+ removeItem(item) {
39
+ const index = untracked(this.items).indexOf(item);
40
+ if (index < 0) return false;
41
+ const arr = [...untracked(this.items)];
42
+ arr.splice(index, 1);
43
+ this._items.set(arr);
44
+ return true;
45
+ }
46
+ dequeue(count) {
47
+ if (count !== void 0) {
48
+ if (count < 1) return [];
49
+ if (!untracked(this.length)) return [];
50
+ const items = untracked(this.items).slice(0, count);
51
+ this._items.set(untracked(this.items).slice(count));
52
+ return items;
53
+ }
54
+ if (!untracked(this.length)) return void 0;
55
+ const item = untracked(this.items).at(0);
56
+ this._items.set(untracked(this.items).slice(1));
57
+ return item;
58
+ }
59
+ removeFromBack(count) {
60
+ if (count !== void 0) {
61
+ if (count < 1) return [];
62
+ if (!untracked(this.length)) return [];
63
+ const start = count * -1;
64
+ const items = untracked(this.items).slice(start);
65
+ this._items.set(untracked(this.items).slice(0, start));
66
+ return items;
67
+ }
68
+ if (!untracked(this.length)) return void 0;
69
+ const item = untracked(this.items).at(-1);
70
+ this._items.set(untracked(this.items).slice(0, -1));
71
+ return item;
72
+ }
73
+ enqueue(...items) {
74
+ let list = [...untracked(this.items), ...items];
75
+ if (this.maxSize !== void 0 && list.length > this.maxSize) {
76
+ const delta = list.length - this.maxSize;
77
+ list = list.slice(delta);
78
+ }
79
+ this._items.set(list);
80
+ }
81
+ addToFront(...items) {
82
+ let list = [...items, ...untracked(this.items)];
83
+ if (this.maxSize !== void 0 && list.length > this.maxSize) {
84
+ const delta = list.length - this.maxSize;
85
+ list = list.slice(0, -delta);
86
+ }
87
+ this._items.set(list);
88
+ }
89
+ /**
90
+ * Checks if the item exists in the queue
91
+ * @param item
92
+ */
93
+ contains(item) {
94
+ return untracked(this.items).includes(item);
95
+ }
96
+ /**
97
+ * Remove all elements from the set
98
+ */
99
+ clear() {
100
+ this._items.set([]);
101
+ }
102
+ //</editor-fold>
103
+ /**
104
+ * Change the max size of the queue
105
+ * @param size - The new max size
106
+ */
107
+ setMaxSize(size) {
108
+ this._maxSize = size;
109
+ if (size === void 0 || untracked(this.length) <= size) return;
110
+ const delta = untracked(this.length) - size;
111
+ this._items.set(untracked(this.items).slice(delta));
112
+ }
113
+ }
114
+
115
+ export { SignalQueue };
@@ -0,0 +1,143 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+
3
+ class SignalSet {
4
+ constructor(values) {
5
+ this._set = signal(new Set(values));
6
+ this.value = this._set.asReadonly();
7
+ this.size = computed(() => this.value().size);
8
+ this.empty = computed(() => this.size() <= 0);
9
+ this.array = computed(() => Array.from(this.value()));
10
+ }
11
+ /** @inheritDoc */
12
+ [Symbol.iterator]() {
13
+ return this.value()[Symbol.iterator]();
14
+ }
15
+ getCopy() {
16
+ return new Set(untracked(this.value));
17
+ }
18
+ //<editor-fold desc="Actions">
19
+ /**
20
+ * Remove all keys not in the whitelist
21
+ * @param whitelist - The keys to keep
22
+ */
23
+ filter(whitelist) {
24
+ const length = whitelist && "size" in whitelist ? whitelist.size : whitelist?.length;
25
+ if (!length) {
26
+ return this.clear();
27
+ }
28
+ const whitelistSet = whitelist instanceof Set ? whitelist : new Set(whitelist);
29
+ const set = this.getCopy();
30
+ for (let value of untracked(this.value)) {
31
+ if (whitelistSet.has(value)) continue;
32
+ set.delete(value);
33
+ }
34
+ if (untracked(this.size) !== set.size) {
35
+ this._set.set(set);
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ /** Clear the collection */
41
+ clear() {
42
+ if (!untracked(this.size)) return false;
43
+ this._set.set(/* @__PURE__ */ new Set());
44
+ return true;
45
+ }
46
+ /**
47
+ * Add a value to the collection is not already present
48
+ * @param value - The value to add
49
+ * @return added - Returns true if the value was added
50
+ */
51
+ add(value) {
52
+ if (untracked(this.value).has(value)) return false;
53
+ const set = this.getCopy();
54
+ set.add(value);
55
+ this._set.set(set);
56
+ return true;
57
+ }
58
+ /**
59
+ * Add a list of values to the collection is not already present
60
+ * @param values - The values to add
61
+ * @return added - Returns true if any value was added
62
+ */
63
+ addRange(values) {
64
+ const set = this.getCopy();
65
+ const size = set.size;
66
+ values.forEach((v) => set.add(v));
67
+ if (set.size === size) return false;
68
+ this._set.set(set);
69
+ return true;
70
+ }
71
+ /**
72
+ * Reset the values in the collection to the provided list
73
+ * @param values - The values to set
74
+ * @return changed - Returns true if the collection changed
75
+ */
76
+ set(values = []) {
77
+ if (!values.length && !this.size) return false;
78
+ if (values.length === untracked(this.size)) {
79
+ const same = values.every((x) => this.has(x));
80
+ if (same) return false;
81
+ }
82
+ this._set.set(new Set(values));
83
+ return true;
84
+ }
85
+ /**
86
+ * Remove a value from the collection
87
+ * @param value - The key to remove
88
+ * @return removed - True if the value existed
89
+ */
90
+ delete(value) {
91
+ if (!untracked(this.value).has(value)) return false;
92
+ const set = this.getCopy();
93
+ set.delete(value);
94
+ this._set.set(set);
95
+ return true;
96
+ }
97
+ /**
98
+ * Remove values from the collection
99
+ * @param values - The values to remove
100
+ * @return removed - True if any values were removed
101
+ */
102
+ deleteRange(values) {
103
+ const set = this.getCopy();
104
+ const size = set.size;
105
+ values.forEach((v) => set.delete(v));
106
+ if (set.size === size) return false;
107
+ this._set.set(set);
108
+ return true;
109
+ }
110
+ /**
111
+ * Toggle a value in the set
112
+ * @param value - The value to toggle
113
+ * @param state - A forced state (`true` = always add, `false` = always delete)
114
+ * @returns The applied change (`true` = item added, `false` = item removed, `undefined` = nothing changed)
115
+ */
116
+ toggle(value, state) {
117
+ if (untracked(this.value).has(value)) {
118
+ if (state === true) return void 0;
119
+ const set2 = this.getCopy();
120
+ set2.delete(value);
121
+ this._set.set(set2);
122
+ return false;
123
+ }
124
+ if (state === false) return void 0;
125
+ const set = this.getCopy();
126
+ set.add(value);
127
+ this._set.set(set);
128
+ return true;
129
+ }
130
+ /** Manually modify the inner collection */
131
+ modify(modify) {
132
+ const set = this.getCopy();
133
+ modify(set);
134
+ this._set.set(set);
135
+ }
136
+ /** @inheritDoc */
137
+ has(value) {
138
+ return computed(() => this.value().has(value));
139
+ }
140
+ //</editor-fold>
141
+ }
142
+
143
+ export { SignalSet };
@@ -0,0 +1,90 @@
1
+ import { signal, computed, untracked } from '@angular/core';
2
+ import { pairwiseSignal } from '../operators.mjs';
3
+
4
+ class SignalStack {
5
+ constructor() {
6
+ this._items = signal([]);
7
+ /** A signal containing the items of the stack from bottom to top */
8
+ this.items = this._items.asReadonly();
9
+ /** A signal emitting the number of items in the stack */
10
+ this.length = computed(() => this.items().length);
11
+ /** A signal emitting true if there are no items in the stack */
12
+ this.empty = computed(() => this.length() <= 0);
13
+ this.top = computed(() => this.items().at(-1));
14
+ this.bottom = computed(() => this.items().at(0));
15
+ const delta = pairwiseSignal(this.items, []);
16
+ this.topDelta = computed(() => {
17
+ const [prev, next] = delta();
18
+ const item = next.at(-1);
19
+ return { item, added: !item ? false : !prev.includes(item) };
20
+ }, { equal: (a, b) => a.item === b.item });
21
+ this.bottomDelta = computed(() => {
22
+ const [prev, next] = delta();
23
+ const item = next.at(0);
24
+ return { item, added: !item ? false : !prev.includes(item) };
25
+ }, { equal: (a, b) => a.item === b.item });
26
+ }
27
+ //</editor-fold>
28
+ //<editor-fold desc="Actions">
29
+ /**
30
+ * Removes an item from the scheduler
31
+ * @param item
32
+ */
33
+ removeItem(item) {
34
+ const index = untracked(this.items).indexOf(item);
35
+ if (index < 0) return false;
36
+ const arr = [...untracked(this.items)];
37
+ arr.splice(index, 1);
38
+ this._items.set(arr);
39
+ return true;
40
+ }
41
+ removeFromBottom(count) {
42
+ if (count !== void 0) {
43
+ if (count < 1) return [];
44
+ if (!untracked(this.length)) return [];
45
+ const items = untracked(this.items).slice(0, count);
46
+ this._items.set(untracked(this.items).slice(count));
47
+ return items;
48
+ }
49
+ if (!untracked(this.length)) return void 0;
50
+ const item = untracked(this.items).at(0);
51
+ this._items.set(untracked(this.items).slice(1));
52
+ return item;
53
+ }
54
+ pop(count) {
55
+ if (count !== void 0) {
56
+ if (count < 1) return [];
57
+ if (!untracked(this.length)) return [];
58
+ const start = count * -1;
59
+ const items = untracked(this.items).slice(start);
60
+ this._items.set(untracked(this.items).slice(0, start));
61
+ return items;
62
+ }
63
+ if (!untracked(this.length)) return void 0;
64
+ const item = untracked(this.items).at(-1);
65
+ this._items.set(untracked(this.items).slice(0, -1));
66
+ return item;
67
+ }
68
+ push(...items) {
69
+ this._items.set([...untracked(this.items), ...items]);
70
+ }
71
+ addToBottom(...items) {
72
+ this._items.set([...items, ...untracked(this.items)]);
73
+ }
74
+ /**
75
+ * Checks if the item exists in the queue
76
+ * @param item
77
+ */
78
+ contains(item) {
79
+ return untracked(this.items).includes(item);
80
+ }
81
+ /**
82
+ * Remove all elements from the set
83
+ */
84
+ clear() {
85
+ this._items.set([]);
86
+ }
87
+ //</editor-fold>
88
+ }
89
+
90
+ export { SignalStack };
package/dist/index.d.ts CHANGED
@@ -64,15 +64,16 @@ declare function subjectToSignal<T>(subject: BehaviorSubject<T>): Signal<T>;
64
64
  */
65
65
  declare function subjectToSignal<T>(subject: Subject<T>, initial: T): Signal<T>;
66
66
 
67
- /** @category Loading Signal */
68
- type LoadingSignal<T> = Signal<T | undefined> & {
67
+ interface LoadingSignalProps {
69
68
  /** The current error state */
70
69
  readonly error: Signal<Error | undefined>;
71
70
  /** The current loading state */
72
71
  readonly loading: Signal<boolean>;
73
72
  /** Dispose the underlying request / action */
74
73
  readonly dispose: () => void;
75
- };
74
+ }
75
+ /** @category Loading Signal */
76
+ type LoadingSignal<T> = Signal<T | undefined> & LoadingSignalProps;
76
77
  /**
77
78
  * Options for creating a LoadingSignal
78
79
  * @category Loading Signal
package/dist/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ export { debouncedSignal, debouncedThrottledSignal, searchSignal, throttledSignal } from './async.mjs';
2
+ export { subjectToSignal } from './interop.mjs';
3
+ export { loadingSignal, toLoadingSignal } from './loading-signal.mjs';
4
+ export { SignalMap } from './data-structures/signal-map.mjs';
5
+ export { SignalSet } from './data-structures/signal-set.mjs';
6
+ export { SignalQueue } from './data-structures/signal-queue.mjs';
7
+ export { SignalStack } from './data-structures/signal-stack.mjs';
8
+ export { requestState } from './request-state/constructors.mjs';
9
+ export { IRequestState, IValueRequestState } from './request-state/request-state.mjs';
10
+ export { AsyncRequestState } from './request-state/async-request-state.mjs';
11
+ export { EmptyRequestState } from './request-state/empty-request-state.mjs';
12
+ export { ErrorRequestState } from './request-state/error-request-state.mjs';
13
+ export { LoadingRequestState } from './request-state/loading-request-state.mjs';
14
+ export { StaticRequestState } from './request-state/static-request-state.mjs';
15
+ export { WritableRequestState } from './request-state/writable-request-state.mjs';
16
+ export { RequestTracker, ValueRequestTracker, requestTracker } from './request-state/request-tracker.mjs';
@@ -0,0 +1,11 @@
1
+ import { signal } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
3
+
4
+ function subjectToSignal(subject, initial) {
5
+ const init = subject instanceof BehaviorSubject ? subject.value : initial;
6
+ const sig = signal(init);
7
+ subject.subscribe((val) => sig.set(val));
8
+ return sig;
9
+ }
10
+
11
+ export { subjectToSignal };
@@ -0,0 +1,68 @@
1
+ import { signal, assertInInjectionContext, DestroyRef, inject } from '@angular/core';
2
+ import { ILoadingState } from '@juulsgaard/rxjs-tools';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+ import { parseError } from '@juulsgaard/ts-tools';
5
+
6
+ function loadingSignal(value, options) {
7
+ const loadingSignal2 = value;
8
+ loadingSignal2.loading = options?.loading ?? signal(false);
9
+ loadingSignal2.error = options?.error ?? signal(void 0);
10
+ loadingSignal2.dispose = options?.dispose ?? (() => void 0);
11
+ return loadingSignal2;
12
+ }
13
+ function toLoadingSignal(loadingState, options) {
14
+ const injector = options?.injector;
15
+ const needsInjector = !!options?.autoDispose;
16
+ if (needsInjector && !injector) assertInInjectionContext(toLoadingSignal);
17
+ let state;
18
+ if (loadingState instanceof ILoadingState) {
19
+ state = fromLoadingState(loadingState);
20
+ } else if (loadingState instanceof Promise) {
21
+ state = fromPromise(loadingState);
22
+ } else {
23
+ state = fromObservable(loadingState);
24
+ }
25
+ if (options?.autoDispose) {
26
+ const destroy = injector?.get(DestroyRef) ?? inject(DestroyRef);
27
+ destroy.onDestroy(() => state.dispose());
28
+ }
29
+ return state;
30
+ }
31
+ function fromObservable(value$) {
32
+ const value = signal(void 0);
33
+ const loading = signal(true);
34
+ const error = signal(void 0);
35
+ const sub = value$.subscribe({
36
+ next: (val) => {
37
+ sub.unsubscribe();
38
+ loading.set(false);
39
+ value.set(val);
40
+ },
41
+ error: (err) => {
42
+ sub.unsubscribe();
43
+ loading.set(false);
44
+ error.set(parseError(err));
45
+ },
46
+ complete: () => {
47
+ sub.unsubscribe();
48
+ loading.set(false);
49
+ }
50
+ });
51
+ return loadingSignal(value, { loading, error, dispose: () => sub.unsubscribe() });
52
+ }
53
+ function fromPromise(promise) {
54
+ const value = signal(void 0);
55
+ const loading = signal(true);
56
+ const error = signal(void 0);
57
+ promise.then((val) => value.set(val), (err) => error.set(parseError(err)));
58
+ promise.finally(() => loading.set(false));
59
+ return loadingSignal(value, { loading, error });
60
+ }
61
+ function fromLoadingState(state) {
62
+ const value = toSignal(state.result$, { manualCleanup: true });
63
+ const loading = toSignal(state.loading$, { manualCleanup: true, initialValue: true });
64
+ const error = toSignal(state.error$, { manualCleanup: true });
65
+ return loadingSignal(value, { loading, error, dispose: () => state.cancel() });
66
+ }
67
+
68
+ export { loadingSignal, toLoadingSignal };
@@ -0,0 +1,13 @@
1
+ import { computed } from '@angular/core';
2
+
3
+ function pairwiseSignal(inputSignal, firstValue) {
4
+ let prevValue = firstValue;
5
+ return computed(() => {
6
+ const nextValue = inputSignal();
7
+ const tuple = [prevValue, nextValue];
8
+ prevValue = nextValue;
9
+ return tuple;
10
+ });
11
+ }
12
+
13
+ export { pairwiseSignal };
@@ -0,0 +1,48 @@
1
+ import { ReplaySubject, isObservable, take, from } from 'rxjs';
2
+ import { signal, computed } from '@angular/core';
3
+ import { parseError, isString, isFunction } from '@juulsgaard/ts-tools';
4
+ import { CancelledError } from '@juulsgaard/rxjs-tools';
5
+ import { IValueRequestState } from './request-state.mjs';
6
+
7
+ class AsyncRequestState extends IValueRequestState {
8
+ constructor(request) {
9
+ super();
10
+ this._request$ = new ReplaySubject();
11
+ this.request$ = this._request$.asObservable();
12
+ this._result$ = new ReplaySubject();
13
+ this.result$ = this._result$.asObservable();
14
+ this._result = signal(void 0);
15
+ this.result = this._result.asReadonly();
16
+ this._loading = signal(true);
17
+ this.loading = this._loading.asReadonly();
18
+ this._error = signal(void 0);
19
+ this.error = this._error.asReadonly();
20
+ this.failed = computed(() => this._error() !== void 0);
21
+ const request$ = isObservable(request) ? request.pipe(take(1)) : from(request);
22
+ this.sub = request$.subscribe(this._request$);
23
+ this.request$.subscribe({
24
+ next: (value) => {
25
+ this._result$.next(value);
26
+ this._result.set(value);
27
+ },
28
+ error: (error) => {
29
+ this._error.set(parseError(error));
30
+ this._result$.complete();
31
+ this._loading.set(false);
32
+ },
33
+ complete: () => {
34
+ this._result$.complete();
35
+ this._loading.set(false);
36
+ }
37
+ });
38
+ }
39
+ cancel(error) {
40
+ if (this.sub.closed) return false;
41
+ this.sub.unsubscribe();
42
+ const fullError = isString(error) ? new CancelledError(error) : isFunction(error) ? error() : error;
43
+ this._request$.error(fullError);
44
+ return true;
45
+ }
46
+ }
47
+
48
+ export { AsyncRequestState };
@@ -0,0 +1,25 @@
1
+ import { isObservable } from 'rxjs';
2
+ import { WritableRequestState } from './writable-request-state.mjs';
3
+ import { AsyncRequestState } from './async-request-state.mjs';
4
+ import { StaticRequestState } from './static-request-state.mjs';
5
+ import { ErrorRequestState } from './error-request-state.mjs';
6
+ import '@angular/core';
7
+ import '@juulsgaard/ts-tools';
8
+
9
+ function valueRequestState(data) {
10
+ if (isObservable(data)) return new AsyncRequestState(data);
11
+ if (data instanceof Promise) return new AsyncRequestState(data);
12
+ return new StaticRequestState(data);
13
+ }
14
+ function errorRequestState(error) {
15
+ return new ErrorRequestState(error instanceof Error ? () => error : error);
16
+ }
17
+ function writableRequestState(onCancel) {
18
+ return new WritableRequestState(onCancel);
19
+ }
20
+ const compiled = valueRequestState;
21
+ compiled.writable = writableRequestState;
22
+ compiled.error = errorRequestState;
23
+ const requestState = compiled;
24
+
25
+ export { requestState };
@@ -0,0 +1,20 @@
1
+ import { EMPTY } from 'rxjs';
2
+ import { signal } from '@angular/core';
3
+ import { IValueRequestState } from './request-state.mjs';
4
+
5
+ class EmptyRequestState extends IValueRequestState {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.request$ = EMPTY;
9
+ this.result$ = this.request$;
10
+ this.result = signal(void 0).asReadonly();
11
+ this.loading = signal(false);
12
+ this.error = signal(void 0);
13
+ this.failed = signal(false);
14
+ }
15
+ cancel() {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ export { EmptyRequestState };
@@ -0,0 +1,20 @@
1
+ import { EMPTY, throwError } from 'rxjs';
2
+ import { signal } from '@angular/core';
3
+ import { IValueRequestState } from './request-state.mjs';
4
+
5
+ class ErrorRequestState extends IValueRequestState {
6
+ constructor(getError) {
7
+ super();
8
+ this.result$ = EMPTY;
9
+ this.result = signal(void 0).asReadonly();
10
+ this.loading = signal(false);
11
+ this.failed = signal(true);
12
+ this.error = signal(getError());
13
+ this.request$ = throwError(getError);
14
+ }
15
+ cancel() {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ export { ErrorRequestState };
@@ -0,0 +1,25 @@
1
+ import { Subject } from 'rxjs';
2
+ import { signal } from '@angular/core';
3
+ import { IValueRequestState } from './request-state.mjs';
4
+
5
+ class LoadingRequestState extends IValueRequestState {
6
+ constructor() {
7
+ super(...arguments);
8
+ this._request$ = new Subject();
9
+ this.request$ = this._request$.asObservable();
10
+ this.result$ = this.request$;
11
+ this.result = signal(void 0).asReadonly();
12
+ this._loading = signal(true);
13
+ this.loading = this._loading.asReadonly();
14
+ this.error = signal(void 0);
15
+ this.failed = signal(false);
16
+ }
17
+ cancel() {
18
+ if (this._request$.closed) return false;
19
+ this._request$.complete();
20
+ this._loading.set(false);
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export { LoadingRequestState };
@@ -0,0 +1,39 @@
1
+ import { first } from 'rxjs';
2
+ import { parseError } from '@juulsgaard/ts-tools';
3
+
4
+ class IRequestState {
5
+ /**
6
+ * Subscribe to the underlying request Observable
7
+ * @param observer
8
+ */
9
+ subscribe(observer) {
10
+ return this.request$.subscribe(observer);
11
+ }
12
+ }
13
+ class IValueRequestState extends IRequestState {
14
+ subscribe(observer) {
15
+ return this.request$.subscribe(observer);
16
+ }
17
+ then(onValue, onError) {
18
+ this.request$.pipe(first()).subscribe({
19
+ next: (v) => onValue(v),
20
+ error: (e) => onError?.(parseError(e))
21
+ });
22
+ return this;
23
+ }
24
+ catch(onError) {
25
+ this.request$.subscribe({
26
+ error: (e) => onError(parseError(e))
27
+ });
28
+ return this;
29
+ }
30
+ finally(onComplete) {
31
+ this.request$.subscribe({
32
+ error: (e) => onComplete(parseError(e)),
33
+ complete: () => onComplete()
34
+ });
35
+ return this;
36
+ }
37
+ }
38
+
39
+ export { IRequestState, IValueRequestState };
@@ -0,0 +1,64 @@
1
+ import { computed, signal, untracked } from '@angular/core';
2
+ import { IValueRequestState } from './request-state.mjs';
3
+ import { requestState } from './constructors.mjs';
4
+
5
+ class RequestTracker {
6
+ constructor(request) {
7
+ /** Emits true if the current RequestState is loading */
8
+ this.loading = computed(() => this._request().loading());
9
+ /** Emits an error if the current RequestState is in an error state */
10
+ this.error = computed(() => this._request().error());
11
+ /** Emits true if the current RequestState has failed */
12
+ this.failed = computed(() => this._request().failed());
13
+ this._request = signal(request ?? requestState.empty());
14
+ this.request = this._request.asReadonly();
15
+ }
16
+ /** The currently active RequestState */
17
+ set current(req) {
18
+ this.set(req);
19
+ }
20
+ get current() {
21
+ return untracked(this.request);
22
+ }
23
+ /**
24
+ * Set a new current RequestState
25
+ * @param state - The new RequestState
26
+ */
27
+ set(state) {
28
+ this._request.set(state);
29
+ }
30
+ /**
31
+ * Resets the Tracker to an Empty state
32
+ */
33
+ reset() {
34
+ this._request.set(requestState.empty());
35
+ }
36
+ }
37
+ class ValueRequestTracker extends RequestTracker {
38
+ set current(req) {
39
+ this.set(req);
40
+ }
41
+ get current() {
42
+ return untracked(this.request);
43
+ }
44
+ constructor(request) {
45
+ super();
46
+ this._valueRequest = signal(request ?? requestState.empty());
47
+ this.request = this._valueRequest.asReadonly();
48
+ }
49
+ set(req) {
50
+ super.set(req);
51
+ this._valueRequest.set(req);
52
+ }
53
+ reset() {
54
+ super.reset();
55
+ this.set(requestState.empty());
56
+ }
57
+ }
58
+ function requestTracker(request) {
59
+ if (!request) return new ValueRequestTracker();
60
+ if (request instanceof IValueRequestState) return new ValueRequestTracker(request);
61
+ return new RequestTracker(request);
62
+ }
63
+
64
+ export { RequestTracker, ValueRequestTracker, requestTracker };
@@ -0,0 +1,32 @@
1
+ import { of } from 'rxjs';
2
+ import { signal } from '@angular/core';
3
+ import { IValueRequestState } from './request-state.mjs';
4
+
5
+ class StaticRequestState extends IValueRequestState {
6
+ constructor(value) {
7
+ super();
8
+ this.value = value;
9
+ this.loading = signal(false);
10
+ this.error = signal(void 0);
11
+ this.failed = signal(false);
12
+ this.request$ = of(value);
13
+ this.result$ = this.request$;
14
+ this.result = signal(value);
15
+ }
16
+ cancel() {
17
+ return false;
18
+ }
19
+ then(next, _error) {
20
+ next(this.value);
21
+ return this;
22
+ }
23
+ catch(_onError) {
24
+ return this;
25
+ }
26
+ finally(onComplete) {
27
+ onComplete();
28
+ return this;
29
+ }
30
+ }
31
+
32
+ export { StaticRequestState };
@@ -0,0 +1,75 @@
1
+ import { ReplaySubject } from 'rxjs';
2
+ import { signal, computed } from '@angular/core';
3
+ import { parseError, isString, isFunction } from '@juulsgaard/ts-tools';
4
+ import { CancelledError } from '@juulsgaard/rxjs-tools';
5
+ import { IValueRequestState } from './request-state.mjs';
6
+
7
+ class WritableRequestState extends IValueRequestState {
8
+ constructor(onCancel) {
9
+ super();
10
+ this.onCancel = onCancel;
11
+ this._completed = false;
12
+ this._request$ = new ReplaySubject();
13
+ this.request$ = this._request$.asObservable();
14
+ this._result$ = new ReplaySubject();
15
+ this.result$ = this._result$.asObservable();
16
+ this._result = signal(void 0);
17
+ this.result = this._result.asReadonly();
18
+ this._loading = signal(true);
19
+ this.loading = this._loading.asReadonly();
20
+ this._error = signal(void 0);
21
+ this.error = this._error.asReadonly();
22
+ this.failed = computed(() => this._error() !== void 0);
23
+ this._cancelled = signal(false);
24
+ /** Emits true when the RequestState has been cancelled by a consumer, otherwise emits false */
25
+ this.cancelled = this._cancelled.asReadonly();
26
+ this._cancelled$ = new ReplaySubject();
27
+ /** Emits true when the RequestState has been cancelled by a consumer */
28
+ this.cancelled$ = this._cancelled$.asObservable();
29
+ }
30
+ get completed() {
31
+ return this._completed;
32
+ }
33
+ /**
34
+ * Create a Readonly version of the RequestState
35
+ */
36
+ asReadonly() {
37
+ return this;
38
+ }
39
+ /**
40
+ * Set the RequestState to an Error state
41
+ * @param error - The error for the RequestState
42
+ */
43
+ setError(error) {
44
+ if (this.completed) return;
45
+ this._completed = true;
46
+ this._request$.error(error);
47
+ this._result$.complete();
48
+ this._loading.set(false);
49
+ this._error.set(parseError(error));
50
+ }
51
+ /**
52
+ * Set the RequestState to a completed state with a given value
53
+ * @param value - The value to emit
54
+ */
55
+ setValue(value) {
56
+ if (this.completed) return;
57
+ this._completed = true;
58
+ this._request$.next(value);
59
+ this._request$.complete();
60
+ this._loading.set(false);
61
+ this._result.set(value);
62
+ }
63
+ cancel(error) {
64
+ if (this.completed) return false;
65
+ const fullError = isString(error) ? new CancelledError(error) : isFunction(error) ? error() : error;
66
+ this.setError(fullError);
67
+ this._cancelled.set(true);
68
+ this._cancelled$.next(true);
69
+ this._cancelled$.complete();
70
+ this.onCancel?.();
71
+ return true;
72
+ }
73
+ }
74
+
75
+ export { WritableRequestState };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@juulsgaard/signal-tools",
3
- "version": "18.0.1",
3
+ "version": "18.0.2",
4
4
  "description": "Tools for working with Angular signals",
5
5
  "repository": "git@github.com:Juulsgaard/signal-tools.git",
6
6
  "homepage": "https://juulsgaard.github.io/signal-tools/",
7
- "main": "dist/index.js",
7
+ "main": "dist/index.mjs",
8
8
  "types": "dist/index.d.ts",
9
9
  "type": "module",
10
10
  "sideEffects": false,
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "peerDependencies": {
17
17
  "@angular/core": "^18.2.8",
18
- "@juulsgaard/ts-tools": "^1.6.4",
18
+ "@juulsgaard/ts-tools": "^1.6.5",
19
19
  "@juulsgaard/rxjs-tools": "^1.7.6",
20
20
  "rxjs": "^7.8.1",
21
21
  "tslib": "^2.7.0",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "devDependencies": {
25
25
  "@angular/core": "18.2.8",
26
+ "@juulsgaard/ts-tools": "1.6.5",
26
27
  "@juulsgaard/rxjs-tools": "1.7.6",
27
28
  "@types/jest": "^29.5.13",
28
29
  "jest": "^29.7.0",