@signaltree/guardrails 4.0.16 → 4.1.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.
@@ -1,922 +0,0 @@
1
- 'use strict';
2
-
3
- var __defProp = Object.defineProperty;
4
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
-
6
- // src/lib/guardrails.ts
7
- function isFunction(value) {
8
- return typeof value === "function";
9
- }
10
- __name(isFunction, "isFunction");
11
- function isString(value) {
12
- return typeof value === "string";
13
- }
14
- __name(isString, "isString");
15
- function isObjectLike(value) {
16
- return typeof value === "object" && value !== null;
17
- }
18
- __name(isObjectLike, "isObjectLike");
19
- function resolveEnabledFlag(option) {
20
- if (option === void 0) {
21
- return true;
22
- }
23
- if (isFunction(option)) {
24
- try {
25
- return option();
26
- } catch {
27
- return true;
28
- }
29
- }
30
- return option;
31
- }
32
- __name(resolveEnabledFlag, "resolveEnabledFlag");
33
- function supportsMiddleware(tree) {
34
- const candidate = tree;
35
- return isFunction(candidate.addTap) && isFunction(candidate.removeTap);
36
- }
37
- __name(supportsMiddleware, "supportsMiddleware");
38
- function tryStructuredClone(value) {
39
- const cloneFn = globalThis.structuredClone;
40
- if (isFunction(cloneFn)) {
41
- try {
42
- return cloneFn(value);
43
- } catch {
44
- }
45
- }
46
- return value;
47
- }
48
- __name(tryStructuredClone, "tryStructuredClone");
49
- function isDevEnvironment() {
50
- if (__DEV__ !== void 0) return __DEV__;
51
- if (process?.env?.["NODE_ENV"] === "production") return false;
52
- if (ngDevMode != null) return Boolean(ngDevMode);
53
- return true;
54
- }
55
- __name(isDevEnvironment, "isDevEnvironment");
56
- var MAX_TIMING_SAMPLES = 1e3;
57
- var RECOMPUTATION_WINDOW_MS = 1e3;
58
- function withGuardrails(config = {}) {
59
- return (tree) => {
60
- const enabled = resolveEnabledFlag(config.enabled);
61
- if (!isDevEnvironment() || !enabled) {
62
- return tree;
63
- }
64
- if (!supportsMiddleware(tree)) {
65
- console.warn("[Guardrails] Tree does not expose middleware hooks; guardrails disabled.");
66
- return tree;
67
- }
68
- const stats = createRuntimeStats();
69
- const context = {
70
- tree,
71
- config,
72
- stats,
73
- issues: [],
74
- hotPaths: [],
75
- currentUpdate: null,
76
- suppressed: false,
77
- timings: [],
78
- hotPathData: /* @__PURE__ */ new Map(),
79
- issueMap: /* @__PURE__ */ new Map(),
80
- signalUsage: /* @__PURE__ */ new Map(),
81
- memoryHistory: [],
82
- recomputationLog: [],
83
- disposed: false
84
- };
85
- const middlewareId = `guardrails:${config.treeId ?? "tree"}:${Math.random().toString(36).slice(2)}`;
86
- context.middlewareId = middlewareId;
87
- const middleware = createGuardrailsMiddleware(context);
88
- tree.addTap(middleware);
89
- const stopMonitoring = startMonitoring(context);
90
- const teardown = /* @__PURE__ */ __name(() => {
91
- if (context.disposed) return;
92
- context.disposed = true;
93
- stopMonitoring();
94
- try {
95
- tree.removeTap(middlewareId);
96
- } catch {
97
- }
98
- }, "teardown");
99
- const originalDestroy = tree.destroy?.bind(tree);
100
- tree.destroy = () => {
101
- teardown();
102
- if (originalDestroy) {
103
- originalDestroy();
104
- }
105
- };
106
- tree["__guardrails"] = createAPI(context, teardown);
107
- return tree;
108
- };
109
- }
110
- __name(withGuardrails, "withGuardrails");
111
- function createRuntimeStats() {
112
- return {
113
- updateCount: 0,
114
- totalUpdateTime: 0,
115
- avgUpdateTime: 0,
116
- p50UpdateTime: 0,
117
- p95UpdateTime: 0,
118
- p99UpdateTime: 0,
119
- maxUpdateTime: 0,
120
- recomputationCount: 0,
121
- recomputationsPerSecond: 0,
122
- signalCount: 0,
123
- signalRetention: 0,
124
- unreadSignalCount: 0,
125
- memoryGrowthRate: 0,
126
- hotPathCount: 0,
127
- violationCount: 0
128
- };
129
- }
130
- __name(createRuntimeStats, "createRuntimeStats");
131
- function createGuardrailsMiddleware(context) {
132
- return {
133
- id: context.middlewareId ?? "guardrails",
134
- before: /* @__PURE__ */ __name((action, payload, state) => {
135
- if (context.suppressed) {
136
- context.currentUpdate = null;
137
- return !context.disposed;
138
- }
139
- const metadata = extractMetadata(payload);
140
- if (shouldSuppressUpdate(context, metadata)) {
141
- context.currentUpdate = null;
142
- return !context.disposed;
143
- }
144
- const details = collectUpdateDetails(payload, state);
145
- context.currentUpdate = {
146
- action,
147
- startTime: performance.now(),
148
- metadata,
149
- details
150
- };
151
- for (const detail of details) {
152
- analyzePreUpdate(context, detail, metadata);
153
- }
154
- return !context.disposed;
155
- }, "before"),
156
- after: /* @__PURE__ */ __name((_action, _payload, _previousState, newState) => {
157
- const pending = context.currentUpdate;
158
- if (!pending) return;
159
- const duration = Math.max(0, performance.now() - pending.startTime);
160
- const timestamp = Date.now();
161
- const recomputations = Math.max(0, pending.details.length - 1);
162
- updateTimingStats(context, duration);
163
- for (const [index, detail] of pending.details.entries()) {
164
- const latest = getValueAtPath(newState, detail.segments);
165
- const diffRatio = calculateDiffRatio(detail.oldValue, latest);
166
- analyzePostUpdate(context, detail, duration, diffRatio, index === 0);
167
- trackHotPath(context, detail.path, duration);
168
- trackSignalUsage(context, detail.path, timestamp);
169
- }
170
- updateSignalStats(context, timestamp);
171
- recordRecomputations(context, recomputations, timestamp);
172
- context.currentUpdate = null;
173
- }, "after")
174
- };
175
- }
176
- __name(createGuardrailsMiddleware, "createGuardrailsMiddleware");
177
- function updatePercentiles(context) {
178
- if (context.timings.length === 0) return;
179
- const sorted = [
180
- ...context.timings
181
- ].sort((a, b) => a - b);
182
- context.stats.p50UpdateTime = sorted[Math.floor(sorted.length * 0.5)] || 0;
183
- context.stats.p95UpdateTime = sorted[Math.floor(sorted.length * 0.95)] || 0;
184
- context.stats.p99UpdateTime = sorted[Math.floor(sorted.length * 0.99)] || 0;
185
- }
186
- __name(updatePercentiles, "updatePercentiles");
187
- function calculateDiffRatio(oldValue, newValue) {
188
- if (!isComparableRecord(oldValue) || !isComparableRecord(newValue)) {
189
- return Object.is(oldValue, newValue) ? 0 : 1;
190
- }
191
- if (oldValue === newValue) return 0;
192
- const oldKeys = new Set(Object.keys(oldValue));
193
- const newKeys = new Set(Object.keys(newValue));
194
- const allKeys = /* @__PURE__ */ new Set([
195
- ...oldKeys,
196
- ...newKeys
197
- ]);
198
- let changed = 0;
199
- for (const key of allKeys) {
200
- if (!oldKeys.has(key) || !newKeys.has(key) || oldValue[key] !== newValue[key]) {
201
- changed++;
202
- }
203
- }
204
- return allKeys.size === 0 ? 0 : changed / allKeys.size;
205
- }
206
- __name(calculateDiffRatio, "calculateDiffRatio");
207
- function analyzePreUpdate(context, detail, metadata) {
208
- if (!context.config.customRules) return;
209
- for (const rule of context.config.customRules) {
210
- evaluateRule(context, rule, {
211
- path: detail.segments,
212
- value: detail.newValue,
213
- oldValue: detail.oldValue,
214
- metadata,
215
- tree: context.tree,
216
- stats: context.stats
217
- });
218
- }
219
- }
220
- __name(analyzePreUpdate, "analyzePreUpdate");
221
- function analyzePostUpdate(context, detail, duration, diffRatio, isPrimary) {
222
- if (isPrimary && context.config.budgets?.maxUpdateTime && duration > context.config.budgets.maxUpdateTime) {
223
- addIssue(context, {
224
- type: "budget",
225
- severity: "error",
226
- message: `Update took ${duration.toFixed(2)}ms (budget: ${context.config.budgets.maxUpdateTime}ms)`,
227
- path: detail.path,
228
- count: 1
229
- });
230
- }
231
- const minDiff = context.config.analysis?.minDiffForParentReplace ?? 0.8;
232
- if (context.config.analysis?.warnParentReplace && diffRatio > minDiff) {
233
- addIssue(context, {
234
- type: "analysis",
235
- severity: "warning",
236
- message: `High diff ratio (${(diffRatio * 100).toFixed(0)}%) - consider scoped updates`,
237
- path: detail.path,
238
- count: 1,
239
- diffRatio
240
- });
241
- }
242
- }
243
- __name(analyzePostUpdate, "analyzePostUpdate");
244
- function trackHotPath(context, path, duration) {
245
- if (!context.config.hotPaths?.enabled) return;
246
- const pathKey = Array.isArray(path) ? path.join(".") : path;
247
- const now = Date.now();
248
- const windowMs = context.config.hotPaths.windowMs || 1e3;
249
- let data = context.hotPathData.get(pathKey);
250
- if (!data) {
251
- data = {
252
- count: 0,
253
- lastUpdate: now,
254
- durations: []
255
- };
256
- context.hotPathData.set(pathKey, data);
257
- }
258
- if (now - data.lastUpdate > windowMs) {
259
- data.count = 0;
260
- data.durations = [];
261
- }
262
- data.count++;
263
- data.durations.push(duration);
264
- data.lastUpdate = now;
265
- const threshold = context.config.hotPaths.threshold || 10;
266
- const updatesPerSecond = data.count / windowMs * 1e3;
267
- if (updatesPerSecond > threshold) {
268
- const sorted = [
269
- ...data.durations
270
- ].sort((a, b) => a - b);
271
- const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
272
- const avg = data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length;
273
- updateHotPath(context, {
274
- path: pathKey,
275
- updatesPerSecond,
276
- heatScore: Math.min(100, updatesPerSecond / threshold * 50),
277
- downstreamEffects: 0,
278
- avgDuration: avg,
279
- p95Duration: p95
280
- });
281
- }
282
- }
283
- __name(trackHotPath, "trackHotPath");
284
- function trackSignalUsage(context, path, timestamp) {
285
- const key = Array.isArray(path) ? path.join(".") : path;
286
- const entry = context.signalUsage.get(key) ?? {
287
- updates: 0,
288
- lastSeen: timestamp
289
- };
290
- entry.updates += 1;
291
- entry.lastSeen = timestamp;
292
- context.signalUsage.set(key, entry);
293
- }
294
- __name(trackSignalUsage, "trackSignalUsage");
295
- function updateSignalStats(context, timestamp) {
296
- const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5e3;
297
- const historyWindow = Math.max(retentionWindow, 1e3);
298
- const signalCount = context.signalUsage.size;
299
- context.stats.signalCount = signalCount;
300
- const staleCount = [
301
- ...context.signalUsage.values()
302
- ].filter((entry) => timestamp - entry.lastSeen > retentionWindow).length;
303
- context.stats.signalRetention = staleCount;
304
- context.stats.unreadSignalCount = 0;
305
- context.memoryHistory.push({
306
- timestamp,
307
- count: signalCount
308
- });
309
- context.memoryHistory = context.memoryHistory.filter((entry) => timestamp - entry.timestamp <= historyWindow);
310
- const baseline = context.memoryHistory[0]?.count ?? signalCount;
311
- const growth = baseline === 0 ? 0 : (signalCount - baseline) / Math.max(1, baseline);
312
- context.stats.memoryGrowthRate = growth;
313
- }
314
- __name(updateSignalStats, "updateSignalStats");
315
- function recordRecomputations(context, count, timestamp) {
316
- if (count > 0) {
317
- context.stats.recomputationCount += count;
318
- for (let i = 0; i < count; i++) {
319
- context.recomputationLog.push(timestamp);
320
- }
321
- }
322
- if (context.recomputationLog.length) {
323
- context.recomputationLog = context.recomputationLog.filter((value) => timestamp - value <= RECOMPUTATION_WINDOW_MS);
324
- }
325
- context.stats.recomputationsPerSecond = context.recomputationLog.length;
326
- }
327
- __name(recordRecomputations, "recordRecomputations");
328
- function updateHotPath(context, hotPath) {
329
- const existing = context.hotPaths.find((h) => h.path === hotPath.path);
330
- if (existing) {
331
- Object.assign(existing, hotPath);
332
- } else {
333
- context.hotPaths.push(hotPath);
334
- const topN = context.config.hotPaths?.topN || 5;
335
- if (context.hotPaths.length > topN) {
336
- context.hotPaths.sort((a, b) => b.heatScore - a.heatScore);
337
- context.hotPaths.length = topN;
338
- }
339
- }
340
- context.stats.hotPathCount = context.hotPaths.length;
341
- }
342
- __name(updateHotPath, "updateHotPath");
343
- function evaluateRule(context, rule, ruleContext) {
344
- const handleFailure = /* @__PURE__ */ __name(() => {
345
- const message = typeof rule.message === "function" ? rule.message(ruleContext) : rule.message;
346
- addIssue(context, {
347
- type: "rule",
348
- severity: rule.severity || "warning",
349
- message,
350
- path: ruleContext.path.join("."),
351
- count: 1,
352
- metadata: {
353
- rule: rule.name
354
- }
355
- });
356
- }, "handleFailure");
357
- try {
358
- const result = rule.test(ruleContext);
359
- if (result instanceof Promise) {
360
- result.then((outcome) => {
361
- if (!outcome) {
362
- handleFailure();
363
- }
364
- }).catch((error) => {
365
- console.warn(`[Guardrails] Rule ${rule.name} rejected:`, error);
366
- });
367
- return;
368
- }
369
- if (!result) {
370
- handleFailure();
371
- }
372
- } catch (error) {
373
- console.warn(`[Guardrails] Rule ${rule.name} threw error:`, error);
374
- }
375
- }
376
- __name(evaluateRule, "evaluateRule");
377
- function addIssue(context, issue) {
378
- if (context.suppressed) return;
379
- if (context.config.reporting?.aggregateWarnings !== false) {
380
- const key = `${issue.type}:${issue.path}:${issue.message}`;
381
- const existing = context.issueMap.get(key);
382
- if (existing) {
383
- existing.count++;
384
- return;
385
- }
386
- context.issueMap.set(key, issue);
387
- }
388
- context.issues.push(issue);
389
- context.stats.violationCount++;
390
- if (context.config.mode === "throw") {
391
- throw new Error(`[Guardrails] ${issue.message}`);
392
- }
393
- }
394
- __name(addIssue, "addIssue");
395
- function shouldSuppressUpdate(context, metadata) {
396
- if (context.suppressed) return true;
397
- if (!metadata) return false;
398
- if (metadata.suppressGuardrails && context.config.suppression?.respectMetadata !== false) {
399
- return true;
400
- }
401
- const autoSuppress = new Set(context.config.suppression?.autoSuppress ?? []);
402
- return [
403
- metadata.intent,
404
- metadata.source
405
- ].some((value) => isString(value) && autoSuppress.has(value));
406
- }
407
- __name(shouldSuppressUpdate, "shouldSuppressUpdate");
408
- function startMonitoring(context) {
409
- const interval = setInterval(() => {
410
- if (context.disposed) {
411
- clearInterval(interval);
412
- return;
413
- }
414
- checkMemory(context);
415
- maybeReport(context);
416
- }, context.config.reporting?.interval || 5e3);
417
- return () => clearInterval(interval);
418
- }
419
- __name(startMonitoring, "startMonitoring");
420
- function checkMemory(context) {
421
- if (!context.config.memoryLeaks?.enabled) return;
422
- const now = Date.now();
423
- const retentionWindow = context.config.memoryLeaks?.checkInterval ?? 5e3;
424
- const retentionThreshold = context.config.memoryLeaks?.retentionThreshold ?? 100;
425
- const growthThreshold = context.config.memoryLeaks?.growthRate ?? 0.2;
426
- const staleCount = [
427
- ...context.signalUsage.values()
428
- ].filter((entry) => now - entry.lastSeen > retentionWindow).length;
429
- context.stats.signalRetention = staleCount;
430
- const exceedsRetention = context.stats.signalRetention > retentionThreshold;
431
- const exceedsGrowth = context.stats.memoryGrowthRate > growthThreshold;
432
- if (exceedsRetention || exceedsGrowth) {
433
- addIssue(context, {
434
- type: "memory",
435
- severity: "warning",
436
- message: `Potential memory leak detected (signals: ${context.stats.signalCount}, growth ${(context.stats.memoryGrowthRate * 100).toFixed(1)}%)`,
437
- path: "root",
438
- count: 1,
439
- metadata: {
440
- signalCount: context.stats.signalCount,
441
- growth: context.stats.memoryGrowthRate
442
- }
443
- });
444
- }
445
- }
446
- __name(checkMemory, "checkMemory");
447
- function maybeReport(context) {
448
- if (context.config.reporting?.console === false) return;
449
- const report = generateReport(context);
450
- if (context.config.reporting?.customReporter) {
451
- context.config.reporting.customReporter(report);
452
- }
453
- if (context.config.reporting?.console && context.issues.length > 0) {
454
- reportToConsole(report, context.config.reporting.console === "verbose");
455
- }
456
- context.issues = [];
457
- context.issueMap.clear();
458
- }
459
- __name(maybeReport, "maybeReport");
460
- function reportToConsole(report, verbose) {
461
- console.group("[Guardrails] Performance Report");
462
- logIssues(report.issues);
463
- logHotPaths(report.hotPaths);
464
- if (verbose) {
465
- logVerboseStats(report);
466
- }
467
- console.groupEnd();
468
- }
469
- __name(reportToConsole, "reportToConsole");
470
- function logIssues(issues) {
471
- if (issues.length === 0) return;
472
- console.warn(`${issues.length} issues detected:`);
473
- for (const issue of issues) {
474
- const prefix = getSeverityPrefix(issue.severity);
475
- const countSuffix = issue.count > 1 ? ` (x${issue.count})` : "";
476
- const message = `${prefix} [${issue.type}] ${issue.message}${countSuffix}`;
477
- console.log(` ${message}`);
478
- }
479
- }
480
- __name(logIssues, "logIssues");
481
- function logHotPaths(hotPaths) {
482
- if (hotPaths.length === 0) return;
483
- console.log(`
484
- Hot Paths (${hotPaths.length}):`);
485
- for (const hp of hotPaths) {
486
- const entry = ` \u{1F525} ${hp.path}: ${hp.updatesPerSecond.toFixed(1)}/s (heat: ${hp.heatScore.toFixed(0)})`;
487
- console.log(entry);
488
- }
489
- }
490
- __name(logHotPaths, "logHotPaths");
491
- function logVerboseStats(report) {
492
- console.log("\nStats:", report.stats);
493
- console.log("Budgets:", report.budgets);
494
- }
495
- __name(logVerboseStats, "logVerboseStats");
496
- function getSeverityPrefix(severity) {
497
- if (severity === "error") return "\u274C";
498
- if (severity === "warning") return "\u26A0\uFE0F";
499
- return "\u2139\uFE0F";
500
- }
501
- __name(getSeverityPrefix, "getSeverityPrefix");
502
- function generateReport(context) {
503
- const memoryCurrent = context.stats.signalCount;
504
- const memoryLimit = context.config.budgets?.maxMemory ?? 50;
505
- const recomputationCurrent = context.stats.recomputationsPerSecond;
506
- const recomputationLimit = context.config.budgets?.maxRecomputations ?? 100;
507
- const budgets = {
508
- updateTime: createBudgetItem(context.stats.avgUpdateTime, context.config.budgets?.maxUpdateTime || 16),
509
- memory: createBudgetItem(memoryCurrent, memoryLimit),
510
- recomputations: createBudgetItem(recomputationCurrent, recomputationLimit)
511
- };
512
- return {
513
- timestamp: Date.now(),
514
- treeId: context.config.treeId,
515
- issues: Array.from(context.issueMap.values()),
516
- hotPaths: context.hotPaths,
517
- budgets,
518
- stats: context.stats,
519
- recommendations: generateRecommendations(context)
520
- };
521
- }
522
- __name(generateReport, "generateReport");
523
- function createBudgetItem(current, limit) {
524
- if (limit <= 0) {
525
- return {
526
- current,
527
- limit,
528
- usage: 0,
529
- status: "ok"
530
- };
531
- }
532
- const usage = current / limit * 100;
533
- const threshold = 80;
534
- let status;
535
- if (usage > 100) {
536
- status = "exceeded";
537
- } else if (usage > threshold) {
538
- status = "warning";
539
- } else {
540
- status = "ok";
541
- }
542
- return {
543
- current,
544
- limit,
545
- usage,
546
- status
547
- };
548
- }
549
- __name(createBudgetItem, "createBudgetItem");
550
- function generateRecommendations(context) {
551
- const recommendations = [];
552
- if (context.hotPaths.length > 0) {
553
- recommendations.push("Consider batching or debouncing updates to hot paths");
554
- }
555
- if (context.stats.avgUpdateTime > 10) {
556
- recommendations.push("Average update time is high - review update logic");
557
- }
558
- return recommendations;
559
- }
560
- __name(generateRecommendations, "generateRecommendations");
561
- function createAPI(context, teardown) {
562
- return {
563
- getReport: /* @__PURE__ */ __name(() => generateReport(context), "getReport"),
564
- getStats: /* @__PURE__ */ __name(() => context.stats, "getStats"),
565
- suppress: /* @__PURE__ */ __name((fn) => {
566
- const was = context.suppressed;
567
- context.suppressed = true;
568
- try {
569
- fn();
570
- } finally {
571
- context.suppressed = was;
572
- }
573
- }, "suppress"),
574
- dispose: /* @__PURE__ */ __name(() => {
575
- teardown();
576
- const finalReport = generateReport(context);
577
- if (context.config.reporting?.console !== false) {
578
- console.log("[Guardrails] Final report on disposal:", finalReport);
579
- }
580
- if (context.config.reporting?.customReporter) {
581
- context.config.reporting.customReporter(finalReport);
582
- }
583
- }, "dispose")
584
- };
585
- }
586
- __name(createAPI, "createAPI");
587
- function extractMetadata(payload) {
588
- if (!isObjectLike(payload)) return void 0;
589
- const candidate = payload["metadata"];
590
- return isObjectLike(candidate) ? candidate : void 0;
591
- }
592
- __name(extractMetadata, "extractMetadata");
593
- function collectUpdateDetails(payload, stateSnapshot) {
594
- const details = [];
595
- const visit = /* @__PURE__ */ __name((value, segments, currentState) => {
596
- const path = segments.length ? segments.join(".") : "root";
597
- const oldValue = captureValue(currentState);
598
- if (isObjectLike(value)) {
599
- details.push({
600
- path,
601
- segments: [
602
- ...segments
603
- ],
604
- newValue: value,
605
- oldValue
606
- });
607
- for (const [key, child] of Object.entries(value)) {
608
- visit(child, [
609
- ...segments,
610
- key
611
- ], isObjectLike(currentState) ? currentState[key] : void 0);
612
- }
613
- return;
614
- }
615
- details.push({
616
- path,
617
- segments: [
618
- ...segments
619
- ],
620
- newValue: value,
621
- oldValue
622
- });
623
- }, "visit");
624
- if (isObjectLike(payload)) {
625
- visit(payload, [], stateSnapshot);
626
- } else {
627
- details.push({
628
- path: "root",
629
- segments: [],
630
- newValue: payload,
631
- oldValue: captureValue(stateSnapshot)
632
- });
633
- }
634
- if (details.length === 0) {
635
- details.push({
636
- path: "root",
637
- segments: [],
638
- newValue: payload,
639
- oldValue: captureValue(stateSnapshot)
640
- });
641
- }
642
- return details;
643
- }
644
- __name(collectUpdateDetails, "collectUpdateDetails");
645
- function getValueAtPath(source, segments) {
646
- let current = source;
647
- for (const segment of segments) {
648
- if (!isObjectLike(current)) {
649
- return void 0;
650
- }
651
- current = current[segment];
652
- }
653
- return current;
654
- }
655
- __name(getValueAtPath, "getValueAtPath");
656
- function captureValue(value) {
657
- return tryStructuredClone(value);
658
- }
659
- __name(captureValue, "captureValue");
660
- function isPlainObject(value) {
661
- if (!value || typeof value !== "object") return false;
662
- const proto = Object.getPrototypeOf(value);
663
- return proto === Object.prototype || proto === null;
664
- }
665
- __name(isPlainObject, "isPlainObject");
666
- function updateTimingStats(context, duration) {
667
- context.timings.push(duration);
668
- if (context.timings.length > MAX_TIMING_SAMPLES) {
669
- context.timings.shift();
670
- }
671
- context.stats.updateCount++;
672
- context.stats.totalUpdateTime += duration;
673
- context.stats.avgUpdateTime = context.stats.totalUpdateTime / context.stats.updateCount;
674
- context.stats.maxUpdateTime = Math.max(context.stats.maxUpdateTime, duration);
675
- updatePercentiles(context);
676
- }
677
- __name(updateTimingStats, "updateTimingStats");
678
- function isComparableRecord(value) {
679
- return isPlainObject(value);
680
- }
681
- __name(isComparableRecord, "isComparableRecord");
682
-
683
- // src/lib/rules.ts
684
- var rules = {
685
- /**
686
- * Prevents deep nesting beyond specified depth
687
- */
688
- noDeepNesting: /* @__PURE__ */ __name((maxDepth = 5) => ({
689
- name: "no-deep-nesting",
690
- description: `Prevents nesting deeper than ${maxDepth} levels`,
691
- test: /* @__PURE__ */ __name((ctx) => ctx.path.length <= maxDepth, "test"),
692
- message: /* @__PURE__ */ __name((ctx) => `Path too deep: ${ctx.path.join(".")} (${ctx.path.length} levels, max: ${maxDepth})`, "message"),
693
- severity: "warning",
694
- tags: [
695
- "architecture",
696
- "complexity"
697
- ]
698
- }), "noDeepNesting"),
699
- /**
700
- * Prevents storing functions in state (breaks serialization)
701
- */
702
- noFunctionsInState: /* @__PURE__ */ __name(() => ({
703
- name: "no-functions",
704
- description: "Functions break serialization",
705
- test: /* @__PURE__ */ __name((ctx) => typeof ctx.value !== "function", "test"),
706
- message: "Functions cannot be stored in state (breaks serialization)",
707
- severity: "error",
708
- tags: [
709
- "serialization",
710
- "data"
711
- ]
712
- }), "noFunctionsInState"),
713
- /**
714
- * Prevents cache from being persisted
715
- */
716
- noCacheInPersistence: /* @__PURE__ */ __name(() => ({
717
- name: "no-cache-persistence",
718
- description: "Prevent cache from being persisted",
719
- test: /* @__PURE__ */ __name((ctx) => {
720
- if (ctx.metadata?.source === "serialization" && ctx.path.includes("cache")) {
721
- return false;
722
- }
723
- return true;
724
- }, "test"),
725
- message: "Cache should not be persisted",
726
- severity: "warning",
727
- tags: [
728
- "persistence",
729
- "cache"
730
- ]
731
- }), "noCacheInPersistence"),
732
- /**
733
- * Limits payload size
734
- */
735
- maxPayloadSize: /* @__PURE__ */ __name((maxKB = 100) => ({
736
- name: "max-payload-size",
737
- description: `Limit payload size to ${maxKB}KB`,
738
- test: /* @__PURE__ */ __name((ctx) => {
739
- try {
740
- const size = JSON.stringify(ctx.value).length;
741
- return size < maxKB * 1024;
742
- } catch {
743
- return true;
744
- }
745
- }, "test"),
746
- message: /* @__PURE__ */ __name((ctx) => {
747
- const size = JSON.stringify(ctx.value).length;
748
- const kb = (size / 1024).toFixed(1);
749
- return `Payload size ${kb}KB exceeds limit of ${maxKB}KB`;
750
- }, "message"),
751
- severity: "warning",
752
- tags: [
753
- "performance",
754
- "data"
755
- ]
756
- }), "maxPayloadSize"),
757
- /**
758
- * Prevents storing sensitive data
759
- */
760
- noSensitiveData: /* @__PURE__ */ __name((sensitiveKeys = [
761
- "password",
762
- "token",
763
- "secret",
764
- "apiKey"
765
- ]) => ({
766
- name: "no-sensitive-data",
767
- description: "Prevents storing sensitive data",
768
- test: /* @__PURE__ */ __name((ctx) => {
769
- return !ctx.path.some((segment) => sensitiveKeys.some((key) => typeof segment === "string" && segment.toLowerCase().includes(key.toLowerCase())));
770
- }, "test"),
771
- message: /* @__PURE__ */ __name((ctx) => `Sensitive data detected in path: ${ctx.path.join(".")}`, "message"),
772
- severity: "error",
773
- tags: [
774
- "security",
775
- "data"
776
- ]
777
- }), "noSensitiveData")
778
- };
779
-
780
- // src/factories/index.ts
781
- function isGuardrailsConfig(value) {
782
- return Boolean(value) && typeof value === "object";
783
- }
784
- __name(isGuardrailsConfig, "isGuardrailsConfig");
785
- function resolveGuardrailsConfig(guardrails) {
786
- if (guardrails === false) {
787
- return void 0;
788
- }
789
- if (isGuardrailsConfig(guardrails)) {
790
- return guardrails;
791
- }
792
- return {
793
- budgets: {
794
- maxUpdateTime: 16,
795
- maxRecomputations: 100
796
- },
797
- hotPaths: {
798
- enabled: true,
799
- threshold: 10
800
- },
801
- reporting: {
802
- console: true
803
- }
804
- };
805
- }
806
- __name(resolveGuardrailsConfig, "resolveGuardrailsConfig");
807
- function createFeatureTree(signalTree, initial, options) {
808
- const env = options.env ?? process?.env?.["NODE_ENV"] ?? "production";
809
- const isDev = env === "development";
810
- const isTest = env === "test";
811
- const enhancers = [];
812
- if (isDev || isTest) {
813
- const guardrailsConfig = resolveGuardrailsConfig(options.guardrails);
814
- if (guardrailsConfig) {
815
- enhancers.push(withGuardrails(guardrailsConfig));
816
- }
817
- }
818
- if (options.enhancers?.length) {
819
- enhancers.push(...options.enhancers);
820
- }
821
- let tree = signalTree(initial);
822
- for (const enhancer of enhancers) {
823
- tree = tree.with(enhancer);
824
- }
825
- return tree;
826
- }
827
- __name(createFeatureTree, "createFeatureTree");
828
- function createAngularFeatureTree(signalTree, initial, options) {
829
- const isDev = Boolean(ngDevMode);
830
- return createFeatureTree(signalTree, initial, {
831
- ...options,
832
- env: isDev ? "development" : "production"
833
- });
834
- }
835
- __name(createAngularFeatureTree, "createAngularFeatureTree");
836
- function createAppShellTree(signalTree, initial) {
837
- return createFeatureTree(signalTree, initial, {
838
- guardrails: {
839
- budgets: {
840
- maxUpdateTime: 4,
841
- maxMemory: 20
842
- },
843
- hotPaths: {
844
- threshold: 5
845
- },
846
- customRules: [
847
- rules.noDeepNesting(3)
848
- ]
849
- }
850
- });
851
- }
852
- __name(createAppShellTree, "createAppShellTree");
853
- function createPerformanceTree(signalTree, initial, name) {
854
- return createFeatureTree(signalTree, initial, {
855
- guardrails: {
856
- budgets: {
857
- maxUpdateTime: 8,
858
- maxRecomputations: 200
859
- },
860
- hotPaths: {
861
- threshold: 50
862
- },
863
- memoryLeaks: {
864
- enabled: false
865
- },
866
- reporting: {
867
- interval: 1e4
868
- }
869
- }
870
- });
871
- }
872
- __name(createPerformanceTree, "createPerformanceTree");
873
- function createFormTree(signalTree, initial, formName) {
874
- return createFeatureTree(signalTree, initial, {
875
- guardrails: {
876
- customRules: [
877
- rules.noDeepNesting(4),
878
- rules.maxPayloadSize(50),
879
- rules.noSensitiveData()
880
- ]
881
- }
882
- });
883
- }
884
- __name(createFormTree, "createFormTree");
885
- function createCacheTree(signalTree, initial) {
886
- return createFeatureTree(signalTree, initial, {
887
- guardrails: {
888
- mode: "silent",
889
- memoryLeaks: {
890
- enabled: false
891
- }
892
- }});
893
- }
894
- __name(createCacheTree, "createCacheTree");
895
- function createTestTree(signalTree, initial, overrides) {
896
- return createFeatureTree(signalTree, initial, {
897
- env: "test",
898
- guardrails: {
899
- mode: "throw",
900
- budgets: {
901
- maxUpdateTime: 5,
902
- maxRecomputations: 50
903
- },
904
- customRules: [
905
- rules.noFunctionsInState(),
906
- rules.noDeepNesting(4)
907
- ],
908
- ...overrides
909
- }
910
- });
911
- }
912
- __name(createTestTree, "createTestTree");
913
-
914
- exports.createAngularFeatureTree = createAngularFeatureTree;
915
- exports.createAppShellTree = createAppShellTree;
916
- exports.createCacheTree = createCacheTree;
917
- exports.createFeatureTree = createFeatureTree;
918
- exports.createFormTree = createFormTree;
919
- exports.createPerformanceTree = createPerformanceTree;
920
- exports.createTestTree = createTestTree;
921
- //# sourceMappingURL=index.cjs.map
922
- //# sourceMappingURL=index.cjs.map