@signaltree/core 6.3.0 → 7.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 +54 -0
- package/dist/enhancers/entities/entities.js +3 -30
- package/dist/index.js +4 -0
- package/dist/lib/internals/batch-scope.js +8 -0
- package/dist/lib/internals/materialize-markers.js +66 -0
- package/dist/lib/internals/merge-derived.js +59 -0
- package/dist/lib/markers/derived.js +6 -0
- package/dist/lib/markers/status.js +65 -0
- package/dist/lib/markers/stored.js +105 -0
- package/dist/lib/signal-tree.js +129 -5
- package/package.json +2 -2
- package/src/index.d.ts +6 -0
- package/src/lib/internals/batch-scope.d.ts +3 -0
- package/src/lib/internals/builder-types.d.ts +13 -0
- package/src/lib/internals/derived-types.d.ts +10 -0
- package/src/lib/internals/materialize-markers.d.ts +4 -0
- package/src/lib/internals/merge-derived.d.ts +4 -0
- package/src/lib/markers/derived.d.ts +8 -0
- package/src/lib/markers/index.d.ts +3 -0
- package/src/lib/markers/status.d.ts +31 -0
- package/src/lib/markers/stored.d.ts +23 -0
- package/src/lib/signal-tree.d.ts +5 -2
- package/src/lib/types.d.ts +4 -2
- package/src/lib/utils.d.ts +2 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
BUSINESS SOURCE LICENSE 1.1
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jonathan D Borgia
|
|
4
|
+
|
|
5
|
+
This Business Source License 1.1 ("License") governs the use of the software and associated documentation files (the "Software"). You are granted a limited license to use the Software under the terms of this License.
|
|
6
|
+
|
|
7
|
+
1. Definitions
|
|
8
|
+
|
|
9
|
+
"Change Date" means the date on which the Change License set out in section 6 will apply to the Software. The Change Date for this release is 2028-09-05.
|
|
10
|
+
|
|
11
|
+
"Change License" means the open source license that will apply to the Software on and after the Change Date. The Change License for this release is the MIT License.
|
|
12
|
+
|
|
13
|
+
"Licensor" means the copyright owner granting rights under this License (Jonathan D Borgia).
|
|
14
|
+
|
|
15
|
+
"You" ("Licensee") means an individual or legal entity exercising rights under this License who has not violated the terms of this License or had their rights terminated.
|
|
16
|
+
|
|
17
|
+
2. License Grant
|
|
18
|
+
|
|
19
|
+
Subject to the terms and conditions of this License, Licensor hereby grants You a non-exclusive, non-transferable, worldwide license to use, reproduce, display, perform, and distribute the Software, and to make modifications and derivative works for internal use, until the Change Date.
|
|
20
|
+
|
|
21
|
+
3. Commercial Use
|
|
22
|
+
|
|
23
|
+
You may use the Software in commercial applications, including for providing services, selling products that include the Software, or otherwise exploiting the Software commercially, subject to the other terms of this License.
|
|
24
|
+
|
|
25
|
+
4. Limitations and Conditions
|
|
26
|
+
|
|
27
|
+
a. You may not remove or alter this License, the copyright notice, or notices of the Change Date.
|
|
28
|
+
|
|
29
|
+
b. You may not publicly offer a modified version of the Software that would directly compete with Licensor's public offering of the Software if doing so would circumvent the intent of this License.
|
|
30
|
+
|
|
31
|
+
c. Except as expressly provided in this License, no rights are granted to You under any patent or trademark of Licensor.
|
|
32
|
+
|
|
33
|
+
5. Disclaimer and Limitation of Liability
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. TO THE FULLEST EXTENT PERMITTED BY LAW, LICENSOR WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM OR RELATING TO THE SOFTWARE.
|
|
36
|
+
|
|
37
|
+
6. Change License
|
|
38
|
+
|
|
39
|
+
On and after the Change Date specified above, the Software will be licensed under the Change License (MIT License) on the same terms and conditions as set forth by that Change License.
|
|
40
|
+
|
|
41
|
+
7. Governing Law
|
|
42
|
+
|
|
43
|
+
This License will be governed by and construed in accordance with the laws of the State of New York, USA, without regard to conflict of law principles.
|
|
44
|
+
|
|
45
|
+
8. Accepting this License
|
|
46
|
+
|
|
47
|
+
You accept this License by copying, modifying, or distributing the Software or any portion thereof.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
LICENSE NOTE
|
|
52
|
+
|
|
53
|
+
- Original license file replaced on 2025-09-05 to Business Source License 1.1. Change Date: 2028-09-05. Change License: MIT.
|
|
54
|
+
or standard modifications for your own applications.
|
|
@@ -1,38 +1,11 @@
|
|
|
1
|
-
import { createEntitySignal } from '../../lib/entity-signal.js';
|
|
2
|
-
import { getPathNotifier } from '../../lib/path-notifier.js';
|
|
3
|
-
|
|
4
|
-
function isEntityMapMarker(value) {
|
|
5
|
-
return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
|
|
6
|
-
}
|
|
7
1
|
function entities(config = {}) {
|
|
2
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
3
|
+
console.warn('SignalTree: entities() enhancer is deprecated in v7. ' + 'EntityMap markers are now automatically processed. ' + 'Remove .with(entities()) from your code. ' + 'This enhancer will be removed in v8.');
|
|
4
|
+
}
|
|
8
5
|
const {
|
|
9
6
|
enabled = true
|
|
10
7
|
} = config;
|
|
11
8
|
return tree => {
|
|
12
|
-
if (!enabled) {
|
|
13
|
-
tree.__entitiesEnabled = true;
|
|
14
|
-
return tree;
|
|
15
|
-
}
|
|
16
|
-
const notifier = getPathNotifier();
|
|
17
|
-
function materialize(node, path = []) {
|
|
18
|
-
if (!node || typeof node !== 'object') return;
|
|
19
|
-
for (const [k, v] of Object.entries(node)) {
|
|
20
|
-
if (isEntityMapMarker(v)) {
|
|
21
|
-
const cfg = v.__entityMapConfig ?? {};
|
|
22
|
-
const sig = createEntitySignal(cfg, notifier, path.concat(k).join('.'));
|
|
23
|
-
try {
|
|
24
|
-
node[k] = sig;
|
|
25
|
-
} catch {}
|
|
26
|
-
try {
|
|
27
|
-
tree[k] = sig;
|
|
28
|
-
} catch {}
|
|
29
|
-
} else if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
30
|
-
materialize(v, path.concat(k));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
materialize(tree.state);
|
|
35
|
-
materialize(tree.$);
|
|
36
9
|
tree.__entitiesEnabled = true;
|
|
37
10
|
return tree;
|
|
38
11
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree.js';
|
|
2
2
|
export { ENHANCER_META, entityMap } from './lib/types.js';
|
|
3
|
+
export { isDerivedMarker } from './lib/markers/derived.js';
|
|
4
|
+
export { LoadingState, isStatusMarker, status } from './lib/markers/status.js';
|
|
5
|
+
export { isStoredMarker, stored } from './lib/markers/stored.js';
|
|
6
|
+
export { registerMarkerProcessor } from './lib/internals/materialize-markers.js';
|
|
3
7
|
export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
|
|
4
8
|
export { createEditSession } from './lib/edit-session.js';
|
|
5
9
|
export { getPathNotifier } from './lib/path-notifier.js';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isSignal } from '@angular/core';
|
|
2
|
+
import { createEntitySignal } from '../entity-signal.js';
|
|
3
|
+
import { createStatusSignal, isStatusMarker } from '../markers/status.js';
|
|
4
|
+
import { createStoredSignal, isStoredMarker } from '../markers/stored.js';
|
|
5
|
+
import { getPathNotifier } from '../path-notifier.js';
|
|
6
|
+
import { isNodeAccessor } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
function isEntityMapMarker(value) {
|
|
9
|
+
return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
|
|
10
|
+
}
|
|
11
|
+
const MARKER_PROCESSORS = [];
|
|
12
|
+
function registerMarkerProcessor(check, create) {
|
|
13
|
+
MARKER_PROCESSORS.push({
|
|
14
|
+
check,
|
|
15
|
+
create: create
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
registerMarkerProcessor(isEntityMapMarker, (marker, notifier, path) => {
|
|
19
|
+
const cfg = marker.__entityMapConfig ?? {};
|
|
20
|
+
return createEntitySignal(cfg, notifier, path);
|
|
21
|
+
});
|
|
22
|
+
registerMarkerProcessor(isStatusMarker, marker => createStatusSignal(marker));
|
|
23
|
+
registerMarkerProcessor(isStoredMarker, marker => createStoredSignal(marker));
|
|
24
|
+
function materializeMarkers(node, notifier, path = []) {
|
|
25
|
+
if (node == null) return;
|
|
26
|
+
if (typeof node !== 'object' && typeof node !== 'function') return;
|
|
27
|
+
if (isSignal(node)) return;
|
|
28
|
+
const isAccessor = typeof node === 'function' && isNodeAccessor(node);
|
|
29
|
+
if (typeof node === 'function' && !isAccessor) return;
|
|
30
|
+
const getNotifier = () => {
|
|
31
|
+
if (!notifier) {
|
|
32
|
+
notifier = getPathNotifier();
|
|
33
|
+
}
|
|
34
|
+
return notifier;
|
|
35
|
+
};
|
|
36
|
+
const keys = Object.keys(node);
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = node[key];
|
|
39
|
+
const currentPath = [...path, key];
|
|
40
|
+
const pathString = currentPath.join('.');
|
|
41
|
+
let processed = false;
|
|
42
|
+
for (const processor of MARKER_PROCESSORS) {
|
|
43
|
+
if (processor.check(value)) {
|
|
44
|
+
try {
|
|
45
|
+
const materialized = processor.create(value, getNotifier(), pathString);
|
|
46
|
+
node[key] = materialized;
|
|
47
|
+
processed = true;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
50
|
+
console.error(`SignalTree: Failed to materialize marker at "${pathString}"`, err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!processed && value != null) {
|
|
57
|
+
if (isNodeAccessor(value)) {
|
|
58
|
+
materializeMarkers(value, notifier, currentPath);
|
|
59
|
+
} else if (typeof value === 'object' && !Array.isArray(value) && !isSignal(value)) {
|
|
60
|
+
materializeMarkers(value, notifier, currentPath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { materializeMarkers, registerMarkerProcessor };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { computed, isSignal } from '@angular/core';
|
|
2
|
+
import { isDerivedMarker } from '../markers/derived.js';
|
|
3
|
+
|
|
4
|
+
function isSignalLike(value) {
|
|
5
|
+
return isSignal(value);
|
|
6
|
+
}
|
|
7
|
+
function ensurePathAndGetTarget($, path) {
|
|
8
|
+
if (!path) return $;
|
|
9
|
+
const parts = path.split('.');
|
|
10
|
+
let current = $;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (!(part in current)) {
|
|
13
|
+
current[part] = {};
|
|
14
|
+
}
|
|
15
|
+
current = current[part];
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
function mergeDerivedState($, derivedDef, path = '') {
|
|
20
|
+
if (!derivedDef || typeof derivedDef !== 'object') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, value] of Object.entries(derivedDef)) {
|
|
24
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
25
|
+
if (isDerivedMarker(value)) {
|
|
26
|
+
const target = ensurePathAndGetTarget($, path);
|
|
27
|
+
if (key in target && isSignalLike(target[key])) {
|
|
28
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
29
|
+
console.warn(`SignalTree: Derived "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
target[key] = computed(value.factory);
|
|
33
|
+
} else if (isSignalLike(value)) {
|
|
34
|
+
const target = ensurePathAndGetTarget($, path);
|
|
35
|
+
if (key in target && isSignalLike(target[key])) {
|
|
36
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
37
|
+
console.warn(`SignalTree: Derived signal "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
target[key] = value;
|
|
41
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
42
|
+
const target = ensurePathAndGetTarget($, path);
|
|
43
|
+
if (!(key in target)) {
|
|
44
|
+
target[key] = {};
|
|
45
|
+
} else if (isSignalLike(target[key])) {
|
|
46
|
+
throw new Error(`SignalTree: Cannot merge derived object into "${currentPath}" ` + `because source is a signal. Either make source an object or use a different key.`);
|
|
47
|
+
}
|
|
48
|
+
mergeDerivedState($, value, currentPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function applyDerivedFactories($, factories) {
|
|
53
|
+
for (const factory of factories) {
|
|
54
|
+
const derivedDef = factory($);
|
|
55
|
+
mergeDerivedState($, derivedDef);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { applyDerivedFactories, mergeDerivedState };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { signal, computed } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
const STATUS_MARKER = Symbol('STATUS_MARKER');
|
|
4
|
+
var LoadingState;
|
|
5
|
+
(function (LoadingState) {
|
|
6
|
+
LoadingState["NotLoaded"] = "NOT_LOADED";
|
|
7
|
+
LoadingState["Loading"] = "LOADING";
|
|
8
|
+
LoadingState["Loaded"] = "LOADED";
|
|
9
|
+
LoadingState["Error"] = "ERROR";
|
|
10
|
+
})(LoadingState || (LoadingState = {}));
|
|
11
|
+
function status(initialState = LoadingState.NotLoaded) {
|
|
12
|
+
return {
|
|
13
|
+
[STATUS_MARKER]: true,
|
|
14
|
+
initialState
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function isStatusMarker(value) {
|
|
18
|
+
return value !== null && typeof value === 'object' && STATUS_MARKER in value && value[STATUS_MARKER] === true;
|
|
19
|
+
}
|
|
20
|
+
function createStatusSignal(marker) {
|
|
21
|
+
const stateSignal = signal(marker.initialState);
|
|
22
|
+
const errorSignal = signal(null);
|
|
23
|
+
let _isNotLoaded = null;
|
|
24
|
+
let _isLoading = null;
|
|
25
|
+
let _isLoaded = null;
|
|
26
|
+
let _isError = null;
|
|
27
|
+
return {
|
|
28
|
+
state: stateSignal,
|
|
29
|
+
error: errorSignal,
|
|
30
|
+
get isNotLoaded() {
|
|
31
|
+
return _isNotLoaded ??= computed(() => stateSignal() === LoadingState.NotLoaded);
|
|
32
|
+
},
|
|
33
|
+
get isLoading() {
|
|
34
|
+
return _isLoading ??= computed(() => stateSignal() === LoadingState.Loading);
|
|
35
|
+
},
|
|
36
|
+
get isLoaded() {
|
|
37
|
+
return _isLoaded ??= computed(() => stateSignal() === LoadingState.Loaded);
|
|
38
|
+
},
|
|
39
|
+
get isError() {
|
|
40
|
+
return _isError ??= computed(() => stateSignal() === LoadingState.Error);
|
|
41
|
+
},
|
|
42
|
+
setNotLoaded() {
|
|
43
|
+
stateSignal.set(LoadingState.NotLoaded);
|
|
44
|
+
errorSignal.set(null);
|
|
45
|
+
},
|
|
46
|
+
setLoading() {
|
|
47
|
+
stateSignal.set(LoadingState.Loading);
|
|
48
|
+
errorSignal.set(null);
|
|
49
|
+
},
|
|
50
|
+
setLoaded() {
|
|
51
|
+
stateSignal.set(LoadingState.Loaded);
|
|
52
|
+
errorSignal.set(null);
|
|
53
|
+
},
|
|
54
|
+
setError(err) {
|
|
55
|
+
stateSignal.set(LoadingState.Error);
|
|
56
|
+
errorSignal.set(err);
|
|
57
|
+
},
|
|
58
|
+
reset() {
|
|
59
|
+
stateSignal.set(LoadingState.NotLoaded);
|
|
60
|
+
errorSignal.set(null);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { LoadingState, STATUS_MARKER, createStatusSignal, isStatusMarker, status };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
const STORED_MARKER = Symbol('STORED_MARKER');
|
|
4
|
+
function stored(key, defaultValue, options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
[STORED_MARKER]: true,
|
|
7
|
+
key,
|
|
8
|
+
defaultValue,
|
|
9
|
+
options
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function isStoredMarker(value) {
|
|
13
|
+
return value !== null && typeof value === 'object' && STORED_MARKER in value && value[STORED_MARKER] === true;
|
|
14
|
+
}
|
|
15
|
+
function createStoredSignal(marker) {
|
|
16
|
+
const {
|
|
17
|
+
key,
|
|
18
|
+
defaultValue,
|
|
19
|
+
options: {
|
|
20
|
+
serialize = JSON.stringify,
|
|
21
|
+
deserialize = JSON.parse,
|
|
22
|
+
debounceMs = 100
|
|
23
|
+
}
|
|
24
|
+
} = marker;
|
|
25
|
+
const storage = marker.options.storage !== undefined ? marker.options.storage : typeof localStorage !== 'undefined' ? localStorage : null;
|
|
26
|
+
let initialValue = defaultValue;
|
|
27
|
+
if (storage) {
|
|
28
|
+
try {
|
|
29
|
+
const storedValue = storage.getItem(key);
|
|
30
|
+
if (storedValue !== null) {
|
|
31
|
+
initialValue = deserialize(storedValue);
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
35
|
+
console.warn(`SignalTree: Failed to read "${key}" from storage`, e);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const sig = signal(initialValue);
|
|
40
|
+
let pendingWrite = null;
|
|
41
|
+
let pendingValue;
|
|
42
|
+
const saveToStorage = value => {
|
|
43
|
+
if (!storage) return;
|
|
44
|
+
if (debounceMs === 0) {
|
|
45
|
+
queueMicrotask(() => {
|
|
46
|
+
try {
|
|
47
|
+
storage.setItem(key, serialize(value));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
50
|
+
console.warn(`SignalTree: Failed to save "${key}" to storage`, e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
pendingValue = value;
|
|
57
|
+
if (pendingWrite !== null) {
|
|
58
|
+
clearTimeout(pendingWrite);
|
|
59
|
+
}
|
|
60
|
+
pendingWrite = setTimeout(() => {
|
|
61
|
+
pendingWrite = null;
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
try {
|
|
64
|
+
storage.setItem(key, serialize(pendingValue));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
67
|
+
console.warn(`SignalTree: Failed to save "${key}" to storage`, e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}, debounceMs);
|
|
72
|
+
};
|
|
73
|
+
const storedSignal = () => sig();
|
|
74
|
+
storedSignal.set = value => {
|
|
75
|
+
sig.set(value);
|
|
76
|
+
saveToStorage(value);
|
|
77
|
+
};
|
|
78
|
+
storedSignal.update = fn => {
|
|
79
|
+
const newValue = fn(sig());
|
|
80
|
+
sig.set(newValue);
|
|
81
|
+
saveToStorage(newValue);
|
|
82
|
+
};
|
|
83
|
+
storedSignal.clear = () => {
|
|
84
|
+
sig.set(defaultValue);
|
|
85
|
+
if (storage) {
|
|
86
|
+
storage.removeItem(key);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
storedSignal.reload = () => {
|
|
90
|
+
if (!storage) return;
|
|
91
|
+
try {
|
|
92
|
+
const storedValue = storage.getItem(key);
|
|
93
|
+
if (storedValue !== null) {
|
|
94
|
+
sig.set(deserialize(storedValue));
|
|
95
|
+
} else {
|
|
96
|
+
sig.set(defaultValue);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
sig.set(defaultValue);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return storedSignal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { STORED_MARKER, createStoredSignal, isStoredMarker, stored };
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { signal, isSignal } from '@angular/core';
|
|
2
2
|
import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
|
|
3
|
+
import { batchScope } from './internals/batch-scope.js';
|
|
4
|
+
import { materializeMarkers } from './internals/materialize-markers.js';
|
|
5
|
+
import { applyDerivedFactories } from './internals/merge-derived.js';
|
|
6
|
+
import { isStatusMarker } from './markers/status.js';
|
|
7
|
+
import { isStoredMarker } from './markers/stored.js';
|
|
3
8
|
import { SignalMemoryManager } from './memory/memory-manager.js';
|
|
4
9
|
import { getPathNotifier } from './path-notifier.js';
|
|
5
10
|
import { SecurityValidator } from './security/security-validator.js';
|
|
@@ -88,16 +93,21 @@ function makeNodeAccessor(store) {
|
|
|
88
93
|
if (typeof arg === 'function') {
|
|
89
94
|
const updater = arg;
|
|
90
95
|
const current = unwrap(store);
|
|
91
|
-
recursiveUpdate(store, updater(current));
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
batchScope(() => recursiveUpdate(store, updater(current)));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) {
|
|
100
|
+
batchScope(() => recursiveUpdate(store, arg));
|
|
101
|
+
return;
|
|
94
102
|
}
|
|
103
|
+
recursiveUpdate(store, arg);
|
|
95
104
|
};
|
|
96
105
|
accessor[NODE_ACCESSOR_SYMBOL] = true;
|
|
97
106
|
for (const key of Object.keys(store)) {
|
|
98
107
|
Object.defineProperty(accessor, key, {
|
|
99
108
|
value: store[key],
|
|
100
109
|
enumerable: true,
|
|
110
|
+
writable: true,
|
|
101
111
|
configurable: true
|
|
102
112
|
});
|
|
103
113
|
}
|
|
@@ -142,6 +152,14 @@ function createSignalStore(obj, equalityFn) {
|
|
|
142
152
|
store[key] = value;
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
155
|
+
if (isStatusMarker(value)) {
|
|
156
|
+
store[key] = value;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (isStoredMarker(value)) {
|
|
160
|
+
store[key] = value;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
145
163
|
if (isSignal(value)) {
|
|
146
164
|
store[key] = value;
|
|
147
165
|
continue;
|
|
@@ -275,8 +293,114 @@ function create(initialState, config) {
|
|
|
275
293
|
}
|
|
276
294
|
return tree;
|
|
277
295
|
}
|
|
278
|
-
function signalTree(initialState,
|
|
279
|
-
|
|
296
|
+
function signalTree(initialState, configOrDerived) {
|
|
297
|
+
const isFactory = typeof configOrDerived === 'function';
|
|
298
|
+
const config = isFactory ? {} : configOrDerived ?? {};
|
|
299
|
+
const baseTree = create(initialState, config);
|
|
300
|
+
const builder = createBuilder(baseTree);
|
|
301
|
+
if (isFactory) {
|
|
302
|
+
return builder.derived(configOrDerived);
|
|
303
|
+
}
|
|
304
|
+
return builder;
|
|
305
|
+
}
|
|
306
|
+
function createBuilder(baseTree) {
|
|
307
|
+
const derivedQueue = [];
|
|
308
|
+
let isFinalized = false;
|
|
309
|
+
const finalize = () => {
|
|
310
|
+
if (isFinalized) return;
|
|
311
|
+
isFinalized = true;
|
|
312
|
+
materializeMarkers(baseTree.$);
|
|
313
|
+
materializeMarkers(baseTree.state);
|
|
314
|
+
if (derivedQueue.length > 0) {
|
|
315
|
+
applyDerivedFactories(baseTree.$, derivedQueue);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const builder = function (arg) {
|
|
319
|
+
if (arguments.length === 0) {
|
|
320
|
+
return baseTree();
|
|
321
|
+
}
|
|
322
|
+
return baseTree(arg);
|
|
323
|
+
};
|
|
324
|
+
builder[NODE_ACCESSOR_SYMBOL] = true;
|
|
325
|
+
Object.defineProperty(builder, 'state', {
|
|
326
|
+
get() {
|
|
327
|
+
finalize();
|
|
328
|
+
return baseTree.state;
|
|
329
|
+
},
|
|
330
|
+
enumerable: false,
|
|
331
|
+
configurable: true
|
|
332
|
+
});
|
|
333
|
+
Object.defineProperty(builder, '$', {
|
|
334
|
+
get() {
|
|
335
|
+
finalize();
|
|
336
|
+
return baseTree.$;
|
|
337
|
+
},
|
|
338
|
+
enumerable: false,
|
|
339
|
+
configurable: true
|
|
340
|
+
});
|
|
341
|
+
Object.defineProperty(builder, 'with', {
|
|
342
|
+
value: function (enhancer) {
|
|
343
|
+
const enhanced = baseTree.with(enhancer);
|
|
344
|
+
const newBuilder = createBuilder(enhanced);
|
|
345
|
+
for (const key of Object.keys(enhanced)) {
|
|
346
|
+
if (key !== '$' && key !== 'state' && key !== 'with' && key !== 'bind' && key !== 'destroy' && key !== 'derived') {
|
|
347
|
+
try {
|
|
348
|
+
newBuilder[key] = enhanced[key];
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const factory of derivedQueue) {
|
|
353
|
+
newBuilder.derived(factory);
|
|
354
|
+
}
|
|
355
|
+
return newBuilder;
|
|
356
|
+
},
|
|
357
|
+
enumerable: false,
|
|
358
|
+
writable: false,
|
|
359
|
+
configurable: true
|
|
360
|
+
});
|
|
361
|
+
if (typeof baseTree.bind === 'function') {
|
|
362
|
+
Object.defineProperty(builder, 'bind', {
|
|
363
|
+
value: baseTree.bind.bind(baseTree),
|
|
364
|
+
enumerable: false,
|
|
365
|
+
writable: false,
|
|
366
|
+
configurable: true
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
Object.defineProperty(builder, 'bind', {
|
|
370
|
+
value: () => builder,
|
|
371
|
+
enumerable: false,
|
|
372
|
+
writable: false,
|
|
373
|
+
configurable: true
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (typeof baseTree.destroy === 'function') {
|
|
377
|
+
Object.defineProperty(builder, 'destroy', {
|
|
378
|
+
value: baseTree.destroy.bind(baseTree),
|
|
379
|
+
enumerable: false,
|
|
380
|
+
writable: true,
|
|
381
|
+
configurable: true
|
|
382
|
+
});
|
|
383
|
+
} else {
|
|
384
|
+
Object.defineProperty(builder, 'destroy', {
|
|
385
|
+
value: () => {},
|
|
386
|
+
enumerable: false,
|
|
387
|
+
writable: true,
|
|
388
|
+
configurable: true
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
Object.defineProperty(builder, 'derived', {
|
|
392
|
+
value: function (factory) {
|
|
393
|
+
if (isFinalized) {
|
|
394
|
+
throw new Error('SignalTree: Cannot add derived() after tree.$ has been accessed. ' + 'Chain all .derived() calls before accessing $.');
|
|
395
|
+
}
|
|
396
|
+
derivedQueue.push(factory);
|
|
397
|
+
return builder;
|
|
398
|
+
},
|
|
399
|
+
enumerable: false,
|
|
400
|
+
writable: false,
|
|
401
|
+
configurable: true
|
|
402
|
+
});
|
|
403
|
+
return builder;
|
|
280
404
|
}
|
|
281
405
|
|
|
282
406
|
export { isNodeAccessor, signalTree };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Lightweight, type-safe signal-based state management for Angular. Core package providing hierarchical signal trees, basic entity management, and async actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -89,4 +89,4 @@
|
|
|
89
89
|
"src/**/*.d.ts",
|
|
90
90
|
"README.md"
|
|
91
91
|
]
|
|
92
|
-
}
|
|
92
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree';
|
|
2
2
|
export type { ISignalTree, SignalTree, SignalTreeBase, FullSignalTree, ProdSignalTree, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, } from './lib/types';
|
|
3
3
|
export { entityMap } from './lib/types';
|
|
4
|
+
export type { ProcessDerived, DeepMergeTree, DerivedFactory, } from './lib/internals/derived-types';
|
|
5
|
+
export type { SignalTreeBuilder } from './lib/internals/builder-types';
|
|
6
|
+
export { isDerivedMarker, type DerivedMarker, type DerivedType, } from './lib/markers/derived';
|
|
7
|
+
export { status, isStatusMarker, LoadingState, type StatusMarker, type StatusSignal, type StatusConfig, } from './lib/markers/status';
|
|
8
|
+
export { stored, isStoredMarker, type StoredMarker, type StoredSignal, type StoredOptions, } from './lib/markers/stored';
|
|
9
|
+
export { registerMarkerProcessor } from './lib/internals/materialize-markers';
|
|
4
10
|
export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
|
|
5
11
|
export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
|
|
6
12
|
export { getPathNotifier } from './lib/path-notifier';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProcessDerived } from './derived-types';
|
|
2
|
+
import type { ISignalTree, TreeNode } from '../types';
|
|
3
|
+
export interface SignalTreeBuilder<TSource, TAccum = TreeNode<TSource>> {
|
|
4
|
+
(): TSource;
|
|
5
|
+
(value: Partial<TSource>): void;
|
|
6
|
+
(updater: (current: TSource) => TSource): void;
|
|
7
|
+
readonly $: TAccum;
|
|
8
|
+
readonly state: TAccum;
|
|
9
|
+
with<TAdded>(enhancer: (tree: ISignalTree<TSource>) => ISignalTree<TSource> & TAdded): SignalTreeBuilder<TSource, TAccum> & TAdded;
|
|
10
|
+
bind(thisArg?: unknown): (value?: TSource) => TSource | void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
derived<TDerived extends object>(factory: ($: TAccum) => TDerived): SignalTreeBuilder<TSource, TAccum & ProcessDerived<TDerived>>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Signal } from '@angular/core';
|
|
2
|
+
import type { DerivedMarker } from '../markers/derived';
|
|
3
|
+
import type { TreeNode } from '../types';
|
|
4
|
+
export type ProcessDerived<T> = T extends DerivedMarker<infer R> ? Signal<R> : T extends Signal<infer S> ? Signal<S> : T extends object ? {
|
|
5
|
+
[P in keyof T]: ProcessDerived<T[P]>;
|
|
6
|
+
} : never;
|
|
7
|
+
export type DeepMergeTree<TSource, TDerived> = {
|
|
8
|
+
[K in keyof TSource | keyof TDerived]: K extends keyof TSource ? K extends keyof TDerived ? TSource[K] extends object ? TDerived[K] extends object ? TDerived[K] extends DerivedMarker<infer R> ? Signal<R> : TSource[K] & DeepMergeTree<TSource[K], ProcessDerived<TDerived[K]>> : TSource[K] : ProcessDerived<TDerived[K]> : TSource[K] : K extends keyof TDerived ? ProcessDerived<TDerived[K]> : never;
|
|
9
|
+
};
|
|
10
|
+
export type DerivedFactory<TSource, TDerived> = ($: TreeNode<TSource>) => TDerived;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { PathNotifier } from '../path-notifier';
|
|
2
|
+
export declare function registerMarkerProcessor<T, R>(check: (value: unknown) => value is T, create: (marker: T, notifier: PathNotifier, path: string) => R): void;
|
|
3
|
+
export declare function materializeMarkers(node: unknown, notifier?: PathNotifier, path?: string[]): void;
|
|
4
|
+
export declare function hasMarkers(node: unknown, visited?: WeakSet<object>): boolean;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { isDerivedMarker, getDerivedMarkerSymbol, type DerivedMarker, type DerivedType, } from './derived';
|
|
2
|
+
export { status, isStatusMarker, createStatusSignal, LoadingState, STATUS_MARKER, type StatusMarker, type StatusSignal, type StatusConfig, } from './status';
|
|
3
|
+
export { stored, isStoredMarker, createStoredSignal, STORED_MARKER, type StoredMarker, type StoredSignal, type StoredOptions, } from './stored';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Signal, WritableSignal } from '@angular/core';
|
|
2
|
+
export declare const STATUS_MARKER: unique symbol;
|
|
3
|
+
export declare enum LoadingState {
|
|
4
|
+
NotLoaded = "NOT_LOADED",
|
|
5
|
+
Loading = "LOADING",
|
|
6
|
+
Loaded = "LOADED",
|
|
7
|
+
Error = "ERROR"
|
|
8
|
+
}
|
|
9
|
+
export interface StatusConfig {
|
|
10
|
+
initialState?: LoadingState;
|
|
11
|
+
}
|
|
12
|
+
export interface StatusMarker {
|
|
13
|
+
[STATUS_MARKER]: true;
|
|
14
|
+
initialState: LoadingState;
|
|
15
|
+
}
|
|
16
|
+
export interface StatusSignal {
|
|
17
|
+
state: WritableSignal<LoadingState>;
|
|
18
|
+
error: WritableSignal<Error | null>;
|
|
19
|
+
isNotLoaded: Signal<boolean>;
|
|
20
|
+
isLoading: Signal<boolean>;
|
|
21
|
+
isLoaded: Signal<boolean>;
|
|
22
|
+
isError: Signal<boolean>;
|
|
23
|
+
setNotLoaded(): void;
|
|
24
|
+
setLoading(): void;
|
|
25
|
+
setLoaded(): void;
|
|
26
|
+
setError(error: Error): void;
|
|
27
|
+
reset(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function status(initialState?: LoadingState): StatusMarker;
|
|
30
|
+
export declare function isStatusMarker(value: unknown): value is StatusMarker;
|
|
31
|
+
export declare function createStatusSignal(marker: StatusMarker): StatusSignal;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const STORED_MARKER: unique symbol;
|
|
2
|
+
export interface StoredOptions<T> {
|
|
3
|
+
serialize?: (value: T) => string;
|
|
4
|
+
deserialize?: (stored: string) => T;
|
|
5
|
+
storage?: Storage | null;
|
|
6
|
+
debounceMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface StoredMarker<T> {
|
|
9
|
+
[STORED_MARKER]: true;
|
|
10
|
+
key: string;
|
|
11
|
+
defaultValue: T;
|
|
12
|
+
options: StoredOptions<T>;
|
|
13
|
+
}
|
|
14
|
+
export interface StoredSignal<T> {
|
|
15
|
+
(): T;
|
|
16
|
+
set(value: T): void;
|
|
17
|
+
update(fn: (current: T) => T): void;
|
|
18
|
+
clear(): void;
|
|
19
|
+
reload(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function stored<T>(key: string, defaultValue: T, options?: StoredOptions<T>): StoredMarker<T>;
|
|
22
|
+
export declare function isStoredMarker(value: unknown): value is StoredMarker<unknown>;
|
|
23
|
+
export declare function createStoredSignal<T>(marker: StoredMarker<T>): StoredSignal<T>;
|
package/src/lib/signal-tree.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SignalTreeBuilder } from './internals/builder-types';
|
|
2
|
+
import { ProcessDerived } from './internals/derived-types';
|
|
3
|
+
import type { TreeNode, TreeConfig, NodeAccessor } from './types';
|
|
2
4
|
export declare function isNodeAccessor(value: unknown): value is NodeAccessor<unknown>;
|
|
3
|
-
export declare function signalTree<T extends object>(initialState: T,
|
|
5
|
+
export declare function signalTree<T extends object, TDerived extends object>(initialState: T, derivedFactory: ($: TreeNode<T>) => TDerived): SignalTreeBuilder<T, TreeNode<T> & ProcessDerived<TDerived>>;
|
|
6
|
+
export declare function signalTree<T extends object>(initialState: T, config?: TreeConfig): SignalTreeBuilder<T, TreeNode<T>>;
|
package/src/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Signal, WritableSignal } from '@angular/core';
|
|
2
|
+
import { StatusMarker, StatusSignal } from './markers/status';
|
|
3
|
+
import { StoredMarker, StoredSignal } from './markers/stored';
|
|
2
4
|
import { SecurityValidatorConfig } from './security/security-validator';
|
|
3
5
|
export interface TimeTravelConfig {
|
|
4
6
|
enabled?: boolean;
|
|
@@ -28,11 +30,11 @@ declare module '@angular/core' {
|
|
|
28
30
|
}
|
|
29
31
|
export interface NodeAccessor<T> {
|
|
30
32
|
(): T;
|
|
31
|
-
(value: T): void;
|
|
33
|
+
(value: Partial<T>): void;
|
|
32
34
|
(updater: (current: T) => T): void;
|
|
33
35
|
}
|
|
34
36
|
export type TreeNode<T> = {
|
|
35
|
-
[K in keyof T]: T[K] extends EntityMapMarker<infer E, infer Key> ? EntitySignal<E, Key> : T[K] extends Primitive ? CallableWritableSignal<T[K]> : T[K] extends readonly unknown[] ? CallableWritableSignal<T[K]> : T[K] extends Date | RegExp | Map<any, any> | Set<any> | Error | ((...args: unknown[]) => unknown) ? CallableWritableSignal<T[K]> : T[K] extends object ? NodeAccessor<T[K]> & TreeNode<T[K]> : CallableWritableSignal<T[K]>;
|
|
37
|
+
[K in keyof T]: T[K] extends EntityMapMarker<infer E, infer Key> ? EntitySignal<E, Key> : T[K] extends StatusMarker ? StatusSignal : T[K] extends StoredMarker<infer V> ? StoredSignal<V> : T[K] extends Primitive ? CallableWritableSignal<T[K]> : T[K] extends readonly unknown[] ? CallableWritableSignal<T[K]> : T[K] extends Date | RegExp | Map<any, any> | Set<any> | Error | ((...args: unknown[]) => unknown) ? CallableWritableSignal<T[K]> : T[K] extends object ? NodeAccessor<T[K]> & TreeNode<T[K]> : CallableWritableSignal<T[K]>;
|
|
36
38
|
};
|
|
37
39
|
export interface ISignalTree<T> extends NodeAccessor<T> {
|
|
38
40
|
readonly state: TreeNode<T>;
|
package/src/lib/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WritableSignal } from '@angular/core';
|
|
2
2
|
import { deepEqual, isBuiltInObject, parsePath } from '@signaltree/shared';
|
|
3
3
|
export { deepEqual };
|
|
4
4
|
export { deepEqual as equal };
|
|
@@ -10,14 +10,7 @@ export interface MemoryManager {
|
|
|
10
10
|
cacheSignal(path: string, signal: WritableSignal<unknown>): void;
|
|
11
11
|
dispose(): void;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
(): T;
|
|
15
|
-
(value: T): void;
|
|
16
|
-
(updater: (current: T) => T): void;
|
|
17
|
-
}
|
|
18
|
-
export type TreeNode<T> = {
|
|
19
|
-
[K in keyof T]: T[K] extends readonly unknown[] ? WritableSignal<T[K]> : T[K] extends object ? T[K] extends Signal<unknown> ? T[K] : T[K] extends (...args: unknown[]) => unknown ? WritableSignal<T[K]> : NodeAccessor<T[K]> : WritableSignal<T[K]>;
|
|
20
|
-
};
|
|
13
|
+
import type { NodeAccessor, TreeNode } from './types';
|
|
21
14
|
export declare function isNodeAccessor(value: unknown): value is NodeAccessor<unknown>;
|
|
22
15
|
export declare function isAnySignal(value: unknown): boolean;
|
|
23
16
|
export declare function toWritableSignal<T>(node: NodeAccessor<T>, injector?: unknown): WritableSignal<T>;
|