@signaltree/guardrails 5.1.6 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -10
- package/dist/factories/index.js +6 -6
- package/dist/lib/guardrails.js +21 -13
- package/dist/noop.js +5 -5
- package/package.json +8 -8
- package/src/factories/index.d.ts +11 -12
- package/src/index.d.ts +2 -2
- package/src/lib/guardrails.d.ts +6 -3
- package/src/lib/types.d.ts +5 -5
- package/src/noop.d.ts +5 -6
- package/dist/lib/rules.js +0 -62
package/README.md
CHANGED
|
@@ -22,12 +22,14 @@ npm install --save-dev @signaltree/guardrails
|
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
24
|
import { signalTree } from '@signaltree/core';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
const tree = signalTree({ count: 0 }).with(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
25
|
+
import { guardrails } from '@signaltree/guardrails';
|
|
26
|
+
|
|
27
|
+
const tree = signalTree({ count: 0 }).with(
|
|
28
|
+
guardrails({
|
|
29
|
+
budgets: { maxUpdateTime: 16 },
|
|
30
|
+
hotPaths: { threshold: 10 },
|
|
31
|
+
})
|
|
32
|
+
);
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
## Using Factories
|
|
@@ -36,10 +38,14 @@ const tree = signalTree({ count: 0 }).with(withGuardrails({
|
|
|
36
38
|
import { signalTree } from '@signaltree/core';
|
|
37
39
|
import { createFeatureTree } from '@signaltree/guardrails/factories';
|
|
38
40
|
|
|
39
|
-
const tree = createFeatureTree(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const tree = createFeatureTree(
|
|
42
|
+
signalTree,
|
|
43
|
+
{ data: [] },
|
|
44
|
+
{
|
|
45
|
+
name: 'dashboard',
|
|
46
|
+
guardrails: true,
|
|
47
|
+
}
|
|
48
|
+
);
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
## Configuration
|
package/dist/factories/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { rules } from '../lib/rules.js';
|
|
1
|
+
import { rules, guardrails } from '../noop.js';
|
|
3
2
|
|
|
4
3
|
function isGuardrailsConfig(value) {
|
|
5
4
|
return Boolean(value) && typeof value === 'object';
|
|
@@ -33,17 +32,18 @@ function createFeatureTree(signalTree, initial, options) {
|
|
|
33
32
|
if (isDev || isTest) {
|
|
34
33
|
const guardrailsConfig = resolveGuardrailsConfig(options.guardrails);
|
|
35
34
|
if (guardrailsConfig) {
|
|
36
|
-
enhancers.push(
|
|
35
|
+
enhancers.push(guardrails(guardrailsConfig));
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
if (options.enhancers?.length) {
|
|
40
39
|
enhancers.push(...options.enhancers);
|
|
41
40
|
}
|
|
42
|
-
|
|
41
|
+
const tree = signalTree(initial);
|
|
42
|
+
let enhanced = tree;
|
|
43
43
|
for (const enhancer of enhancers) {
|
|
44
|
-
|
|
44
|
+
enhanced = enhanced.with(enhancer);
|
|
45
45
|
}
|
|
46
|
-
return
|
|
46
|
+
return enhanced;
|
|
47
47
|
}
|
|
48
48
|
function createAngularFeatureTree(signalTree, initial, options) {
|
|
49
49
|
const isDev = Boolean(ngDevMode);
|
package/dist/lib/guardrails.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getPathNotifier } from '@signaltree/core';
|
|
2
|
+
import { deepEqual } from '@signaltree/shared';
|
|
2
3
|
|
|
3
4
|
function isFunction(value) {
|
|
4
5
|
return typeof value === 'function';
|
|
@@ -26,7 +27,11 @@ function tryStructuredClone(value) {
|
|
|
26
27
|
return cloneFn(value);
|
|
27
28
|
} catch {}
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(JSON.stringify(value));
|
|
32
|
+
} catch {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
30
35
|
}
|
|
31
36
|
function isDevEnvironment() {
|
|
32
37
|
if (__DEV__ !== undefined) return __DEV__;
|
|
@@ -37,16 +42,16 @@ function isDevEnvironment() {
|
|
|
37
42
|
const MAX_TIMING_SAMPLES = 1000;
|
|
38
43
|
const RECOMPUTATION_WINDOW_MS = 1000;
|
|
39
44
|
const POLLING_INTERVAL_MS = 50;
|
|
40
|
-
function
|
|
41
|
-
return tree
|
|
45
|
+
function guardrails(config = {}) {
|
|
46
|
+
return function (tree) {
|
|
42
47
|
const enabled = resolveEnabledFlag(config.enabled);
|
|
43
48
|
if (!isDevEnvironment() || !enabled) {
|
|
44
49
|
return tree;
|
|
45
50
|
}
|
|
46
51
|
const stats = createRuntimeStats();
|
|
47
52
|
const context = {
|
|
48
|
-
tree,
|
|
49
|
-
config,
|
|
53
|
+
tree: tree,
|
|
54
|
+
config: config,
|
|
50
55
|
stats,
|
|
51
56
|
issues: [],
|
|
52
57
|
hotPaths: [],
|
|
@@ -98,6 +103,7 @@ function withGuardrails(config = {}) {
|
|
|
98
103
|
return tree;
|
|
99
104
|
};
|
|
100
105
|
}
|
|
106
|
+
const withGuardrails = Object.assign(guardrails, {});
|
|
101
107
|
function startChangeDetection(context) {
|
|
102
108
|
if (!context.config.changeDetection?.disablePathNotifier) {
|
|
103
109
|
try {
|
|
@@ -111,10 +117,13 @@ function startChangeDetection(context) {
|
|
|
111
117
|
} catch {}
|
|
112
118
|
}
|
|
113
119
|
try {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
const maybeSubscribe = context.tree.subscribe;
|
|
121
|
+
if (typeof maybeSubscribe === 'function') {
|
|
122
|
+
const unsubscribe = maybeSubscribe.call(context.tree, () => {
|
|
123
|
+
handleStateChange(context);
|
|
124
|
+
});
|
|
125
|
+
return unsubscribe;
|
|
126
|
+
}
|
|
118
127
|
} catch {}
|
|
119
128
|
return startPollingChangeDetection(context);
|
|
120
129
|
}
|
|
@@ -146,9 +155,8 @@ function handleStateChange(context) {
|
|
|
146
155
|
context.previousState = tryStructuredClone(currentState);
|
|
147
156
|
return;
|
|
148
157
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
if (currentJson !== previousJson) {
|
|
158
|
+
const equal = deepEqual(currentState, previousState);
|
|
159
|
+
if (!equal) {
|
|
152
160
|
const startTime = performance.now();
|
|
153
161
|
const timestamp = Date.now();
|
|
154
162
|
const changedPaths = detectChangedPaths(previousState, currentState);
|
|
@@ -614,4 +622,4 @@ function isComparableRecord(value) {
|
|
|
614
622
|
return isPlainObject(value);
|
|
615
623
|
}
|
|
616
624
|
|
|
617
|
-
export { withGuardrails };
|
|
625
|
+
export { guardrails, withGuardrails };
|
package/dist/noop.js
CHANGED
|
@@ -5,17 +5,17 @@ const noopRule = name => ({
|
|
|
5
5
|
message: '',
|
|
6
6
|
severity: 'info'
|
|
7
7
|
});
|
|
8
|
-
function
|
|
8
|
+
function guardrails(config = {}) {
|
|
9
9
|
return tree => {
|
|
10
10
|
return tree;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
const rules = {
|
|
14
|
-
noDeepNesting: () => noopRule('noop'),
|
|
14
|
+
noDeepNesting: (_maxDepth = 5) => noopRule('noop'),
|
|
15
15
|
noFunctionsInState: () => noopRule('noop'),
|
|
16
16
|
noCacheInPersistence: () => noopRule('noop'),
|
|
17
|
-
maxPayloadSize: () => noopRule('noop'),
|
|
18
|
-
noSensitiveData: () => noopRule('noop')
|
|
17
|
+
maxPayloadSize: (_maxKB = 100) => noopRule('noop'),
|
|
18
|
+
noSensitiveData: (_sensitiveKeys = ['password', 'token', 'secret', 'apiKey']) => noopRule('noop')
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export {
|
|
21
|
+
export { guardrails, rules };
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/guardrails",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Development-only performance monitoring and anti-pattern detection for SignalTree",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
|
-
"main": "./dist/
|
|
8
|
-
"module": "./dist/
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
9
|
"types": "./src/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./src/index.d.ts",
|
|
13
13
|
"development": {
|
|
14
|
-
"import": "./dist/
|
|
15
|
-
"default": "./dist/
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
16
|
},
|
|
17
17
|
"production": {
|
|
18
18
|
"import": "./dist/noop.js",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"type-check": "tsc --project tsconfig.lib.json --noEmit"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@signaltree/core": "^
|
|
60
|
+
"@signaltree/core": "^6.0.0",
|
|
61
|
+
"@signaltree/shared": "^6.0.0",
|
|
61
62
|
"tslib": "^2.0.0",
|
|
62
63
|
"vitest": "^2.0.0"
|
|
63
64
|
},
|
|
@@ -68,8 +69,7 @@
|
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@signaltree/core": "workspace:*",
|
|
71
|
-
"@signaltree/shared": "workspace:*"
|
|
72
|
-
"@signaltree/types": "workspace:*"
|
|
72
|
+
"@signaltree/shared": "workspace:*"
|
|
73
73
|
},
|
|
74
74
|
"keywords": [
|
|
75
75
|
"signaltree",
|
package/src/factories/index.d.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ISignalTree, TreeConfig, Enhancer } from '@signaltree/core';
|
|
2
2
|
import type { GuardrailsConfig } from '../lib/types';
|
|
3
|
-
type SignalTreeFactory<T extends Record<string, unknown>> = (initial: T, config?: TreeConfig) =>
|
|
4
|
-
type EnhancerFn<T extends Record<string, unknown>> = (tree: SignalTree<T>) => SignalTree<T>;
|
|
3
|
+
type SignalTreeFactory<T extends Record<string, unknown>> = (initial: T, config?: TreeConfig) => ISignalTree<T>;
|
|
5
4
|
interface FeatureTreeOptions<T extends Record<string, unknown>> {
|
|
6
5
|
name: string;
|
|
7
6
|
env?: 'development' | 'test' | 'staging' | 'production';
|
|
8
7
|
persistence?: boolean | Record<string, unknown>;
|
|
9
|
-
guardrails?: boolean | GuardrailsConfig
|
|
8
|
+
guardrails?: boolean | GuardrailsConfig<T>;
|
|
10
9
|
devtools?: boolean;
|
|
11
|
-
enhancers?:
|
|
10
|
+
enhancers?: Enhancer<unknown>[];
|
|
12
11
|
}
|
|
13
|
-
export declare function createFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: FeatureTreeOptions<T>):
|
|
14
|
-
export declare function createAngularFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: Omit<FeatureTreeOptions<T>, 'env'>):
|
|
15
|
-
export declare function createAppShellTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T):
|
|
16
|
-
export declare function createPerformanceTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, name: string):
|
|
17
|
-
export declare function createFormTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, formName: string):
|
|
18
|
-
export declare function createCacheTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T):
|
|
19
|
-
export declare function createTestTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, overrides?: Partial<GuardrailsConfig
|
|
12
|
+
export declare function createFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: FeatureTreeOptions<T>): ISignalTree<T>;
|
|
13
|
+
export declare function createAngularFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: Omit<FeatureTreeOptions<T>, 'env'>): ISignalTree<T>;
|
|
14
|
+
export declare function createAppShellTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): ISignalTree<T>;
|
|
15
|
+
export declare function createPerformanceTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, name: string): ISignalTree<T>;
|
|
16
|
+
export declare function createFormTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, formName: string): ISignalTree<T>;
|
|
17
|
+
export declare function createCacheTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): ISignalTree<T>;
|
|
18
|
+
export declare function createTestTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, overrides?: Partial<GuardrailsConfig<T>>): ISignalTree<T>;
|
|
20
19
|
export {};
|
package/src/index.d.ts
CHANGED
package/src/lib/guardrails.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { GuardrailsConfig } from './types';
|
|
3
|
-
export declare function
|
|
1
|
+
import type { ISignalTree } from '@signaltree/core';
|
|
2
|
+
import type { GuardrailsConfig, GuardrailsAPI } from './types';
|
|
3
|
+
export declare function guardrails(config?: GuardrailsConfig<any>): <Tree extends ISignalTree<any>>(tree: Tree) => Tree & {
|
|
4
|
+
__guardrails?: GuardrailsAPI;
|
|
5
|
+
};
|
|
6
|
+
export declare const withGuardrails: typeof guardrails;
|
package/src/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface GuardrailsConfig<T
|
|
1
|
+
import type { ISignalTree } from '@signaltree/core';
|
|
2
|
+
export interface GuardrailsConfig<T = Record<string, unknown>> {
|
|
3
3
|
mode?: 'warn' | 'throw' | 'silent';
|
|
4
4
|
enabled?: boolean | (() => boolean);
|
|
5
5
|
changeDetection?: {
|
|
@@ -57,7 +57,7 @@ export interface UpdateMetadata {
|
|
|
57
57
|
correlationId?: string;
|
|
58
58
|
[key: string]: unknown;
|
|
59
59
|
}
|
|
60
|
-
export interface GuardrailRule<T
|
|
60
|
+
export interface GuardrailRule<T = Record<string, unknown>> {
|
|
61
61
|
name: string;
|
|
62
62
|
description?: string;
|
|
63
63
|
test: (context: RuleContext<T>) => boolean | Promise<boolean>;
|
|
@@ -66,12 +66,12 @@ export interface GuardrailRule<T extends Record<string, unknown> = Record<string
|
|
|
66
66
|
fix?: (context: RuleContext<T>) => void;
|
|
67
67
|
tags?: string[];
|
|
68
68
|
}
|
|
69
|
-
export interface RuleContext<T
|
|
69
|
+
export interface RuleContext<T = Record<string, unknown>> {
|
|
70
70
|
path: string[];
|
|
71
71
|
value: unknown;
|
|
72
72
|
oldValue?: unknown;
|
|
73
73
|
metadata?: UpdateMetadata;
|
|
74
|
-
tree:
|
|
74
|
+
tree: ISignalTree<T>;
|
|
75
75
|
duration?: number;
|
|
76
76
|
diffRatio?: number;
|
|
77
77
|
recomputeCount?: number;
|
package/src/noop.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { SignalTree } from '@signaltree/core';
|
|
2
1
|
import type { GuardrailsConfig, GuardrailRule } from './lib/types';
|
|
3
|
-
export declare function
|
|
2
|
+
export declare function guardrails(config?: GuardrailsConfig<any>): <S>(tree: import("@signaltree/core").SignalTree<S>) => import("@signaltree/core").SignalTree<S>;
|
|
4
3
|
export declare const rules: {
|
|
5
|
-
noDeepNesting: () => GuardrailRule<Record<string, unknown>>;
|
|
4
|
+
noDeepNesting: (_maxDepth?: number) => GuardrailRule<Record<string, unknown>>;
|
|
6
5
|
noFunctionsInState: () => GuardrailRule<Record<string, unknown>>;
|
|
7
6
|
noCacheInPersistence: () => GuardrailRule<Record<string, unknown>>;
|
|
8
|
-
maxPayloadSize: () => GuardrailRule<Record<string, unknown>>;
|
|
9
|
-
noSensitiveData: () => GuardrailRule<Record<string, unknown>>;
|
|
7
|
+
maxPayloadSize: (_maxKB?: number) => GuardrailRule<Record<string, unknown>>;
|
|
8
|
+
noSensitiveData: (_sensitiveKeys?: string[]) => GuardrailRule<Record<string, unknown>>;
|
|
10
9
|
};
|
|
11
|
-
export
|
|
10
|
+
export * from './lib/types';
|
package/dist/lib/rules.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
const rules = {
|
|
2
|
-
noDeepNesting: (maxDepth = 5) => ({
|
|
3
|
-
name: 'no-deep-nesting',
|
|
4
|
-
description: `Prevents nesting deeper than ${maxDepth} levels`,
|
|
5
|
-
test: ctx => ctx.path.length <= maxDepth,
|
|
6
|
-
message: ctx => `Path too deep: ${ctx.path.join('.')} (${ctx.path.length} levels, max: ${maxDepth})`,
|
|
7
|
-
severity: 'warning',
|
|
8
|
-
tags: ['architecture', 'complexity']
|
|
9
|
-
}),
|
|
10
|
-
noFunctionsInState: () => ({
|
|
11
|
-
name: 'no-functions',
|
|
12
|
-
description: 'Functions break serialization',
|
|
13
|
-
test: ctx => typeof ctx.value !== 'function',
|
|
14
|
-
message: 'Functions cannot be stored in state (breaks serialization)',
|
|
15
|
-
severity: 'error',
|
|
16
|
-
tags: ['serialization', 'data']
|
|
17
|
-
}),
|
|
18
|
-
noCacheInPersistence: () => ({
|
|
19
|
-
name: 'no-cache-persistence',
|
|
20
|
-
description: 'Prevent cache from being persisted',
|
|
21
|
-
test: ctx => {
|
|
22
|
-
if (ctx.metadata?.source === 'serialization' && ctx.path.includes('cache')) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
return true;
|
|
26
|
-
},
|
|
27
|
-
message: 'Cache should not be persisted',
|
|
28
|
-
severity: 'warning',
|
|
29
|
-
tags: ['persistence', 'cache']
|
|
30
|
-
}),
|
|
31
|
-
maxPayloadSize: (maxKB = 100) => ({
|
|
32
|
-
name: 'max-payload-size',
|
|
33
|
-
description: `Limit payload size to ${maxKB}KB`,
|
|
34
|
-
test: ctx => {
|
|
35
|
-
try {
|
|
36
|
-
const size = JSON.stringify(ctx.value).length;
|
|
37
|
-
return size < maxKB * 1024;
|
|
38
|
-
} catch {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
message: ctx => {
|
|
43
|
-
const size = JSON.stringify(ctx.value).length;
|
|
44
|
-
const kb = (size / 1024).toFixed(1);
|
|
45
|
-
return `Payload size ${kb}KB exceeds limit of ${maxKB}KB`;
|
|
46
|
-
},
|
|
47
|
-
severity: 'warning',
|
|
48
|
-
tags: ['performance', 'data']
|
|
49
|
-
}),
|
|
50
|
-
noSensitiveData: (sensitiveKeys = ['password', 'token', 'secret', 'apiKey']) => ({
|
|
51
|
-
name: 'no-sensitive-data',
|
|
52
|
-
description: 'Prevents storing sensitive data',
|
|
53
|
-
test: ctx => {
|
|
54
|
-
return !ctx.path.some(segment => sensitiveKeys.some(key => typeof segment === 'string' && segment.toLowerCase().includes(key.toLowerCase())));
|
|
55
|
-
},
|
|
56
|
-
message: ctx => `Sensitive data detected in path: ${ctx.path.join('.')}`,
|
|
57
|
-
severity: 'error',
|
|
58
|
-
tags: ['security', 'data']
|
|
59
|
-
})
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export { rules };
|