@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 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 { withGuardrails } from '@signaltree/guardrails';
26
-
27
- const tree = signalTree({ count: 0 }).with(withGuardrails({
28
- budgets: { maxUpdateTime: 16 },
29
- hotPaths: { threshold: 10 },
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(signalTree, { data: [] }, {
40
- name: 'dashboard',
41
- guardrails: true,
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
@@ -1,5 +1,4 @@
1
- import { withGuardrails } from '../lib/guardrails.js';
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(withGuardrails(guardrailsConfig));
35
+ enhancers.push(guardrails(guardrailsConfig));
37
36
  }
38
37
  }
39
38
  if (options.enhancers?.length) {
40
39
  enhancers.push(...options.enhancers);
41
40
  }
42
- let tree = signalTree(initial);
41
+ const tree = signalTree(initial);
42
+ let enhanced = tree;
43
43
  for (const enhancer of enhancers) {
44
- tree = tree.with(enhancer);
44
+ enhanced = enhanced.with(enhancer);
45
45
  }
46
- return tree;
46
+ return enhanced;
47
47
  }
48
48
  function createAngularFeatureTree(signalTree, initial, options) {
49
49
  const isDev = Boolean(ngDevMode);
@@ -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
- return value;
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 withGuardrails(config = {}) {
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 unsubscribe = context.tree.subscribe(() => {
115
- handleStateChange(context);
116
- });
117
- return unsubscribe;
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 currentJson = JSON.stringify(currentState);
150
- const previousJson = JSON.stringify(previousState);
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 withGuardrails(config) {
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 { rules, withGuardrails };
21
+ export { guardrails, rules };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@signaltree/guardrails",
3
- "version": "5.1.6",
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/lib/guardrails.js",
8
- "module": "./dist/lib/guardrails.js",
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/lib/guardrails.js",
15
- "default": "./dist/lib/guardrails.js"
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": "^5.0.0",
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",
@@ -1,20 +1,19 @@
1
- import type { SignalTree, TreeConfig } from '@signaltree/core';
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) => SignalTree<T>;
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?: EnhancerFn<T>[];
10
+ enhancers?: Enhancer<unknown>[];
12
11
  }
13
- export declare function createFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: FeatureTreeOptions<T>): SignalTree<T>;
14
- export declare function createAngularFeatureTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, options: Omit<FeatureTreeOptions<T>, 'env'>): SignalTree<T>;
15
- export declare function createAppShellTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): SignalTree<T>;
16
- export declare function createPerformanceTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, name: string): SignalTree<T>;
17
- export declare function createFormTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, formName: string): SignalTree<T>;
18
- export declare function createCacheTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T): SignalTree<T>;
19
- export declare function createTestTree<T extends Record<string, unknown>>(signalTree: SignalTreeFactory<T>, initial: T, overrides?: Partial<GuardrailsConfig>): SignalTree<T>;
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
@@ -1,4 +1,4 @@
1
- import { withGuardrails } from './lib/guardrails';
1
+ import { guardrails } from './lib/guardrails';
2
2
  import { rules } from './lib/rules';
3
- export { withGuardrails, rules };
3
+ export { guardrails, rules };
4
4
  export type * from './lib/types';
@@ -1,3 +1,6 @@
1
- import type { SignalTree } from '@signaltree/core';
2
- import type { GuardrailsConfig } from './types';
3
- export declare function withGuardrails<T extends Record<string, unknown>>(config?: GuardrailsConfig<T>): (tree: SignalTree<T>) => SignalTree<T>;
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;
@@ -1,5 +1,5 @@
1
- import type { SignalTree } from '@signaltree/core';
2
- export interface GuardrailsConfig<T extends Record<string, unknown> = Record<string, unknown>> {
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 extends Record<string, unknown> = Record<string, unknown>> {
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 extends Record<string, unknown> = Record<string, unknown>> {
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: SignalTree<T>;
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 withGuardrails<T extends Record<string, unknown>>(config?: GuardrailsConfig): (tree: SignalTree<T>) => SignalTree<T>;
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 type * from './lib/types';
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 };